/** Build a removable, offline-only browser probe around the production player. * Input manifest/media must already be staged in outputRoot by a trusted local * caller. No device API, live singleton or actual WebSocket is used by this probe. */ import {createRequire} from 'node:module'; import {readFile,writeFile} from 'node:fs/promises'; import path from 'node:path'; import {fileURLToPath} from 'node:url'; const repository=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'); const app=path.join(repository,'apps/control-station'); const outputRoot=path.resolve(process.argv[2]); if(!outputRoot.startsWith(path.join(app,'dist')+path.sep))throw new Error('Probe must remain below the canonical static root.'); const require=createRequire(path.join(app,'package.json')); const {build}=require('esbuild'); const player=path.join(app,'src/components/MseFmp4WebSocketPlayer.tsx'); const source=` import React from 'react'; import {createRoot} from 'react-dom/client'; import {MseFmp4WebSocketPlayer} from ${JSON.stringify(player)}; const base=new URL('.',location.href), nativeFetch=window.fetch.bind(window); const manifest=await (await nativeFetch(new URL('manifest.json',base))).json(); const report={schema:'missioncore.offline-browser-camera-probe/v1',startedAt:new Date().toISOString(),cases:[],unexpectedRequests:[],hardware:false,realWebSocket:false,realMediaSource:true}; let current=null,finished=false; const output=document.querySelector('pre'); function renderReport(){output.textContent=JSON.stringify(report,null,2);} window.fetch=async(input,options={})=>{ const url=new URL(typeof input==='string'?input:input.url??input,location.href); if(url.origin===base.origin&&url.pathname.startsWith(base.pathname)&&(options.method??'GET')==='GET')return nativeFetch(input,options); if(url.pathname==='/api/v1/viewer/live-diagnostics'&&options.method==='POST'){ if(current)current.events.push({atMs:performance.now()-current.started,event:JSON.parse(options.body)}); return new Response('{}',{status:200}); } report.unexpectedRequests.push({path:url.pathname,method:options.method??'GET'});renderReport(); throw new Error('Offline probe forbids all device and unrelated HTTP requests.'); }; class ArchiveSocket extends EventTarget{ static CONNECTING=0;static OPEN=1;static CLOSING=2;static CLOSED=3; readyState=0;binaryType='arraybuffer';timers=[]; constructor(url){ super(); if(new URL(url).pathname!==base.pathname+'offline-socket')throw new Error('Unexpected socket destination'); this.owner=current;this.owner.sockets++;this.owner.activeSockets++; this.timers.push(setTimeout(()=>this.open(),0)); } open(){ if(this.readyState===3)return; this.readyState=1;this.dispatchEvent(new Event('open')); const elapsed=this.owner.streamStarted===undefined?0:performance.now()-this.owner.streamStarted; this.owner.streamStarted??=performance.now(); // Every replacement gets init plus only subsequent media, like a disposable // reader joining the existing stream. It cannot rewind the archived source. this.sendBytes(this.owner.bytes[0]); this.owner.entries.slice(1).forEach((row,index)=>{ if(row.atMsthis.sendBytes(this.owner.bytes[index+1]),Math.max(0,row.atMs-elapsed))); }); } sendBytes(bytes){if(this.readyState===1){this.owner.delivered++;this.dispatchEvent(new MessageEvent('message',{data:bytes.slice(0)}));}} close(){if(this.readyState===3)return;this.readyState=3;this.timers.forEach(clearTimeout);this.owner.activeSockets--;this.dispatchEvent(new CloseEvent('close',{code:1000}));} send(){throw new Error('Offline camera is read-only.');} } window.WebSocket=ArchiveSocket; const root=createRoot(document.querySelector('#player')); const sampleTimer=setInterval(()=>{ if(!current||finished)return; const video=document.querySelector('video'); const quality=video?.getVideoPlaybackQuality?.(); current.samples.push({atMs:performance.now()-current.started,status:document.querySelector('.mse-fmp4-player')?.dataset.status,currentTime:video?.currentTime??null,readyState:video?.readyState??null,error:video?.error?.code??null,decoded:quality?.totalVideoFrames??video?.webkitDecodedFrameCount??null,dropped:quality?.droppedVideoFrames??null}); renderReport(); },500); const wait=ms=>new Promise(resolve=>setTimeout(resolve,ms)); for(const fixture of manifest.cases){ const bytes=await Promise.all(fixture.entries.map(async row=>(await nativeFetch(new URL(row.path,base))).arrayBuffer())); current={name:fixture.name,started:performance.now(),entries:fixture.entries,bytes,samples:[],events:[],sockets:0,activeSockets:0,delivered:0}; // The public DOM report excludes binary bytes and internal timer state. const publicCase={name:current.name,samples:current.samples,events:current.events};report.cases.push(publicCase); document.querySelector('h1').textContent='Проверка браузерной камеры: '+fixture.name+' · только архив'; root.render(React.createElement(MseFmp4WebSocketPlayer,{key:fixture.name,label:'Сохранённая камера · тест декодера',delivery:{kind:'mse-fmp4-websocket',id:'offline-'+fixture.name,url:base.pathname+'offline-socket',mediaType:'video/mp4; codecs="avc1.641028"'},recoveryAuthorityIdentity:'offline-fixture-'+fixture.name})); await wait(fixture.durationMs); root.render(null);await wait(100); Object.assign(publicCase,{sockets:current.sockets,activeSockets:current.activeSockets,delivered:current.delivered,expectedMedia:fixture.entries.length-1}); renderReport(); } clearInterval(sampleTimer);root.unmount();finished=true;report.finishedAt=new Date().toISOString(); const gapSamples=report.cases[0].samples.filter(s=>s.atMs>10000&&s.atMs<20000); report.checks={cleanPlayed:report.cases[0].samples.some(s=>s.status==='playing'&&s.decoded>0),cleanGapVisible:gapSamples.length>0&&gapSamples.every(s=>s.status==='buffering'),cleanNoRestarts:report.cases[0].sockets===1,cleanNoDecodeErrors:!report.cases[0].samples.some(s=>s.error)&&!report.cases[0].events.some(e=>e.event.event_code==='live_camera_transport_restart_requested'),corruptDetected:report.cases[1].samples.some(s=>s.error)||report.cases[1].events.some(e=>e.event.event_code==='live_camera_transport_restart_requested'),allReadersClosed:report.cases.every(c=>c.activeSockets===0),noUnexpectedRequests:report.unexpectedRequests.length===0}; document.querySelector('h1').textContent='Проверка браузерной камеры завершена · только архив';renderReport(); `; await writeFile(path.join(outputRoot,'probe-source.jsx'),source); await build({stdin:{contents:source,resolveDir:app,sourcefile:'offline-camera-probe.jsx',loader:'jsx'},absWorkingDir:app,bundle:true,format:'esm',target:'es2022',jsx:'automatic',define:{'process.env.NODE_ENV':'"production"'},outfile:path.join(outputRoot,'probe.js')}); const shell=await readFile(path.join(app,'dist/index.html'),'utf8'); const css=shell.match(/href="([^\"]+\.css)"/)?.[1]; if(!css)throw new Error('Canonical built stylesheet is missing.'); await writeFile(path.join(outputRoot,'index.html'), `Offline camera decoder qualification

Проверка браузерной камеры · только архив

Штатный плеер и настоящий MediaSource. Источник — локальные файлы. Подключения к K1 нет.

`); console.log(JSON.stringify({outputRoot,productionPlayer:player,hardware:false}));