71 lines
3.6 KiB
JavaScript
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};
|
|
}
|
|
}
|