refactor(viewer): profile rerun loading by playback stage
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, before, test } from "node:test";
|
||||
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let laboratoryRecordedEvidenceDemand;
|
||||
let e47SemanticChunkWindowStarts;
|
||||
let cancelE47SemanticRequestsOutsideWindow;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({ laboratoryRecordedEvidenceDemand } = await server.ssrLoadModule(
|
||||
"/src/core/laboratory/recordedEvidenceProfile.ts",
|
||||
));
|
||||
({
|
||||
e47SemanticChunkWindowStarts,
|
||||
cancelE47SemanticRequestsOutsideWindow,
|
||||
} = await server.ssrLoadModule(
|
||||
"/src/workspaces/laboratory/useE47SemanticTimeline.ts",
|
||||
));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
test("camera-only LAB presentation does not acquire hidden spatial or semantic payloads", () => {
|
||||
assert.deepEqual(laboratoryRecordedEvidenceDemand({
|
||||
mediaMode: "video",
|
||||
spatialMode: null,
|
||||
showMediaSemantic: false,
|
||||
showSpatialSemantic: true,
|
||||
showMediaPoints: false,
|
||||
classifiedSpatialMode: "overlay",
|
||||
}), {
|
||||
sourceTimelineMetadata: true,
|
||||
recordedVideo: true,
|
||||
exactCameraFrame: false,
|
||||
sourceSpatialPoints: false,
|
||||
cameraPointOverlay: false,
|
||||
selectedSemanticMask: false,
|
||||
selectedSemanticPoints: false,
|
||||
classifiedSpatial: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("visible M4 layers acquire only their selected camera and spatial evidence", () => {
|
||||
assert.deepEqual(laboratoryRecordedEvidenceDemand({
|
||||
mediaMode: "camera",
|
||||
spatialMode: "plan",
|
||||
showMediaSemantic: true,
|
||||
showSpatialSemantic: true,
|
||||
showMediaPoints: true,
|
||||
classifiedSpatialMode: "overlay",
|
||||
}), {
|
||||
sourceTimelineMetadata: true,
|
||||
recordedVideo: false,
|
||||
exactCameraFrame: true,
|
||||
sourceSpatialPoints: true,
|
||||
cameraPointOverlay: true,
|
||||
selectedSemanticMask: true,
|
||||
selectedSemanticPoints: true,
|
||||
classifiedSpatial: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("a classified replacement does not also load the hidden source point track", () => {
|
||||
const demand = laboratoryRecordedEvidenceDemand({
|
||||
mediaMode: null,
|
||||
spatialMode: "3d",
|
||||
showMediaSemantic: false,
|
||||
showSpatialSemantic: false,
|
||||
showMediaPoints: false,
|
||||
classifiedSpatialMode: "replace-source",
|
||||
});
|
||||
assert.equal(demand.sourceSpatialPoints, false);
|
||||
assert.equal(demand.classifiedSpatial, true);
|
||||
});
|
||||
|
||||
test("semantic point chunks are ordered active-first and stale requests are cancelled", () => {
|
||||
assert.deepEqual(e47SemanticChunkWindowStarts(48, 4_489), [48, 72, 96, 24]);
|
||||
assert.deepEqual(e47SemanticChunkWindowStarts(0, 4_489), [0, 24, 48]);
|
||||
|
||||
const cancelled = [];
|
||||
const requests = new Map([
|
||||
[0, { abort: () => cancelled.push(0) }],
|
||||
[24, { abort: () => cancelled.push(24) }],
|
||||
[240, { abort: () => cancelled.push(240) }],
|
||||
]);
|
||||
cancelE47SemanticRequestsOutsideWindow(requests, [240, 264]);
|
||||
assert.deepEqual(cancelled, [0, 24]);
|
||||
assert.deepEqual([...requests.keys()], [240]);
|
||||
});
|
||||
@@ -7,6 +7,7 @@ let createLiveViewerDiagnosticLifecycle;
|
||||
let createAbortFencedBuildVerifier;
|
||||
let createUiBuildStaleCoordinator;
|
||||
let liveViewerDiagnosticBody;
|
||||
let reloadRecordedViewerAfterStaleModuleFailure;
|
||||
let server;
|
||||
let uiBuildIdFromModuleScripts;
|
||||
let verifyLiveViewerClientBuild;
|
||||
@@ -22,6 +23,7 @@ before(async () => {
|
||||
createLiveViewerDiagnosticLifecycle,
|
||||
createUiBuildStaleCoordinator,
|
||||
liveViewerDiagnosticBody,
|
||||
reloadRecordedViewerAfterStaleModuleFailure,
|
||||
uiBuildIdFromModuleScripts,
|
||||
verifyLiveViewerClientBuild,
|
||||
} = await server.ssrLoadModule("/src/core/observation/liveViewerDiagnostics.ts"));
|
||||
@@ -204,6 +206,36 @@ test("build drift is reported once with the exact loaded build", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("a recorded viewer reloads only when its failed lazy module belongs to a stale build", async () => {
|
||||
const reloads = [];
|
||||
const loadedUiBuildId = "/assets/index-abcdefgh.js";
|
||||
const currentUiBuildId = "/assets/index-ijklmnop.js";
|
||||
const fetcher = async (_url, options) => {
|
||||
assert.equal(options.headers["X-MissionCore-UI-Build"], loadedUiBuildId);
|
||||
return new Response(JSON.stringify({}), {
|
||||
status: 200,
|
||||
headers: { "X-MissionCore-UI-Build": currentUiBuildId },
|
||||
});
|
||||
};
|
||||
|
||||
assert.equal(await reloadRecordedViewerAfterStaleModuleFailure({
|
||||
loadedUiBuildId,
|
||||
fetcher,
|
||||
reload: () => reloads.push("reload"),
|
||||
}), true);
|
||||
assert.deepEqual(reloads, ["reload"]);
|
||||
|
||||
assert.equal(await reloadRecordedViewerAfterStaleModuleFailure({
|
||||
loadedUiBuildId: currentUiBuildId,
|
||||
fetcher: async () => new Response(JSON.stringify({}), {
|
||||
status: 200,
|
||||
headers: { "X-MissionCore-UI-Build": currentUiBuildId },
|
||||
}),
|
||||
reload: () => reloads.push("unexpected"),
|
||||
}), false);
|
||||
assert.deepEqual(reloads, ["reload"]);
|
||||
});
|
||||
|
||||
test("last unsubscribe fences an already queued build verification callback", () => {
|
||||
const controller = new AbortController();
|
||||
const observedSignals = [];
|
||||
|
||||
@@ -932,7 +932,7 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
|
||||
assert.match(visual, /timelineFrame\.availableFrames\.find/);
|
||||
assert.match(visual, /localSurfaceBodyXyzM=\{localSurface\.pointsBodyXyzM\}/);
|
||||
assert.match(visual, /showLocalSurface=\{showLocalSurface\}/);
|
||||
assert.match(visual, /\{semantic \? \([\s\S]*>\s*SEMANTICS\s*<\/Button>/);
|
||||
assert.match(visual, /\{activeSemantic \? \([\s\S]*>\s*SEMANTICS\s*<\/Button>/);
|
||||
assert.match(visual, /current safety — все \{classifiedCellCount\.toLocaleString\("ru-RU"\)\} TGS-ячейки UNOBSERVED/);
|
||||
assert.match(
|
||||
visual,
|
||||
|
||||
@@ -11,7 +11,11 @@ let recordedMediaSeekableCoverage;
|
||||
let recordedMediaFragmentUrl;
|
||||
let recordedMediaDecodeStartSequence;
|
||||
let recordedMediaSegmentAppendOrder;
|
||||
let recordedMediaSegmentSequenceAtTime;
|
||||
let recordedMediaCanRollTarget;
|
||||
let nextRecordedMediaRandomAccessSequence;
|
||||
let recordedMediaRecoveryTargetSequence;
|
||||
let selectRecordedMediaPreparationEpoch;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
@@ -26,7 +30,11 @@ before(async () => {
|
||||
recordedMediaFragmentUrl,
|
||||
recordedMediaDecodeStartSequence,
|
||||
recordedMediaSegmentAppendOrder,
|
||||
recordedMediaSegmentSequenceAtTime,
|
||||
recordedMediaCanRollTarget,
|
||||
nextRecordedMediaRandomAccessSequence,
|
||||
recordedMediaRecoveryTargetSequence,
|
||||
selectRecordedMediaPreparationEpoch,
|
||||
} = await server.ssrLoadModule("/src/components/RecordedFmp4Player.tsx"));
|
||||
});
|
||||
|
||||
@@ -161,7 +169,7 @@ test("decoded duration and seekable range cover the complete declared epoch", ()
|
||||
assert.equal(recordedMediaSeekableCoverage(20, 20, 20, 1, 1.01), false);
|
||||
});
|
||||
|
||||
test("recorded player keeps full-archive range fallback and uses bounded generation-bound fragments", async () => {
|
||||
test("production replay derives bounded fragments and retains native range fallback", async () => {
|
||||
const source = await readFile(
|
||||
new URL("../src/components/RecordedFmp4Player.tsx", import.meta.url),
|
||||
"utf8",
|
||||
@@ -176,6 +184,10 @@ test("recorded player keeps full-archive range fallback and uses bounded generat
|
||||
assert.match(source, /hasPresentedFrame/);
|
||||
assert.match(source, /retainForwardFrame/);
|
||||
assert.match(source, /candidateTarget\.sequence >= previousTarget\.sequence/);
|
||||
assert.match(source, /effectiveSegmentCount = segmentCount \?\? epoch\?\.segmentCount \?\? null/);
|
||||
assert.match(source, /requestedSegmentSequence = segmentSequence \?\?/);
|
||||
assert.match(source, /waitForRecordedVideoInitialFrame/);
|
||||
assert.match(source, /setSegmentRecoveryGeneration/);
|
||||
|
||||
assert.equal(recordedMediaDecodeStartSequence([1, 1491, 1501], 1500), 1491);
|
||||
assert.deepEqual(
|
||||
@@ -209,6 +221,44 @@ test("recorded player keeps full-archive range fallback and uses bounded generat
|
||||
);
|
||||
});
|
||||
|
||||
test("production replay derives its bounded fragment directly from the source clock", () => {
|
||||
const ends = [0.101, 0.185, 0.286, 0.401];
|
||||
const epochStart = 39.215263458;
|
||||
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart), 1);
|
||||
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 0.101), 1);
|
||||
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 0.103), 2);
|
||||
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 0.4), 4);
|
||||
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 1), null);
|
||||
});
|
||||
|
||||
test("a corrupt fragment advances recovery to the next random-access frame", () => {
|
||||
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 1), 11);
|
||||
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 20), 21);
|
||||
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 49), null);
|
||||
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 0), null);
|
||||
});
|
||||
|
||||
test("paused seek displays the recovery keyframe until the source clock leaves the corrupt GOP", () => {
|
||||
assert.equal(recordedMediaRecoveryTargetSequence(120, 120, 149), 149);
|
||||
assert.equal(recordedMediaRecoveryTargetSequence(130, 120, 149), 149);
|
||||
assert.equal(recordedMediaRecoveryTargetSequence(119, 120, 149), 119);
|
||||
assert.equal(recordedMediaRecoveryTargetSequence(149, 120, 149), 149);
|
||||
assert.equal(recordedMediaRecoveryTargetSequence(151, 120, 149), 151);
|
||||
assert.equal(recordedMediaRecoveryTargetSequence(null, 120, 149), null);
|
||||
assert.equal(recordedMediaRecoveryTargetSequence(120, null, null), 120);
|
||||
});
|
||||
|
||||
test("camera admission selects one bounded epoch while the shared clock is outside video", () => {
|
||||
const epochs = [
|
||||
{ ordinal: 1, timelineStartSeconds: 39, timelineEndSeconds: 100 },
|
||||
{ ordinal: 2, timelineStartSeconds: 120, timelineEndSeconds: 180 },
|
||||
];
|
||||
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 0).ordinal, 1);
|
||||
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 70).ordinal, 1);
|
||||
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 110).ordinal, 2);
|
||||
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 200).ordinal, 2);
|
||||
});
|
||||
|
||||
test("recorded player preserves forward rolling playback but seeks backward clip loops", () => {
|
||||
assert.equal(recordedMediaCanRollTarget(20, 21, true, true), true);
|
||||
assert.equal(recordedMediaCanRollTarget(20, 20, true, true), true);
|
||||
|
||||
@@ -34,7 +34,7 @@ function camera(phase, byteLength = 1_024) {
|
||||
return { phase, byteLength, message: null };
|
||||
}
|
||||
|
||||
test("recorded session admits RRD and every declared camera as one atomic generation", () => {
|
||||
test("recorded session keeps camera and timeline atomic without hiding a ready spatial frame", () => {
|
||||
const ids = ["camera.left", "camera.right"];
|
||||
const oneCamera = {
|
||||
"camera.left": camera("ready"),
|
||||
@@ -42,7 +42,7 @@ test("recorded session admits RRD and every declared camera as one atomic genera
|
||||
};
|
||||
const partialGate = admission.recordedSessionAdmissionPhase("ready", ids, oneCamera);
|
||||
assert.equal(partialGate, "loading");
|
||||
assert.equal(rerunPresentationStatus("ready", partialGate, true), "loading");
|
||||
assert.equal(rerunPresentationStatus("ready", partialGate, true), "ready");
|
||||
assert.equal(
|
||||
recordedMediaPresentationState("ready", "generation", "generation", false, partialGate),
|
||||
"loading",
|
||||
@@ -71,7 +71,7 @@ test("any RRD or camera failure closes the complete recorded session", () => {
|
||||
...cameras,
|
||||
"camera.right": camera("ready"),
|
||||
}), "error");
|
||||
assert.equal(rerunPresentationStatus("ready", "error", true), "error");
|
||||
assert.equal(rerunPresentationStatus("ready", "error", true), "ready");
|
||||
assert.equal(
|
||||
recordedMediaPresentationState("ready", "generation", "generation", false, "error"),
|
||||
"error",
|
||||
|
||||
@@ -257,6 +257,14 @@ test("recorded RRD bytes are never split across LogChannel.send_rrd calls", asyn
|
||||
source,
|
||||
/if \(readyToRender && !readyPublished\)[\s\S]*clearRecordedAdmissionWatchdog\(\);/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/reloadRecordedViewerAfterStaleModuleFailure\(\{[\s\S]*loadedUiBuildId: diagnosticLifecycle\.lineage\.uiBuildId/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/!recordedPerceptionUrl \|\| !recordedPerceptionLayers\.enabled/,
|
||||
);
|
||||
});
|
||||
|
||||
test("one live document owns one native Rerun receiver", async () => {
|
||||
@@ -287,8 +295,12 @@ test("raw replay exercises the same streaming receiver lifecycle as a live scan"
|
||||
source,
|
||||
/const livePresentationActivitySequence = metrics\?\.publishedFrameCount \?\?[\s\S]*liveRerunSource && !streamActive \? 1 : null/,
|
||||
);
|
||||
assert.match(source, /followLive=\{liveRerunSource\}/);
|
||||
assert.match(source, /liveActivitySequence=\{livePresentationActivitySequence\}/);
|
||||
assert.match(
|
||||
source,
|
||||
/liveAcquisitionRerunProfile\(\{[\s\S]*liveActivitySequence: livePresentationActivitySequence/,
|
||||
);
|
||||
assert.match(source, /<RerunViewport[\s\S]*profile=\{rerunViewerProfile\}/);
|
||||
assert.doesNotMatch(source, /followLive=\{liveRerunSource\}/);
|
||||
assert.match(
|
||||
source,
|
||||
/sourceUrl\.trim\(\) && pointCloudVisible && !intentionalSourceEnd/,
|
||||
|
||||
@@ -11,6 +11,7 @@ let isRecordedPlaybackPresentationReady;
|
||||
let isUsableRecordedPlaybackRange;
|
||||
let recordedPlaybackBufferState;
|
||||
let recordedPlaybackRangeWhenReady;
|
||||
let rerunPresentationStatus;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
@@ -26,6 +27,7 @@ before(async () => {
|
||||
isUsableRecordedPlaybackRange,
|
||||
recordedPlaybackBufferState,
|
||||
recordedPlaybackRangeWhenReady,
|
||||
rerunPresentationStatus,
|
||||
} = await server.ssrLoadModule("/src/components/RerunViewport.tsx"));
|
||||
});
|
||||
|
||||
@@ -33,7 +35,7 @@ after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
test("a first frame reports buffer telemetry but is not ready for presentation", () => {
|
||||
test("a verified first frame is presentable while full-range controls stay closed", () => {
|
||||
assert.equal(isUsableRecordedPlaybackRange(null), false);
|
||||
assert.equal(isUsableRecordedPlaybackRange({ min: Number.NaN, max: 0 }), false);
|
||||
assert.equal(isUsableRecordedPlaybackRange({ min: 2, max: 1 }), false);
|
||||
@@ -47,8 +49,11 @@ test("a first frame reports buffer telemetry but is not ready for presentation",
|
||||
bufferProgress: 0,
|
||||
fullyBuffered: false,
|
||||
});
|
||||
assert.equal(isRecordedPlaybackReady(true, true, firstFrame), false);
|
||||
assert.equal(isRecordedPlaybackReady(true, true, firstFrame), true);
|
||||
assert.equal(recordedPlaybackRangeWhenReady({ min: 0, max: 0 }, firstFrame, true), null);
|
||||
assert.equal(rerunPresentationStatus("ready", "loading", true), "ready");
|
||||
assert.equal(rerunPresentationStatus("loading", "ready", true), "loading");
|
||||
assert.equal(rerunPresentationStatus("ready", "error", true), "ready");
|
||||
});
|
||||
|
||||
test("host timeline remains unmounted until the verified recording is fully ready", () => {
|
||||
@@ -107,7 +112,7 @@ test("buffer progress grows independently and preserves the full-buffer toleranc
|
||||
);
|
||||
assert.equal(missingDeclaredStart.bufferProgress, 1);
|
||||
assert.equal(missingDeclaredStart.fullyBuffered, false);
|
||||
assert.equal(isRecordedPlaybackReady(true, true, missingDeclaredStart), false);
|
||||
assert.equal(isRecordedPlaybackReady(true, true, missingDeclaredStart), true);
|
||||
});
|
||||
|
||||
test("a verified split boundary spill covers and clamps the declared LAB window", () => {
|
||||
|
||||
@@ -81,7 +81,7 @@ test("M4 keeps independent semantic controls in media and spatial panes", async
|
||||
);
|
||||
assert.match(source, /showMediaSemantic/);
|
||||
assert.match(source, /showSpatialSemantic/);
|
||||
assert.match(source, /semantic && showMediaSemantic && frame/);
|
||||
assert.match(source, /activeSemantic && evidenceDemand\.selectedSemanticMask && frame/);
|
||||
assert.match(source, /Array\.from\(\{ length: 12 \}, \(_, index\) => index \+ 1\)/);
|
||||
assert.match(source, /\|\| !showSpatialSemantic/);
|
||||
assert.match(source, /aria-label="Слои камеры и видео"/);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, before, test } from "node:test";
|
||||
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE;
|
||||
let liveAcquisitionRerunProfile;
|
||||
let recordedSessionRerunProfile;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({
|
||||
LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE,
|
||||
liveAcquisitionRerunProfile,
|
||||
recordedSessionRerunProfile,
|
||||
} = await server.ssrLoadModule("/src/core/observation/viewerProfile.ts"));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
test("live acquisition profile cannot acquire recorded playback policy", () => {
|
||||
assert.deepEqual(liveAcquisitionRerunProfile({
|
||||
sourceUrl: "rerun+http://127.0.0.1:9877/proxy",
|
||||
liveActivitySequence: 12,
|
||||
liveStreamId: "acquisition-1",
|
||||
liveRecoveryAuthorityIdentity: "authority-1",
|
||||
}), {
|
||||
kind: "live-acquisition",
|
||||
clock: "stream_time",
|
||||
sourceUrl: "rerun+http://127.0.0.1:9877/proxy",
|
||||
liveActivitySequence: 12,
|
||||
liveStreamId: "acquisition-1",
|
||||
liveRecoveryAuthorityIdentity: "authority-1",
|
||||
});
|
||||
});
|
||||
|
||||
test("recorded session profile owns progressive admission and on-demand layers", () => {
|
||||
const artifact = {
|
||||
sourceUrl: "/api/v1/observation-sessions/session-1/recording.rrd",
|
||||
viewerSourceUrl: "/api/v1/observation-sessions/session-1/recording.rrd?generation=abc",
|
||||
byteLength: 42,
|
||||
sha256: "a".repeat(64),
|
||||
};
|
||||
const profile = recordedSessionRerunProfile({
|
||||
sourceUrl: artifact.sourceUrl,
|
||||
artifact,
|
||||
autoplayWhenReady: true,
|
||||
presentationGate: "loading",
|
||||
expectedTimelineStartSeconds: 0,
|
||||
expectedTimelineEndSeconds: 20,
|
||||
initialPlaybackStartSeconds: 1,
|
||||
view: "spatial",
|
||||
viewResetGeneration: 0,
|
||||
followTrajectory: false,
|
||||
perceptionLayers: {
|
||||
enabled: false,
|
||||
detections2d: false,
|
||||
segmentation: false,
|
||||
cuboids3d: false,
|
||||
},
|
||||
perceptionRetryGeneration: 0,
|
||||
lockPerceptionCameraInteraction: false,
|
||||
});
|
||||
|
||||
assert.equal(profile.kind, "recorded-session");
|
||||
assert.equal(profile.clock, "session_time");
|
||||
assert.equal(profile.artifact, artifact);
|
||||
assert.equal(profile.perceptionLayers.enabled, false);
|
||||
});
|
||||
|
||||
test("LAB recorded evidence remains outside native Rerun lifecycle", () => {
|
||||
assert.deepEqual(LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE, {
|
||||
kind: "lab-recorded-evidence",
|
||||
clock: "source-sequence",
|
||||
cameraTransport: "generation-bound-fmp4",
|
||||
spatialTransport: "bounded-sealed-artifacts",
|
||||
loadPolicy: "visible-evidence-only",
|
||||
workerRequired: false,
|
||||
});
|
||||
assert.equal(Object.isFrozen(LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE), true);
|
||||
});
|
||||
Reference in New Issue
Block a user