import {colorCode,PulseEdges} from './test-signals.js'; import {pairedSummary} from './paired-signals.js'; // Browser adapter. Source/device timestamps are not supplied by these APIs; // this clock map is only a processing-time estimate, never release evidence. // https://www.w3.org/TR/webaudio/#dom-baseaudiocontext-currenttime // https://wicg.github.io/video-rvfc/ export class PairedObserver { constructor(context,video) { this.context=context;this.video=video;this.audio=[];this.frames=[]; this.edges=new PulseEdges();this.previous=null;this.overflow=0;this.clockInvalid=false; this.canvas=document.createElement('canvas');this.canvas.width=16;this.canvas.height=16; this.drawing=this.canvas.getContext('2d',{willReadFrequently:true}); this.offset=null; } audioMessage(data) { if(!Array.isArray(data.signals)||data.signals.length>32){this.clockInvalid=true;return;} this.overflow=Math.max(this.overflow,data.overflow??0); const before=performance.now(),clock=this.context.currentTime*1000,after=performance.now(); if(this.context.state!=='running'||after-before>5||!Number.isFinite(clock)){this.clockInvalid=true;return;} const offset=(before+after)/2-clock; if(this.offset!==null&&Math.abs(offset-this.offset)>25)this.clockInvalid=true; this.offset=offset; for(const event of data.signals) { if(this.audio.length>=256){this.overflow++;continue;} this.audio.push({code:event.code,startMs:event.startMs+offset,endMs:event.endMs+offset}); } } frame(timeMs) { if(!this.drawing||!this.video.videoWidth||!this.video.videoHeight)return; let pixels; try { // Small transient centre sample. Clear both the canvas and pixel // array immediately: no retained image, recording or upload. const w=this.video.videoWidth,h=this.video.videoHeight; this.drawing.drawImage(this.video,w/4,h/4,w/2,h/2,0,0,16,16); pixels=this.drawing.getImageData(0,0,16,16).data; const code=colorCode(pixels); const previous=this.previous;this.previous=timeMs; if(previous===null||timeMs<=previous||timeMs-previous>250)return; const event=this.edges.observe(code,previous,timeMs); if(event){if(this.frames.length<256)this.frames.push(event);else this.overflow++;} } catch {this.clockInvalid=true;} finally {pixels?.fill(0);this.drawing.clearRect(0,0,16,16);} } summary() { const result=pairedSummary(this.audio,this.frames); if(this.clockInvalid||this.overflow)result.estimatedMedianSignedMs=null; return {...result,audioSignals:this.audio.length,videoSignals:this.frames.length, overflow:this.overflow,clockInvalid:this.clockInvalid, note:'Coded signal / browser processing-time estimate only; input-clock calibration and source age are unmeasured.'}; } }