// Observe the native client's local video channel without adding a UDP peer. // Read the token internally from a deliberately enabled debug log; never print it. import { readFile, writeFile, appendFile, mkdir, access } from 'node:fs/promises'; import { dirname } from 'node:path'; const args = Object.fromEntries(process.argv.slice(2).reduce((pairs, value, i, all) => { if (i % 2 === 0) pairs.push([value.replace(/^--/, ''), all[i+1]]); return pairs; }, [])); if (!args.log || !args.out) throw new Error('Use --log --out [--seconds 1800] [--warmup 30]'); const seconds = Number(args.seconds ?? 300); const warmup = Number(args.warmup ?? 10); const sampleEveryMs = 1000*Number(args['sample-every'] ?? 0); const camera = Number(args.camera ?? '0x3D4D8A9A'); await mkdir(dirname(args.out), { recursive: true }); const log = await readFile(args.log, 'utf8'); const url = [...log.matchAll(/ws:\/\/127\.0\.0\.1:\d+\/\?token=[A-Za-z0-9_-]+/g)].at(-1)?.[0]; if (!url) throw new Error('No local video-channel URL in the debug log'); const ws = new WebSocket(url); ws.binaryType = 'arraybuffer'; let latest = null, demo = true, diagnostics = false, clients = 0, connected = false; let binary = 0, invalid = 0, lastArrival = null, gaps = [], bytes = 0; let firstFrame = null; let sampleCount = 0, nextSampleAt = 0; const allGaps = [], windows = []; let measurementStarted = null, previousAt = null, previousStats = null; let windowBinary = 0, windowInvalid = 0, windowBytes = 0; let totalFrames = 0, totalDropped = 0, counterResets = 0, socketCloses = 0; let offlineSnapshots = 0; let diagnosticsUnavailableSnapshots = 0; let finished = false; ws.addEventListener('close', () => { connected = false; if (!finished) socketCloses++; }); ws.addEventListener('message', event => { if (typeof event.data === 'string') { const msg = JSON.parse(event.data); if (msg.t === 'snapshot') { latest = msg.stats.find(stat => stat.camera === camera) ?? null; demo = msg.demo; diagnostics = msg.diagnostics; clients = msg.clients; if (measurementStarted !== null && (!latest || !latest.online)) offlineSnapshots++; if (measurementStarted !== null && !diagnostics) diagnosticsUnavailableSnapshots++; } return; } const data = Buffer.from(event.data); if (data.length < 16 || data.readUInt32LE(0) !== camera) return; if (measurementStarted === null) return; const valid = data.length === data.readUInt32LE(12)+16 && data[16] === 255 && data[17] === 216 && data.at(-2) === 255 && data.at(-1) === 217; if (!valid) { invalid++; windowInvalid++; return; } const now = performance.now(); if (lastArrival !== null) { gaps.push(now-lastArrival); allGaps.push(now-lastArrival); } lastArrival = now; binary++; windowBinary++; bytes += data.length-16; windowBytes += data.length-16; if (firstFrame === null) { firstFrame = Buffer.from(data.subarray(16)); void writeFile(`${args.out}.first.jpg`, firstFrame).catch(error => console.error('Could not save sample JPEG:', error.message)); } if (sampleEveryMs > 0 && now >= nextSampleAt) { nextSampleAt = now+sampleEveryMs; const path = `${args.out}.sample-${String(++sampleCount).padStart(4,'0')}.jpg`; void writeFile(path, data.subarray(16)).catch(error => console.error('Could not save diagnostic JPEG:', error.message)); } }); await Promise.race([ new Promise((resolve, reject) => { ws.addEventListener('open', () => { connected = true; resolve(); }, { once:true }); ws.addEventListener('error', reject, { once:true }); }), new Promise((_, reject) => setTimeout(() => reject(new Error('Video channel open timeout')), 15_000)), ]); await new Promise(resolve => setTimeout(resolve, warmup*1000)); if (demo || !latest?.online || !latest?.hasFrames || clients < 2) throw new Error('Native GUI and online real hardware video are required before starting the soak'); if (!diagnostics) throw new Error('Receiver counters require referee diagnostics; team release mode hides them'); measurementStarted = previousAt = performance.now(); previousStats = latest; await writeFile(`${args.out}.windows.ndjson`, ''); function percentile(values, p) { if (!values.length) return null; const sorted = [...values].sort((a,b) => a-b); return Number(sorted[Math.floor((sorted.length-1)*p)].toFixed(3)); } async function sample() { const now = performance.now(); const elapsed = (now-previousAt)/1000; let complete = 0, dropped = 0; if (latest && previousStats) { if (latest.framesShown < previousStats.framesShown || latest.framesDropped < previousStats.framesDropped) counterResets++; else { complete = latest.framesShown-previousStats.framesShown; dropped = latest.framesDropped-previousStats.framesDropped; } } totalFrames += complete; totalDropped += dropped; const row = { utc:new Date().toISOString(), elapsedSeconds:(now-measurementStarted)/1000, windowSeconds:elapsed, completeFrames:complete, framesDropped:dropped, receiverFps:complete/elapsed, frameLossPercent:100*dropped/Math.max(1,complete+dropped), binaryFrames:windowBinary, channelFps:windowBinary/elapsed, invalidJpegs:windowInvalid, meanJpegBytes:windowBinary ? windowBytes/windowBinary : 0, gapP95Ms:percentile(gaps,.95), gapMaxMs:Math.max(0,...gaps, lastArrival===null ? (now-measurementStarted) : now-lastArrival), cameraOnline:latest?.online ?? false, frontendConnected:clients>=2, observerConnected:connected, }; windows.push(row); await appendFile(`${args.out}.windows.ndjson`, JSON.stringify(row)+'\n'); await writeFile(`${args.out}.latest.json`, JSON.stringify(row,null,2)+'\n'); console.log(JSON.stringify(row)); previousAt = now; previousStats = latest; windowBinary = windowInvalid = windowBytes = 0; gaps = []; } let stoppedByRequest = false; while ((performance.now()-measurementStarted)/1000 < seconds) { if (args['stop-file'] && await access(args['stop-file']).then(() => true, () => false)) { stoppedByRequest = true; break; } const remaining = seconds-(performance.now()-measurementStarted)/1000; await new Promise(resolve => setTimeout(resolve, Math.min(1,remaining)*1000)); if (performance.now()-previousAt >= 60_000 || (performance.now()-measurementStarted)/1000 >= seconds) await sample(); } if (stoppedByRequest && performance.now()-previousAt > 1_000) await sample(); finished = true; ws.close(); const actualSeconds = (performance.now()-measurementStarted)/1000; const receiverMetricsValid = counterResets === 0 && diagnosticsUnavailableSnapshots === 0; const summary = { actualSeconds, requestedSeconds:seconds, stoppedByRequest, warmupSeconds:warmup, camera, demo, windows, binaryFrames:binary, invalidJpegs:invalid, receiverMetricsValid, completeFrames:receiverMetricsValid ? totalFrames : null, framesDropped:receiverMetricsValid ? totalDropped : null, rawWindowCountedCompleteFrames:totalFrames, rawWindowCountedDroppedFrames:totalDropped, receiverFps:receiverMetricsValid ? totalFrames/actualSeconds : null, channelFps:binary/actualSeconds, frameLossPercent:receiverMetricsValid ? 100*totalDropped/Math.max(1,totalFrames+totalDropped) : null, gapP95Ms:percentile(allGaps,.95), gapMaxMs:Math.max(0,...allGaps), meanJpegBytes:binary ? bytes/binary : 0, counterResets, socketCloses, offlineSnapshots, diagnosticsUnavailableSnapshots, diagnosticSamplesSaved:sampleCount, allWindowsOnline:windows.every(window => window.cameraOnline), zeroVideoWindows:windows.filter(window => window.binaryFrames === 0).length, noVideoSecondsAtEnd:lastArrival === null ? actualSeconds : (performance.now()-lastArrival)/1000, maxObservedSilenceMs:Math.max(0,...windows.map(window => window.gapMaxMs)), frontendConnected:clients>=2, measurementScope:'Native receiver and local JPEG WebSocket channel; not optical display latency', }; await writeFile(`${args.out}.summary.json`, JSON.stringify(summary,null,2)+'\n'); if (firstFrame) await writeFile(`${args.out}.first.jpg`, firstFrame); console.log(JSON.stringify({ ...summary, windows:windows.length })); if (demo || !binary || invalid || socketCloses || counterResets || offlineSnapshots || diagnosticsUnavailableSnapshots) process.exitCode = 1;