Share the K1 live scene template and preserve idle media channels

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 16:38:39 +03:00
parent 309bdf759e
commit fa8ac760a1
49 changed files with 2813 additions and 2126 deletions
@@ -35,9 +35,15 @@ test('active acquisition exposes STOP and Rerun without another START',()=>{
const active={...device,control:{...device.control,can_start:false,can_stop:true},snapshot:{...device.snapshot,acquisition:'streaming'}};
const markup=render(active);
assert.match(markup,/Остановить устройство/);
assert.match(markup,/k1-preview-spatial/);
assert.match(markup,/rerun-viewport__canvas/);
assert.match(markup,/>Статус</);
assert.match(markup,/>Rerun</);
assert.match(markup,/>Пространственная модель</);
for(const label of ['Движок','Слои','Отображение','Камера K1','Окно накопления облака точек'])assert.ok(markup.includes(label));
assert.ok(markup.includes('data-presented="false"'));
assert.ok(!markup.includes('Loading Rerun'));
assert.ok(!markup.includes('The data layer'));
assert.ok(!markup.includes('data-align="center"'));
assert.doesNotMatch(markup,/Восстановить просмотр|nodedc-activity-indicator/);
assert.doesNotMatch(markup,/Инициировать запуск|Обновить просмотр/);
assert.equal(presentation.k1ManualState(active,false).canStart,false);
@@ -2,9 +2,11 @@ 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;
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();});
@@ -41,3 +43,38 @@ test('partial, oversized, unframed and invalid records never enter the decoder',
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;}
});
@@ -414,7 +414,7 @@ test("data recordings keep the compact session dropdown and laboratory results s
test("recording preparation statuses share the viewer's left alignment", async () => {
const spatialStyles = await readFile(
new URL("../src/styles/spatial.css", import.meta.url),
new URL("../../../packages/spatial-ui/src/spatial.css", import.meta.url),
"utf8",
);
const statusStack = spatialStyles.slice(
@@ -280,7 +280,7 @@ test("recorded player skips only a proven buffered corrupt timestamp interval",
test("loading and error overlays fully conceal recorded camera pixels", async () => {
const css = await readFile(
new URL("../src/styles/observation.css", import.meta.url),
new URL("../../../packages/spatial-ui/src/observation.css", import.meta.url),
"utf8",
);
assert.match(
@@ -382,7 +382,7 @@ test("the spatial header reports raw replay as an active source", async () => {
});
test("the complete vendor canvas host is hidden during partial and failed admission", async () => {
const css = await readFile(new URL("../src/styles/spatial.css", import.meta.url), "utf8");
const css = await readFile(new URL("../../../packages/spatial-ui/src/spatial.css", import.meta.url), "utf8");
assert.match(
css,
/\.rerun-viewport:is\(\[data-status="loading"\], \[data-status="error"\]\)\s+\.rerun-viewport__canvas\s*\{[^}]*visibility:\s*hidden;[^}]*pointer-events:\s*none;/s,