refactor(viewer): profile rerun loading by playback stage

This commit is contained in:
DCCONSTRUCTIONS
2026-08-29 17:46:07 +03:00
parent 856b61be99
commit c06b709fd7
21 changed files with 1313 additions and 609 deletions
@@ -849,12 +849,14 @@ export async function fetchM4ThreatTimelineChunk(
endpointRoot = M4_THREAT_TIMELINE_ENDPOINT_ROOT,
cameraObstacleProjectionDelivery = null,
playbackPointPack,
includePoints = true,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
endpointRoot?: string;
cameraObstacleProjectionDelivery?: M4ThreatTimeline["cameraObstacleProjectionDelivery"];
playbackPointPack?: M4ThreatPlaybackPointPack;
includePoints?: boolean;
} = {},
): Promise<M4ThreatTimelineChunk> {
const params = new URLSearchParams({
@@ -864,7 +866,7 @@ export async function fetchM4ThreatTimelineChunk(
if (cameraObstacleProjectionDelivery !== null) {
params.set("obstacle_projection", cameraObstacleProjectionDelivery);
}
if (playbackPointPack) params.set("include_points", "false");
if (playbackPointPack || !includePoints) params.set("include_points", "false");
const response = await fetcher(
`${endpointRoot}/${result}/timeline/chunk?${params}`,
{ headers: { Accept: "application/json" }, signal },
@@ -0,0 +1,57 @@
import {
LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE,
type LaboratoryRecordedEvidenceViewerProfile,
} from "../observation/viewerProfile";
export type LaboratoryRecordedMediaMode = "video" | "camera" | null;
export type LaboratoryRecordedSpatialMode = "3d" | "plan" | null;
export type LaboratoryClassifiedSpatialMode = "none" | "overlay" | "replace-source";
export interface LaboratoryRecordedEvidenceVisibility {
mediaMode: LaboratoryRecordedMediaMode;
spatialMode: LaboratoryRecordedSpatialMode;
showMediaSemantic: boolean;
showSpatialSemantic: boolean;
showMediaPoints: boolean;
classifiedSpatialMode: LaboratoryClassifiedSpatialMode;
}
export interface LaboratoryRecordedEvidenceDemand {
sourceTimelineMetadata: true;
recordedVideo: boolean;
exactCameraFrame: boolean;
sourceSpatialPoints: boolean;
cameraPointOverlay: boolean;
selectedSemanticMask: boolean;
selectedSemanticPoints: boolean;
classifiedSpatial: boolean;
}
/**
* Translate the already-visible M4 evidence composition into explicit data
* demand. The profile never starts a hidden point, semantic or camera channel
* merely because the selected LAB happens to publish that artifact.
*/
export function laboratoryRecordedEvidenceDemand(
visibility: LaboratoryRecordedEvidenceVisibility,
profile: LaboratoryRecordedEvidenceViewerProfile =
LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE,
): LaboratoryRecordedEvidenceDemand {
if (profile.loadPolicy !== "visible-evidence-only") {
throw new Error("Unsupported LAB recorded evidence load policy");
}
const mediaVisible = visibility.mediaMode !== null;
const spatialVisible = visibility.spatialMode !== null;
return {
sourceTimelineMetadata: true,
recordedVideo: visibility.mediaMode === "video",
exactCameraFrame: visibility.mediaMode === "camera",
sourceSpatialPoints:
spatialVisible && visibility.classifiedSpatialMode !== "replace-source",
cameraPointOverlay: mediaVisible && visibility.showMediaPoints,
selectedSemanticMask: mediaVisible && visibility.showMediaSemantic,
selectedSemanticPoints: spatialVisible && visibility.showSpatialSemantic,
classifiedSpatial:
spatialVisible && visibility.classifiedSpatialMode !== "none",
};
}
@@ -0,0 +1,70 @@
import { isUsableRecordedPlaybackRange } from "./recordedRerunLifecycle";
export function createReentrantViewerDisposer(
cleanupOnce: () => void,
releaseNativeViewer: () => void,
): () => void {
let cleanupComplete = false;
return () => {
try {
if (!cleanupComplete) {
cleanupComplete = true;
cleanupOnce();
}
} finally {
// A deferred native start can resolve after a pre-ready stop. Reapply
// release at every boundary so it cannot reopen after React unmount.
releaseNativeViewer();
}
};
}
interface ActiveLiveViewerOwner {
release: () => void;
}
let activeLiveViewerOwner: ActiveLiveViewerOwner | null = null;
/** Own exactly one native live receiver per application document. */
export function claimExclusiveLiveViewer(release: () => void): () => void {
const owner = { release };
const previous = activeLiveViewerOwner;
activeLiveViewerOwner = owner;
previous?.release();
return () => {
if (activeLiveViewerOwner === owner) activeLiveViewerOwner = null;
};
}
export function isLiveRerunPresentationReady(
viewerStarted: boolean,
rangeNs: { min: number; max: number } | null,
backendActivitySequence: number | null,
): boolean {
return viewerStarted
&& Number.isSafeInteger(backendActivitySequence)
&& (backendActivitySequence ?? 0) > 0
&& isUsableRecordedPlaybackRange(rangeNs);
}
export function liveTimelineNeedsSynchronization(
followLive: boolean,
activeTimeline: string | null | undefined,
rangeNs: { min: number; max: number } | null,
): boolean {
return followLive
&& isUsableRecordedPlaybackRange(rangeNs)
&& activeTimeline !== "stream_time";
}
export function liveRerunReceiverBindingIdentity(
sourceUrl: string,
liveStreamId: string | null,
followLive: boolean,
): string {
return JSON.stringify([
followLive ? "live" : "recorded",
sourceUrl.trim(),
followLive ? liveStreamId?.trim() ?? "" : "",
]);
}
@@ -327,6 +327,53 @@ export function verifyLiveViewerClientBuild(
});
}
/**
* Recover a recorded viewer whose lazy module disappeared during a frontend
* deployment. Live acquisition deliberately never reloads automatically, but
* a saved recording has no device-side authority to preserve and can safely
* move to the current immutable application build.
*/
export async function reloadRecordedViewerAfterStaleModuleFailure({
loadedUiBuildId,
signal,
fetcher = globalThis.fetch,
reload = () => window.location.reload(),
}: {
loadedUiBuildId: string;
signal?: AbortSignal;
fetcher?: typeof globalThis.fetch;
reload?: () => void;
}): Promise<boolean> {
if (
signal?.aborted ||
loadedUiBuildId === DEVELOPMENT_UI_BUILD_ID ||
!HASHED_UI_BUILD_ID.test(loadedUiBuildId)
) return false;
try {
const response = await fetcher("/api/v1/viewer/client-contract", {
method: "GET",
headers: {
Accept: "application/json",
"X-MissionCore-UI-Build": loadedUiBuildId,
},
cache: "no-store",
signal,
});
if (signal?.aborted || !response.ok) return false;
const expectedUiBuildId = response.headers.get(UI_BUILD_HEADER);
if (
!expectedUiBuildId ||
expectedUiBuildId === loadedUiBuildId ||
!HASHED_UI_BUILD_ID.test(expectedUiBuildId)
) return false;
browserUiBuildCoordinator().report({ loadedUiBuildId, expectedUiBuildId });
reload();
return true;
} catch {
return false;
}
}
function startBuildMonitor(): void {
if (buildMonitorAbort || typeof window === "undefined") return;
const lineage = createLiveViewerLineage(createLiveViewerInstanceId(), 1);
@@ -0,0 +1,260 @@
import type { SceneSettings } from "../../sceneSettings";
import type { RecordedAdmissionPhase } from "./recordedSessionAdmission";
import type { RerunPlaybackState, RerunViewportStatus } from "./viewerProfile";
const BUFFER_END_TOLERANCE_NS = 1_000_000;
const RECORDED_OPEN_MIN_TIMEOUT_MS = 120_000;
const RECORDED_OPEN_MAX_TIMEOUT_MS = 1_800_000;
const RECORDED_OPEN_GRACE_MS = 30_000;
const RECORDED_OPEN_MIN_BYTES_PER_SECOND = 2 * 1024 * 1024;
export const RECORDED_BASE_POINT_COLOR_KEY = "intensity|turbo|-";
export interface RecordedPlaybackBufferState {
bufferedEndNs: number | null;
expectedStartNs: number | null;
expectedEndNs: number | null;
bufferProgress: number | null;
fullyBuffered: boolean;
}
export function recordedPointColorKey(
settings: Pick<SceneSettings, "colorMode" | "palette" | "customColor">,
): string {
const custom = settings.palette === "custom" || settings.colorMode === "class"
? settings.customColor.toLowerCase()
: "-";
return `${settings.colorMode}|${settings.palette}|${custom}`;
}
export function recordedOpenWatchdogTimeoutMs(byteLength: number): number {
if (!Number.isSafeInteger(byteLength) || byteLength < 4) {
throw new Error("Unsafe recorded RRD byte length");
}
const transferBudgetMs = Math.ceil(
(byteLength / RECORDED_OPEN_MIN_BYTES_PER_SECOND) * 1_000,
);
return Math.min(
RECORDED_OPEN_MAX_TIMEOUT_MS,
Math.max(RECORDED_OPEN_MIN_TIMEOUT_MS, transferBudgetMs + RECORDED_OPEN_GRACE_MS),
);
}
export function createRecordedOpenWatchdog<T>({
byteLength,
schedule,
cancel,
onTimeout,
}: {
byteLength: number;
schedule: (callback: () => void, timeoutMs: number) => T;
cancel: (handle: T) => void;
onTimeout: () => void;
}): { arm: () => void; clear: () => void; pending: () => boolean } {
const timeoutMs = recordedOpenWatchdogTimeoutMs(byteLength);
let handle: T | null = null;
return {
arm() {
if (handle !== null) return;
handle = schedule(() => {
handle = null;
onTimeout();
}, timeoutMs);
},
clear() {
if (handle === null) return;
cancel(handle);
handle = null;
},
pending: () => handle !== null,
};
}
export function isUsableRecordedPlaybackRange(
rangeNs: { min: number; max: number } | null,
): rangeNs is { min: number; max: number } {
return Boolean(
rangeNs
&& Number.isFinite(rangeNs.min)
&& Number.isFinite(rangeNs.max)
&& rangeNs.max >= rangeNs.min,
);
}
export function recordedPlaybackBufferState(
rangeNs: { min: number; max: number } | null,
expectedTimelineEndSeconds?: number,
expectedTimelineStartSeconds?: number,
): RecordedPlaybackBufferState {
const usableRange = isUsableRecordedPlaybackRange(rangeNs) ? rangeNs : null;
const bufferedEndNs = usableRange?.max ?? null;
if (expectedTimelineEndSeconds === undefined) {
return {
bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null,
bufferProgress: null,
fullyBuffered: usableRange !== null,
};
}
if (!Number.isFinite(expectedTimelineEndSeconds) || expectedTimelineEndSeconds < 0) {
return {
bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null,
bufferProgress: null,
fullyBuffered: false,
};
}
const expectedEndNs = expectedTimelineEndSeconds * 1_000_000_000;
const expectedStartNs = expectedTimelineStartSeconds === undefined
? 0
: expectedTimelineStartSeconds * 1_000_000_000;
if (
!Number.isFinite(expectedEndNs)
|| !Number.isFinite(expectedStartNs)
|| expectedStartNs < 0
|| expectedEndNs < expectedStartNs
) {
return {
bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null,
bufferProgress: null,
fullyBuffered: false,
};
}
const fullyBuffered = usableRange !== null
&& usableRange.min <= expectedStartNs + BUFFER_END_TOLERANCE_NS
&& usableRange.max >= expectedEndNs - BUFFER_END_TOLERANCE_NS;
const expectedDurationNs = expectedEndNs - expectedStartNs;
const bufferProgress = bufferedEndNs === null
? 0
: expectedDurationNs <= 0
? (fullyBuffered ? 1 : 0)
: Math.min(1, Math.max(0, (bufferedEndNs - expectedStartNs) / expectedDurationNs));
return {
bufferedEndNs,
expectedStartNs,
expectedEndNs,
bufferProgress,
fullyBuffered,
};
}
export function isRecordedPlaybackReady(
viewerStarted: boolean,
artifactVerified: boolean,
buffer: RecordedPlaybackBufferState,
): boolean {
return viewerStarted && artifactVerified && buffer.bufferedEndNs !== null;
}
export function recordedPlaybackRangeWhenReady(
rangeNs: { min: number; max: number } | null,
buffer: RecordedPlaybackBufferState,
artifactVerified: boolean,
): { min: number; max: number } | null {
if (
!artifactVerified
|| !buffer.fullyBuffered
|| !isUsableRecordedPlaybackRange(rangeNs)
) return null;
return {
min: buffer.expectedStartNs === null
? rangeNs.min
: Math.max(rangeNs.min, buffer.expectedStartNs),
max: buffer.expectedEndNs === null
? rangeNs.max
: Math.min(rangeNs.max, buffer.expectedEndNs),
};
}
export function isRecordedPlaybackPresentationReady(
status: RerunViewportStatus,
playback: RerunPlaybackState | null,
): boolean {
return status === "ready"
&& playback?.fullyBuffered === true
&& isUsableRecordedPlaybackRange(playback.rangeNs);
}
export function rerunPresentationStatus(
status: RerunViewportStatus,
_gate: RecordedAdmissionPhase,
recorded: boolean,
): RerunViewportStatus {
if (!recorded) return status;
return status === "error" ? "error" : status;
}
export function isRecordedPlaybackFullyBuffered(
rangeNs: { min: number; max: number } | null,
expectedTimelineEndSeconds?: number,
): boolean {
return recordedPlaybackBufferState(rangeNs, expectedTimelineEndSeconds).fullyBuffered;
}
export function attemptRecordedAutoplay(
seekToStart: () => void,
startPlaying: () => void,
): boolean {
try {
seekToStart();
startPlaying();
return true;
} catch {
return false;
}
}
export function createRecordedAutoplayGate(): {
attempt: (
viewerStarted: boolean,
fullyBuffered: boolean,
presentationReady: boolean,
rangeNs: { min: number; max: number } | null,
seekToStart: (startNs: number) => void,
startPlaying: () => void,
preferredStartNs?: number,
) => boolean;
attempted: () => boolean;
} {
let consumed = false;
return {
attempt(
viewerStarted,
fullyBuffered,
presentationReady,
rangeNs,
seekToStart,
startPlaying,
preferredStartNs,
) {
if (
consumed
|| !viewerStarted
|| !fullyBuffered
|| !presentationReady
|| !isUsableRecordedPlaybackRange(rangeNs)
) return false;
consumed = true;
const startNs = Number.isFinite(preferredStartNs)
? Math.min(Math.max(preferredStartNs as number, rangeNs.min), rangeNs.max)
: rangeNs.min;
return attemptRecordedAutoplay(
() => seekToStart(startNs),
startPlaying,
);
},
attempted: () => consumed,
};
}
export function canPublishRecordedPlaybackController(
readyToRender: boolean,
presentationGate: RecordedAdmissionPhase,
): boolean {
return readyToRender && presentationGate === "ready";
}
@@ -0,0 +1,114 @@
import type { RecordedAdmissionPhase } from "./recordedSessionAdmission";
export type RerunViewportStatus = "idle" | "loading" | "ready" | "error";
export type RecordedRerunView = "spatial" | "perception" | "perception3d" | "metrics";
export interface RerunPlaybackState {
recordingId: string;
timeline: string;
rangeNs: { min: number; max: number } | null;
currentNs: number;
playing: boolean;
bufferedEndNs: number | null;
expectedStartNs: number | null;
expectedEndNs: number | null;
bufferProgress: number | null;
fullyBuffered: boolean;
}
export interface RerunPlaybackController {
seek: (timeNs: number) => void;
setPlaying: (playing: boolean) => void;
jumpToEnd: () => void;
}
export interface RecordedPerceptionLayers {
enabled: boolean;
detections2d: boolean;
segmentation: boolean;
cuboids3d: boolean;
}
export interface RecordedRrdArtifactDescriptor {
sourceUrl: string;
viewerSourceUrl: string;
byteLength: number;
sha256: string;
}
/**
* The native live receiver owns one acquisition lineage and never exposes
* recorded transport or autoplay policy.
*/
export interface LiveAcquisitionRerunProfile {
kind: "live-acquisition";
clock: "stream_time";
sourceUrl: string;
liveActivitySequence: number | null;
liveStreamId: string | null;
liveRecoveryAuthorityIdentity: string | null;
}
/**
* A sealed session is progressively presentable, while its shared playback
* controls remain fenced by the aggregate session admission gate.
*/
export interface RecordedSessionRerunProfile {
kind: "recorded-session";
clock: "session_time";
sourceUrl: string;
artifact: RecordedRrdArtifactDescriptor | null;
autoplayWhenReady: boolean;
presentationGate: RecordedAdmissionPhase;
expectedTimelineStartSeconds?: number;
expectedTimelineEndSeconds?: number;
initialPlaybackStartSeconds?: number;
view: RecordedRerunView;
viewResetGeneration: 0 | 1;
followTrajectory: boolean;
perceptionLayers: RecordedPerceptionLayers;
perceptionRetryGeneration: number;
lockPerceptionCameraInteraction: boolean;
}
/**
* LAB recorded evidence is not a native Rerun mode. It owns a source-sequence
* clock and composes the existing sealed fMP4 and retained spatial primitives.
*/
export interface LaboratoryRecordedEvidenceViewerProfile {
kind: "lab-recorded-evidence";
clock: "source-sequence";
cameraTransport: "generation-bound-fmp4";
spatialTransport: "bounded-sealed-artifacts";
loadPolicy: "visible-evidence-only";
workerRequired: false;
}
export type RerunViewerProfile =
| LiveAcquisitionRerunProfile
| RecordedSessionRerunProfile;
export type ObservationViewerProfile =
| RerunViewerProfile
| LaboratoryRecordedEvidenceViewerProfile;
export const LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE = Object.freeze({
kind: "lab-recorded-evidence",
clock: "source-sequence",
cameraTransport: "generation-bound-fmp4",
spatialTransport: "bounded-sealed-artifacts",
loadPolicy: "visible-evidence-only",
workerRequired: false,
} satisfies LaboratoryRecordedEvidenceViewerProfile);
export function liveAcquisitionRerunProfile(
input: Omit<LiveAcquisitionRerunProfile, "kind" | "clock">,
): LiveAcquisitionRerunProfile {
return { kind: "live-acquisition", clock: "stream_time", ...input };
}
export function recordedSessionRerunProfile(
input: Omit<RecordedSessionRerunProfile, "kind" | "clock">,
): RecordedSessionRerunProfile {
return { kind: "recorded-session", clock: "session_time", ...input };
}