// Screen capture, used to photograph the video frame. // // The decks are cross-origin YouTube iframes, so nothing on the page can read // their pixels — canvas taints, html2canvas sees an empty box. Capturing the // tab through getDisplayMedia and cropping out the frame's rect is the only way // to get the actually-playing image. /** * Ask for the screen-share stream. Resolves to a `{ grab, stop }` handle, or to * null if the operator declines or the API is missing — callers degrade rather * than fail. * * Must be called synchronously from a user gesture (getDisplayMedia needs * transient activation), so kick it off before any await and keep the promise. */ export function requestCapture() { // Undefined on a plain-HTTP origin, which is exactly how the README tells // people to reach this page from other machines on the network. if (!navigator.mediaDevices?.getDisplayMedia) return Promise.resolve(null); try { return navigator.mediaDevices .getDisplayMedia({ // preferCurrentTab keeps the stream to this tab's viewport, so stream // pixels map to CSS pixels by one scale factor. Picking a whole screen // instead would put browser chrome in the frame and skew every crop. video: { frameRate: 8 }, audio: false, preferCurrentTab: true, }) .then(attach) .catch(() => null); } catch { // Some rejections arrive synchronously. Never let them escape into // start(), which would abort playback before the gate even lifts. return Promise.resolve(null); } } async function attach(stream) { const [track] = stream.getVideoTracks(); if (!track) return null; const video = document.createElement('video'); video.srcObject = stream; video.muted = true; video.playsInline = true; // Must be in the document, like #probe: a detached video is never composited // and the browser throttles its frames hard, which would leave screenshots // showing a moment several seconds stale. 1x1 and transparent, so it costs // nothing and cannot show up in its own crop. video.id = 'captureSink'; document.body.appendChild(video); await video.play().catch(() => {}); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return { /** * Crop a viewport rect out of the live stream. `rect` is in CSS pixels. * Returns a data URL, or null while the stream has yet to deliver a frame. */ grab({ x, y, width, height }) { if (video.readyState < 2 || !video.videoWidth) return null; if (width <= 0 || height <= 0) return null; const scaleX = video.videoWidth / window.innerWidth; const scaleY = video.videoHeight / window.innerHeight; canvas.width = Math.round(width * scaleX); canvas.height = Math.round(height * scaleY); context.drawImage( video, x * scaleX, y * scaleY, width * scaleX, height * scaleY, 0, 0, canvas.width, canvas.height, ); return canvas.toDataURL('image/jpeg', 0.85); }, stop() { for (const t of stream.getTracks()) t.stop(); video.srcObject = null; video.remove(); }, }; }