Files
archived-vdo.ninja/recorder/index.html
2025-01-10 18:12:23 -05:00

387 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Media Recorder and Playback Tool | Record and Test Your Audio and Video</title>
<meta name="description" content="Use our Media Recorder and Playback Tool to easily record video and audio snippets without ads, sign-ins, or payments. Perfect for testing audio loudness and synchronization.">
<meta name="keywords" content="media recorder, video recording, audio recording, test audio, test video, audio loudness, audio synchronization, no ads, no sign-in, no payment">
<meta name="author" content="VDO.Ninja">
<link rel="canonical" href="https://vdo.ninja/recorder">
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<link id="favicon1" rel="icon" type="image/png" sizes="32x32" href="../media/favicon-32x32.png" />
<link id="favicon2" rel="icon" type="image/png" sizes="16x16" href="../media/favicon-16x16.png" />
<link id="favicon3" rel="icon" href="../media/favicon.ico" />
<link id="thumbnailUrl" itemprop="thumbnailUrl" href="../media/vdoNinja_logo_full.png" />
<style>
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background-color: #2b2b2b;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #e0e0e0;
}
#container {
text-align: center;
width: 90%;
max-width: 800px;
padding: 20px;
background: #3a3a3a;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
h1 {
font-size: 24px;
margin-bottom: 15px;
color: #ffffff;
}
#video {
display: block;
margin: 10px auto;
max-width: 100%;
max-height: 50vh;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
#audio-meter {
width: 100%;
height: 20px;
background: #444;
margin-top: 10px;
border-radius: 10px;
overflow: hidden;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
}
#meter-fill {
width: 0%;
height: 100%;
background: linear-gradient(90deg, #4caf50, #76ff0322);
border-radius: 10px;
}
#controls {
margin-top: 15px;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
select, button {
margin: 5px;
padding: 8px 15px;
font-size: 14px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #007bff;
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: background-color 0.2s;
}
button:hover {
background-color: #0056b3;
}
select {
background-color: #555;
color: #e0e0e0;
padding: 8px;
border: 1px solid #666;
}
.recording-indicator {
color: red;
animation: pulse 1s infinite;
margin-top: 10px;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.hidden {
display: none;
}
@media (max-width: 600px) {
#container {
padding: 10px;
}
h1 {
font-size: 20px;
}
select, button {
padding: 6px 12px;
font-size: 12px;
}
}
@media (max-height: 500px) {
#container {
height: auto;
overflow-y: auto;
}
#video {
max-height: 40vh;
}
}
#video {
background-color: #000;
}
</style>
</head>
<body>
<div id="container">
<h1>Media Recorder and Playback Tool</h1>
<select id="videoSource"></select>
<select id="audioSource"></select>
<video id="video" autoplay muted></video>
<div id="audio-meter">
<div id="meter-fill"></div>
</div>
<div id="controls">
<button id="record" disabled>Record</button>
<button id="play" class="hidden">Play</button>
<button id="download" class="hidden">Download</button>
<div id="recording-indicator" class="hidden recording-indicator">Recording...</div>
</div>
</div>
<script>
const videoElement = document.getElementById('video');
const recordButton = document.getElementById('record');
const playButton = document.getElementById('play');
const downloadButton = document.getElementById('download');
const meterFill = document.getElementById('meter-fill');
const videoSelect = document.getElementById('videoSource');
const audioSelect = document.getElementById('audioSource');
const recordingIndicator = document.getElementById('recording-indicator');
let mediaStream = null;
let mediaRecorder = null;
let recordedChunks = [];
let audioContext = null;
let meter = null;
let sourceNode = null;
let recordedBlob = null;
(function (w) {
w.URLSearchParams =
w.URLSearchParams ||
function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp("[\?&]" + name + "=([^&#]*)").exec(self.searchString);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlParams = new URLSearchParams(window.location.search);
var denoise = false;
if (urlParams.has("denoise")){
denoise = true;
console.log("De-noising enabled");
}
var autogain = true;
if (urlParams.has("noautogain")){
autogain = false;
console.log("Auto-gain disabled");
}
async function init() {
try {
await getDevices();
await startMedia().catch(e => {
console.warn('Initial media start failed:', e);
// Still allow device selection even if initial device is busy
recordButton.setAttribute("disabled", "true");
videoElement.srcObject = null;
videoElement.poster = "camera-off.png"; // Optional: show offline state
});
} catch (error) {
console.error('Error initializing:', error);
}
}
async function getDevices() {
try {
// Request initial permissions - required for Firefox
await navigator.mediaDevices.getUserMedia({ audio: true, video: true })
.then(stream => stream.getTracks().forEach(track => track.stop()))
.catch(e => console.warn('Permission denied:', e));
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');
const audioDevices = devices.filter(device => device.kind === 'audioinput');
// Only update selects if we have device labels (permissions granted)
if (devices.some(device => device.label)) {
videoSelect.innerHTML = videoDevices.map(device =>
`<option value="${device.deviceId}">${device.label || `Video Device ${videoDevices.indexOf(device) + 1}`}</option>`
).join('');
audioSelect.innerHTML = audioDevices.map(device =>
`<option value="${device.deviceId}">${device.label || `Audio Device ${audioDevices.indexOf(device) + 1}`}</option>`
).join('');
}
} catch (error) {
console.error('Error getting devices:', error);
}
}
async function startMedia() {
try {
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
}
const videoSource = videoSelect.value;
const audioSource = audioSelect.value;
// Try video and audio separately to handle partial failures
try {
mediaStream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: videoSource ? { exact: videoSource } : undefined },
audio: {
deviceId: audioSource ? { exact: audioSource } : undefined,
echoCancellation: false,
noiseSuppression: denoise,
autoGainControl: autogain
}
});
} catch (e) {
// If full request fails, try audio-only
console.warn('Full media request failed, trying audio-only:', e);
mediaStream = await navigator.mediaDevices.getUserMedia({
video: false,
audio: {
deviceId: audioSource ? { exact: audioSource } : undefined,
echoCancellation: false,
noiseSuppression: denoise,
autoGainControl: autogain
}
});
}
if (mediaStream.getVideoTracks().length > 0) {
videoElement.srcObject = mediaStream;
recordButton.removeAttribute("disabled");
} else {
videoElement.srcObject = null;
videoElement.poster = "camera-off.png"; // Optional: show offline state
recordButton.setAttribute("disabled", "true");
}
if (audioContext) {
audioContext.close();
}
audioContext = new (window.AudioContext || window.webkitAudioContext)();
await audioContext.audioWorklet.addModule('testrecord-processor.js');
const audioTracks = mediaStream.getAudioTracks();
if (audioTracks.length > 0) {
sourceNode = audioContext.createMediaStreamSource(mediaStream);
meter = new AudioWorkletNode(audioContext, 'meter');
meter.port.onmessage = (event) => {
const volume = event.data;
meterFill.style.width = `${Math.min(volume * 100, 100)}%`;
};
sourceNode.connect(meter).connect(audioContext.destination);
}
} catch (error) {
console.error('Error starting media:', error);
throw error; // Re-throw to allow handling by caller
}
}
recordButton.addEventListener('click', async () => {
try {
await audioContext.resume();
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
recordButton.textContent = 'Record';
recordingIndicator.classList.add('hidden');
videoSelect.removeAttribute("disabled");
audioSelect.removeAttribute("disabled");
} else {
// Show live camera feed again
videoElement.srcObject = mediaStream;
videoElement.controls = false;
videoElement.muted = true;
playButton.classList.add('hidden');
downloadButton.classList.add('hidden');
recordedChunks = [];
mediaRecorder = new MediaRecorder(mediaStream);
mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
recordedBlob = new Blob(recordedChunks, { type: 'video/webm' });
videoElement.srcObject = null;
videoElement.src = URL.createObjectURL(recordedBlob);
videoElement.controls = true;
videoElement.loop = true;
videoElement.muted = false; // Unmute during playback
playButton.classList.remove('hidden');
downloadButton.classList.remove('hidden');
playButton.textContent = 'Play';
videoElement.pause(); // Ensure the video does not autoplay
};
mediaRecorder.start();
recordButton.textContent = 'Stop';
recordingIndicator.classList.remove('hidden');
videoSelect.setAttribute("disabled", "true");
audioSelect.setAttribute("disabled", "true");
}
} catch (error) {
console.error('Error during recording:', error);
}
});
function updatePlayButtonText() {
playButton.textContent = videoElement.paused ? 'Play' : 'Stop';
}
videoElement.addEventListener('play', updatePlayButtonText);
videoElement.addEventListener('pause', updatePlayButtonText);
playButton.addEventListener('click', () => {
if (videoElement.paused) {
videoElement.play();
} else {
videoElement.pause();
}
updatePlayButtonText();
});
downloadButton.addEventListener('click', () => {
const url = URL.createObjectURL(recordedBlob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'recording.webm';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
videoSelect.onchange = startMedia;
audioSelect.onchange = startMedia;
init();
</script>
</body>
</html>