// An opt-in, bounded browser recording for human review. It never owns capture // tracks or a network client; stopping it must not stop the numeric observer. export class LocalRecording { constructor({Recorder=globalThis.MediaRecorder,onStatus=()=>{},onReady=()=>{}, maxBytes=128*1024*1024,maxMs=180000}={}) { Object.assign(this,{Recorder,onStatus,onReady,maxBytes,maxMs}); this.serial=0;this.active=null; } get supported() {return typeof this.Recorder==='function';} start(stream) { this.discard(); if(!this.supported){this.onStatus('Recording unavailable in this browser.');return false;} const serial=this.serial; const mimeType=['video/webm;codecs=vp8,opus','video/webm','video/mp4'].find( type=>this.Recorder.isTypeSupported(type)); try { const recorder=new this.Recorder(stream,{...(mimeType?{mimeType}:{}), videoBitsPerSecond:4000000,audioBitsPerSecond:128000}); const run={recorder,chunks:[],bytes:0,failed:false}; this.active=run; recorder.ondataavailable=({data})=>{ if(serial!==this.serial||run.failed||!data.size)return; if(run.bytes+data.size>this.maxBytes) { // A truncated container may not be playable: fail explicitly, // clear it, and leave measurement running rather than claim success. run.failed=true;run.chunks=[]; this.onStatus('Recording exceeded its memory limit; no clip saved.'); this.finish();return; } run.chunks.push(data);run.bytes+=data.size; }; recorder.onerror=()=>{ if(serial!==this.serial)return; run.failed=true;run.chunks=[]; this.onStatus('Recording failed; numeric measurement continues.'); this.finish(); }; recorder.onstop=()=>{ clearTimeout(run.timer);clearTimeout(run.deadline); if(serial!==this.serial)return; this.active=null; if(!run.failed&&run.bytes) { const blob=new Blob(run.chunks,{type:recorder.mimeType||mimeType||'video/webm'}); this.onReady(blob); this.onStatus('Ready for local replay · nothing uploaded'); } else if(!run.failed)this.onStatus('No recording data was produced.'); run.chunks=[]; }; recorder.start(1000); run.timer=setTimeout(()=>this.finish(),this.maxMs); this.onStatus('Recording locally · nothing uploaded'); return true; } catch { this.discard();this.onStatus('Recording could not start; numeric measurement continues.'); return false; } } finish() { const run=this.active; if(!run||run.finishing)return; run.finishing=true;clearTimeout(run.timer); if(!run.failed)this.onStatus('Preparing local replay…'); run.deadline=setTimeout(()=>{ if(this.active!==run)return; this.discard();this.onStatus('Recording could not finalize; no clip saved.'); },10000); try {if(run.recorder.state!=='inactive')run.recorder.stop();} catch {this.discard();this.onStatus('Recording could not finalize; no clip saved.');} } discard() { this.serial++; const run=this.active;this.active=null; if(!run)return; clearTimeout(run.timer);clearTimeout(run.deadline);run.chunks=[]; run.recorder.ondataavailable=null;run.recorder.onstop=null;run.recorder.onerror=null; try {if(run.recorder.state!=='inactive')run.recorder.stop();}catch {} } }