jenkins 9204773e7d av-test: publish bounded continuity watch and recent observations
Vendor tested Lesavka observer assets and numeric receipt schema from 6b97ceb1. Verified 65 JS tests, 10 service tests, kustomize render and Kubernetes client dry-run. No media upload, capture activation or changes to the homepage and private OCR.
2026-10-22 21:47:13 -05:00

193 lines
10 KiB
JavaScript

import {VideoMetrics,receipt} from './metrics.js';
import {RunReporter} from './reporting.js';
import {PairedObserver} from './paired-observer.js';
import {reviewControls} from './review-controls.js';
import {releaseCapture,stopTracks} from './capture-lifecycle.js';
import {ContinuityWatch,WATCH_MS} from './continuity-watch.js';
const el=id=>document.getElementById(id);
const review=reviewControls(document);
let generation=0, current=null, last=null, reportingRun=null;
function modeControls() {
const watch=el('observation-mode').value==='watch';
el('paired-signal').disabled=watch||current!==null;
review.lock(watch||current!==null);
el('watch-help').hidden=!watch;
el('start').textContent=watch?'Start 10-hour watch':'Start 2-minute check';
}
el('observation-mode').onchange=()=>{
if(current)return;
if(el('observation-mode').value==='watch') {
el('paired-signal').checked=false;el('record-review').checked=false;review.discard();
}
modeControls();
};
function release(run) {
releaseCapture(run,el('preview'),()=>review.finish());
}
function update(run) {
if(current!==run)return;
const sample=receipt(run.video,run.audio,run.settings,performance.now()-run.started,'observing');
if(run.paired)sample.paired=run.paired.summary();
if(run.watch) {
run.watch.observe(sample,run.audioReceivedAt);
sample.watch=run.watch.summary();
}
el('metrics').textContent=JSON.stringify(sample,null,2);
if(run.observing)run.reporter.observe(sample);
}
function stop(reason='operator') {
generation++;
const run=current; current=null;
if(run) {
release(run);
last=receipt(run.video,run.audio,run.settings,performance.now()-run.started,reason);
if(run.paired)last.paired=run.paired.summary();
if(run.watch) {
run.watch.observe(last,run.audioReceivedAt);run.watch.finish(reason);
last.watch=run.watch.summary(true);
}
run.reporter.finish(last);
const display=run.watch?{...last,watch:run.watch.summary()}:last;
el('metrics').textContent=JSON.stringify(display,null,2);
el('export').disabled=false;
}
el('preview').srcObject=null;
el('start').disabled=!el('audio-device').value || !el('video-device').value;
el('stop').disabled=true; el('devices').disabled=false;
el('observation-mode').disabled=false;modeControls();
el('status').textContent=`Stopped · ${reason}`;
}
el('devices').onclick=async()=>{
stop('selecting devices');
const ticket=++generation;
el('devices').disabled=true; el('start').disabled=true; el('stop').disabled=false;
el('status').textContent='Requesting device permission…';
const timer=setTimeout(()=>{if(generation===ticket)stop('permission timeout');},20000);
try {
const stream=await navigator.mediaDevices.getUserMedia({audio:true,video:true});
stopTracks(stream);
if(generation!==ticket)return;
const devices=await navigator.mediaDevices.enumerateDevices();
if(generation!==ticket)return;
for(const [id,kind] of [['video-device','videoinput'],['audio-device','audioinput']]) {
const select=el(id), saved=select.value;
select.replaceChildren(new Option('Select a device',''));
for(const d of devices.filter(d=>d.kind===kind)) select.add(new Option(d.label||'Unnamed device',d.deviceId));
select.value=saved;
}
el('status').textContent='Select a camera and microphone, then Start.';
el('devices').disabled=false; el('stop').disabled=true;
} catch { if(generation===ticket)stop('device permission unavailable'); }
finally {clearTimeout(timer);}
};
for(const id of ['audio-device','video-device']) el(id).onchange=()=>{
if(current)stop('device selection changed');
el('start').disabled=!el('audio-device').value||!el('video-device').value;
};
el('start').onclick=async()=>{
if(current || !el('audio-device').value || !el('video-device').value)return;
if(!el('preview').requestVideoFrameCallback || !window.AudioWorkletNode) {
el('status').textContent='This browser lacks the required observation APIs.';return;
}
const ticket=++generation;
const watchEnabled=el('observation-mode').value==='watch';
const run={started:performance.now(),video:new VideoMetrics(),audio:null,audioReceivedAt:null,
settings:{},watchEnabled,pairedEnabled:!watchEnabled&&el('paired-signal').checked};
current=run; last=null;
reportingRun=run;
run.reporter=watchEnabled?{
observe(){},finish(){if(reportingRun===run)el('report-status').textContent='Local watch · Save metrics before closing';},
}:new RunReporter(message=>{
if(reportingRun===run)el('report-status').textContent=message;
});
if(watchEnabled){review.discard();el('report-status').textContent='Local watch · metrics are not uploaded';}
el('start').disabled=true; el('devices').disabled=true; el('stop').disabled=false; el('export').disabled=true;
el('paired-signal').disabled=true;
el('observation-mode').disabled=true;
review.lock(true);
el('status').textContent='Starting…';
run.setupTimer=setTimeout(()=>{if(current===run)stop('setup timeout');},20000);
try {
const stream=await navigator.mediaDevices.getUserMedia({
video:{deviceId:{exact:el('video-device').value}},
audio:{deviceId:{exact:el('audio-device').value},echoCancellation:false,noiseSuppression:false,autoGainControl:false},
});
if(generation!==ticket){stopTracks(stream);return;}
run.stream=stream;
for(const track of stream.getTracks())track.onended=()=>{if(current===run)stop('device ended');};
const vs=stream.getVideoTracks()[0].getSettings(),as=stream.getAudioTracks()[0].getSettings();
run.settings={video:{width:vs.width,height:vs.height,frameRate:vs.frameRate},
audio:{sampleRate:as.sampleRate,channelCount:as.channelCount,echoCancellation:as.echoCancellation,
noiseSuppression:as.noiseSuppression,autoGainControl:as.autoGainControl}};
run.context=new AudioContext();
await run.context.audioWorklet.addModule(run.pairedEnabled?'paired-worklet.js':'activity-worklet.js');
if(generation!==ticket)return;
if(run.pairedEnabled)run.paired=new PairedObserver(run.context,el('preview'));
run.node=new AudioWorkletNode(run.context,run.pairedEnabled?'av-paired-signals':'av-activity',
{processorOptions:{continuityWatch:watchEnabled}});
run.node.onprocessorerror=()=>{if(current===run)stop('audio observer error');};
run.node.port.onmessage=({data})=>{
if(current!==run)return;
if(watchEnabled&&data.watchStopped===true){stop('audio observer deadline or lost owner');return;}
run.audioReceivedAt=performance.now();
if(run.paired){run.audio=data.activity;run.paired.audioMessage(data);}else run.audio=data;
run.node.port.postMessage('ack');
if(run.observing&&run.paired&&!run.signalStarted) {
const paired=run.paired.summary();
if(paired.continuity.frames>=2&&paired.pilot?.windows>0) {
run.signalStarted=true;clearTimeout(run.signalTimer);
run.started=performance.now();run.video=new VideoMetrics();
run.timer=setTimeout(()=>{if(current===run)stop('120 seconds complete');},120000);
el('status').textContent='Measuring known A/V signals · 120 seconds';
}
}
update(run);
};
run.source=run.context.createMediaStreamSource(new MediaStream(stream.getAudioTracks()));
run.source.connect(run.node);run.node.connect(run.context.destination);
await run.context.resume();
if(generation!==ticket)return;
if(run.context.state!=='running')throw new Error('audio context did not start');
el('preview').srcObject=stream;
await el('preview').play();
if(generation!==ticket)return;
clearTimeout(run.setupTimer);
run.started=performance.now();
if(watchEnabled)run.watch=new ContinuityWatch();
run.observing=true;
// Reporting and freshness cannot depend on the audio path still working.
run.updateTimer=setInterval(()=>update(run),1000);
run.recording=watchEnabled?false:review.begin(stream);
function frame(now,metadata) {
if(current!==run)return;
try {
run.video.observe(metadata.expectedDisplayTime,metadata.presentedFrames);
run.paired?.frame(metadata.expectedDisplayTime);
run.callback=el('preview').requestVideoFrameCallback(frame);
} catch {if(current===run)stop('video observer error');}
}
run.callback=el('preview').requestVideoFrameCallback(frame);
if(watchEnabled) {
run.timer=setTimeout(()=>{if(current===run)stop('10 hours observed');},WATCH_MS);
el('status').textContent='Watching continuity · 10-hour limit · local metrics only';
} else if(run.paired) {
run.signalTimer=setTimeout(()=>{if(current===run)stop('test signal timeout');},60000);
el('status').textContent='Waiting for known A/V signals · start the source now';
} else {
run.timer=setTimeout(()=>{if(current===run)stop('120 seconds complete');},120000);
el('status').textContent=run.recording?'Observing both devices · recording locally':'Observing both devices · no recording';
}
} catch { if(current===run)stop('capture/setup failed'); }
finally { if(current!==run)release(run); }
};
el('stop').onclick=()=>stop();
el('export').onclick=()=>{
if(!last)return;
const url=URL.createObjectURL(new Blob([JSON.stringify(last,null,2)],{type:'application/json'}));
const link=document.createElement('a');link.href=url;
link.download=last.watch?'av-continuity-watch.json':'av-test-metrics.json';link.click();
setTimeout(()=>URL.revokeObjectURL(url),1000);
};
window.addEventListener('pagehide',()=>{stop('page closed');review.discard();});
document.addEventListener('visibilitychange',()=>{if(document.hidden&&current)stop('page hidden; timing invalid');});