71 lines
3.6 KiB
JavaScript

// Known synthetic signatures only. No PCM/frame retention or content upload.
export class VideoContinuity {
constructor(){this.frames=0;this.bad=0;this.duplicates=0;this.missing=0;this.ambiguous=0;this.previous=null;}
observe(rgba,timeMs) {
// Waiting for the known source is not a scored part of the test.
const reject=()=>{if(this.previous){this.frames++;this.bad++;}};
if(!rgba||rgba.length!==80||!Number.isFinite(timeMs)){reject();return;}
const bits=[];
for(let i=0;i<20;i++) {
const rgb=Array.from(rgba.slice(i*4,i*4+3));
if(rgb.some(x=>!Number.isFinite(x)||x<0||x>255)||Math.max(...rgb)-Math.min(...rgb)>35) {reject();return;}
const mean=rgb.reduce((a,b)=>a+b,0)/3;
if(mean>70&&mean<185){reject();return;}
bits.push(mean>=185?1:0);
}
const value=bits.slice(2,18).reduce((n,b)=>(n<<1)|b,0);
const parity=bits.slice(2,18).reduce((a,b)=>a^b,0);
if(bits[0]!==1||bits[1]!==0||bits[18]!==parity||bits[19]===parity||value>=16) {reject();return;}
this.frames++;
if(this.previous) {
const gap=timeMs-this.previous.time,delta=(value-this.previous.value+16)%16;
// Large time/sequence gaps can hide wraps. Unknown is never zero.
if(gap<=0||gap>100||delta>8)this.ambiguous++;
else if(delta===0)this.duplicates++;
else this.missing+=delta-1;
}
this.previous={time:timeMs,value};
}
summary() {
return {frames:this.frames,badMarkers:this.bad,ambiguousGaps:this.ambiguous,
duplicateFraction:this.frames&&!this.ambiguous?this.duplicates/this.frames:null,
missingFraction:this.frames&&!this.ambiguous?this.missing/(this.frames+this.missing):null};
}
}
export class AudioPilot {
constructor(rate) {
if(!Number.isFinite(rate)||rate<16000||rate>192000)throw new RangeError('sample rate');
this.rate=rate;this.size=Math.round(rate*.01);this.coeff=2*Math.cos(2*Math.PI*7000/rate);
this.q1=0;this.q2=0;this.count=0;this.windows=0;this.missing=0;this.pending=0;
this.maxGap=0;this.first=null;this.last=null;this.expected=null;this.discontinuities=0;
}
process(samples,frame) {
if(this.expected!==null&&frame!==this.expected) {
this.discontinuities++;this.q1=0;this.q2=0;this.count=0;
}
this.expected=frame+samples.length;
for(let i=0;i<samples.length;i++) {
const x=samples[i];
if(!Number.isFinite(x)){this.discontinuities++;this.q1=0;this.q2=0;this.count=0;continue;}
const q=x+this.coeff*this.q1-this.q2;this.q2=this.q1;this.q1=q;
if(++this.count!==this.size)continue;
const power=this.q1**2+this.q2**2-this.coeff*this.q1*this.q2;
const present=2*Math.sqrt(Math.max(0,power))/this.size>=.008;
const now=(frame+i+1)/this.rate*1000;
if(present) {
if(this.first===null)this.first=now-this.size/this.rate*1000;
this.last=now;this.windows+=1+this.pending;this.missing+=this.pending;
this.maxGap=Math.max(this.maxGap,this.pending*10);this.pending=0;
} else if(this.first!==null)this.pending++;
this.q1=0;this.q2=0;this.count=0;
}
}
summary() {
return {windows:this.windows,firstMs:this.first,lastMs:this.last,
missingFraction:this.windows&&!this.discontinuities?this.missing/this.windows:null,
gapMaxMs:this.windows&&!this.discontinuities?this.maxGap:null,
trailingGapMs:this.pending*10,discontinuities:this.discontinuities};
}
}