// Metadata only. No frames or PCM are retained. These are OBSERVER metrics, // never an automatically passing sync/freshness/artifact report. export class VideoMetrics { constructor() { this.last = null; this.intervals = []; this.callbacks = 0; this.unobserved = 0; this.resets = 0; } observe(timeMs, presentedFrames) { if (!Number.isFinite(timeMs) || !Number.isSafeInteger(presentedFrames) || presentedFrames < 0) return; if (this.last) { if (timeMs <= this.last.timeMs || presentedFrames <= this.last.presentedFrames) this.resets++; else { if (this.intervals.length < 15000) this.intervals.push(timeMs-this.last.timeMs); this.unobserved += Math.max(0,presentedFrames-this.last.presentedFrames-1); } } this.last = {timeMs,presentedFrames}; this.callbacks++; } summary() { const sorted = [...this.intervals].sort((a,b)=>a-b); return {callbacks:this.callbacks,unobservedPresentedFrames:this.unobserved,clockOrCounterResets:this.resets, callbackIntervalP95Ms:sorted.length ? sorted[Math.ceil(sorted.length*.95)-1] : null, callbackGapMaxMs:sorted.at(-1) ?? null, note:"Browser presentation callbacks; gaps are not proof of transport loss or corrupted video."}; } } export function receipt(video,audio,settings,elapsedMs,reason) { return {schema:"av-test.observer.v2",kind:"diagnostic-only",reason,elapsedMs, settings,video:video.summary(),audio, syncMeasured:false,freshnessMeasured:false,artifactsMeasured:false, releaseEligible:false,recordedMedia:false}; }