/** Media progress and capture freshness are independent of DOM painting and inventory SSE. */ export class PreviewLiveness { private lastFrame:number|null=null; private sourceSeen=Number.NEGATIVE_INFINITY; private staleSince:number|null=null; constructor(private connectedAt:number){} connected(now:number){this.connectedAt=now;} frame(now:number){this.lastFrame=now;} source(fresh:boolean,now:number){ this.sourceSeen=now; this.staleSince=fresh?null:this.staleSince??now; } state(now:number):'live'|'waiting'|'delayed'|'source-stalled'|'peer-stalled'{ if(this.staleSince!==null&&now-this.sourceSeen<3000){ return now-this.staleSince>=12000?'source-stalled':'delayed'; } const elapsed=now-(this.lastFrame??this.connectedAt); if(elapsed>8000)return 'peer-stalled'; if(this.lastFrame===null)return 'waiting'; return elapsed>2000?'delayed':'live'; } }