81 lines
4.4 KiB
JavaScript
81 lines
4.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import {before,after,test} from 'node:test';
|
|
import {execFileSync} from 'node:child_process';
|
|
import {createServer} from 'vite';
|
|
let server,previewFrames,assertRrd,previewFreshness,previewCamera;
|
|
before(async()=>{
|
|
server=await createServer({appType:'custom',logLevel:'silent',server:{middlewareMode:true}});
|
|
({previewCamera}=await server.ssrLoadModule('@xgrids-k1/frontend/sensors/previewCamera.ts'));
|
|
({previewFreshness}=await server.ssrLoadModule('@xgrids-k1/frontend/sensors/previewFreshness.ts'));
|
|
({previewFrames,assertRrd}=await server.ssrLoadModule('@xgrids-k1/frontend/sensors/previewFrames.ts'));
|
|
});
|
|
after(async()=>{await server?.close();});
|
|
const array=bytes=>Uint8Array.from(bytes).buffer;
|
|
const header=size=>{const value=new Uint8Array(8);value.set([77,67,70,49]);new DataView(value.buffer).setUint32(4,size);return value.buffer;};
|
|
test('native RRD crosses many RTC fragments and reaches decoder once, byte-for-byte',()=>{
|
|
const payload=execFileSync('../../.venv/bin/python',['-c',`import rerun as rr, numpy as np, sys
|
|
r=rr.RecordingStream('synthetic-r10-js'); b=r.binary_stream()
|
|
r.log('points',rr.Points3D(np.random.default_rng(42).random((5000,3))))
|
|
sys.stdout.buffer.write(b.read())`]);
|
|
assert.ok(payload.length>16384);
|
|
const calls=[];
|
|
const receiver=previewFrames(value=>{assertRrd(value);calls.push(value);});
|
|
for(let record=0;record<2;record++){
|
|
receiver.push(header(payload.length));
|
|
for(let offset=0;offset<payload.length;offset+=16384){
|
|
receiver.push(array(payload.subarray(offset,offset+16384)));
|
|
if(offset+16384<payload.length)assert.equal(calls.length,record);
|
|
}
|
|
assert.deepEqual(Buffer.from(calls[record]),payload);
|
|
}
|
|
assert.equal(calls.length,2);
|
|
receiver.close();
|
|
});
|
|
test('partial, oversized, unframed and invalid records never enter the decoder',()=>{
|
|
let calls=0;
|
|
const receiver=previewFrames(value=>{assertRrd(value);calls++;});
|
|
assert.throws(()=>receiver.push(array([1,2,3])));
|
|
assert.throws(()=>receiver.push(header(8*1024*1024+1)));
|
|
assert.throws(()=>receiver.push(header(0)));
|
|
receiver.push(header(20));receiver.push(array([1,2]));receiver.close();
|
|
assert.equal(calls,0);
|
|
receiver.push(header(12));assert.throws(()=>receiver.push(array(new Uint8Array(13))));receiver.close();
|
|
receiver.push(header(12));assert.throws(()=>receiver.push(array(new Uint8Array(12))));
|
|
assert.equal(calls,0);
|
|
});
|
|
|
|
test('source age expires even when metadata repeats, old frames never look live',()=>{
|
|
let now=10000;const gate=previewFreshness(()=>now);
|
|
assert.equal(gate.fresh,false);
|
|
gate.receive({type:'lidar-state',sequence:1,age_ms:250,points:3});
|
|
assert.equal(gate.fresh,true);assert.equal(gate.points,3);
|
|
now+=1800;gate.receive({type:'lidar-state',sequence:1,age_ms:0,points:3});
|
|
assert.equal(gate.fresh,false);
|
|
gate.receive({type:'lidar-state',sequence:2,age_ms:9000,points:3});
|
|
assert.equal(gate.fresh,false);
|
|
gate.receive({type:'lidar-state',sequence:3,age_ms:0,points:4});
|
|
assert.equal(gate.fresh,true);assert.equal(gate.points,4);
|
|
assert.throws(()=>gate.receive({type:'lidar-state',sequence:4,age_ms:-1,points:4}));
|
|
});
|
|
|
|
test('onboard camera admits immediate sourceopen and ignores callbacks after close',()=>{
|
|
const saved={media:globalThis.MediaSource,create:URL.createObjectURL,revoke:URL.revokeObjectURL};
|
|
let source,buffer,appended=0,ready=0,failed=0;
|
|
class Buffer extends EventTarget {updating=false;buffered={length:0};appendBuffer(){appended++;}}
|
|
class Media extends EventTarget {
|
|
readyState='open';static isTypeSupported(){return true;}
|
|
constructor(){super();source=this;}
|
|
addSourceBuffer(){buffer=new Buffer();return buffer;}
|
|
}
|
|
class Video extends EventTarget {currentTime=0;set src(_){source.dispatchEvent(new Event('sourceopen'));}pause(){}load(){}removeAttribute(){}play(){return Promise.resolve();}}
|
|
globalThis.MediaSource=Media;URL.createObjectURL=()=> 'blob:synthetic';URL.revokeObjectURL=()=>{};
|
|
try{
|
|
const video=new Video(),decoder=previewCamera(video,()=>ready++,()=>failed++);
|
|
decoder.open('video/mp4');decoder.push(new Uint8Array([1,2,3]));
|
|
assert.equal(appended,1);
|
|
video.dispatchEvent(new Event('loadeddata'));assert.equal(ready,1);
|
|
decoder.close();video.dispatchEvent(new Event('loadeddata'));buffer.dispatchEvent(new Event('error'));
|
|
assert.equal(ready,1);assert.equal(failed,0);
|
|
}finally{globalThis.MediaSource=saved.media;URL.createObjectURL=saved.create;URL.revokeObjectURL=saved.revoke;}
|
|
});
|