fix(k1): harden live handoff and camera recovery

This commit is contained in:
DCCONSTRUCTIONS
2026-08-22 13:09:24 +03:00
parent eaad9deda1
commit 85035fa07b
26 changed files with 1478 additions and 170 deletions
@@ -4,13 +4,28 @@ export type LiveViewerDiagnosticEventCode =
| "live_receiver_recovered"
| "live_receiver_recovery_exhausted"
| "live_receiver_active_store_admitted"
| "live_receiver_error";
| "live_receiver_error"
| "live_camera_transport_restart_requested"
| "live_camera_transport_playing";
export type LiveViewerFailureStage =
| "recording-open-timeout"
| "viewer-start"
| "module-load"
| "receiver-stalled";
| "receiver-stalled"
| "camera-first-media-timeout"
| "camera-first-playable-timeout"
| "camera-queue-capacity"
| "camera-append-quota"
| "camera-append-error"
| "camera-source-buffer-error"
| "camera-fragment-decode"
| "camera-websocket-error"
| "camera-websocket-close"
| "camera-heartbeat-reopen"
| "camera-visibility-reopen"
| "camera-network-online-reopen"
| "camera-page-restore-reopen";
export interface LiveViewerDiagnostic {
eventCode: LiveViewerDiagnosticEventCode;
@@ -20,6 +35,14 @@ export interface LiveViewerDiagnostic {
viewerRangeMaxNs?: number | null;
stalledForMs?: number | null;
recoveryAttempt?: number | null;
cameraQueueBytes?: number | null;
cameraQueueSegments?: number | null;
cameraRetryCount?: number | null;
websocketCloseCode?: number | null;
transportEpoch?: number | null;
cameraAppendErrorName?: string | null;
cameraMediaSourceState?: "closed" | "open" | "ended" | null;
cameraVideoErrorCode?: number | null;
}
export interface LiveViewerLineage {
@@ -69,7 +92,6 @@ const DEVELOPMENT_UI_BUILD_ID = "development";
const SAFE_STREAM_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const HASHED_UI_BUILD_ID = /^\/assets\/[A-Za-z0-9._/-]+-[A-Za-z0-9_-]{8,}\.js$/;
const UI_BUILD_CHECK_INTERVAL_MILLISECONDS = 15_000;
const UI_BUILD_RELOAD_DELAY_MILLISECONDS = 50;
let documentInstanceId: string | null = null;
let sharedUiBuildCoordinator: UiBuildStaleCoordinator | null = null;
@@ -172,32 +194,45 @@ export function liveViewerDiagnosticBody(
...(safeInteger(event.recoveryAttempt) === undefined
? {}
: { recovery_attempt: safeInteger(event.recoveryAttempt) }),
...(safeInteger(event.cameraQueueBytes) === undefined
? {}
: { camera_queue_bytes: safeInteger(event.cameraQueueBytes) }),
...(safeInteger(event.cameraQueueSegments) === undefined
? {}
: { camera_queue_segments: safeInteger(event.cameraQueueSegments) }),
...(safeInteger(event.cameraRetryCount) === undefined
? {}
: { camera_retry_count: safeInteger(event.cameraRetryCount) }),
...(safeInteger(event.websocketCloseCode) === undefined
? {}
: { websocket_close_code: safeInteger(event.websocketCloseCode) }),
...(safeInteger(event.transportEpoch) === undefined
? {}
: { transport_epoch: safeInteger(event.transportEpoch) }),
...(event.cameraAppendErrorName
? { camera_append_error_name: event.cameraAppendErrorName }
: {}),
...(event.cameraMediaSourceState
? { camera_media_source_state: event.cameraMediaSourceState }
: {}),
...(safeInteger(event.cameraVideoErrorCode) === undefined
? {}
: { camera_video_error_code: safeInteger(event.cameraVideoErrorCode) }),
};
}
export function createUiBuildStaleCoordinator({
scheduleReload,
reload,
}: {
scheduleReload: (callback: () => void, delayMilliseconds: number) => void;
reload: () => void;
}): UiBuildStaleCoordinator {
const listeners = new Set<(event: StaleUiBuild) => void>();
export function createUiBuildStaleCoordinator(): UiBuildStaleCoordinator {
let staleEvent: StaleUiBuild | null = null;
let reloadScheduled = false;
return {
subscribe(listener) {
listeners.add(listener);
if (staleEvent) listener(staleEvent);
return () => listeners.delete(listener);
subscribe(_listener) {
// Build drift is diagnostic-only. The loaded document and its local
// Rerun/camera transports remain authoritative until normal unmount or
// their existing bounded recovery replaces them.
return () => undefined;
},
report(event) {
if (event.loadedUiBuildId === event.expectedUiBuildId || staleEvent) return;
staleEvent = event;
for (const listener of [...listeners]) listener(event);
if (reloadScheduled) return;
reloadScheduled = true;
scheduleReload(reload, UI_BUILD_RELOAD_DELAY_MILLISECONDS);
},
stale() {
return staleEvent !== null;
@@ -206,12 +241,7 @@ export function createUiBuildStaleCoordinator({
}
function browserUiBuildCoordinator(): UiBuildStaleCoordinator {
sharedUiBuildCoordinator ??= createUiBuildStaleCoordinator({
scheduleReload: (callback, delayMilliseconds) => {
window.setTimeout(callback, delayMilliseconds);
},
reload: () => window.location.reload(),
});
sharedUiBuildCoordinator ??= createUiBuildStaleCoordinator();
return sharedUiBuildCoordinator;
}
@@ -254,10 +284,17 @@ export function verifyLiveViewerClientBuild(
lineage: Pick<LiveViewerLineage, "uiBuildId">,
signal?: AbortSignal,
): void {
if (signal?.aborted || lineage.uiBuildId === DEVELOPMENT_UI_BUILD_ID) return;
if (
signal?.aborted ||
lineage.uiBuildId === DEVELOPMENT_UI_BUILD_ID ||
browserUiBuildCoordinator().stale()
) return;
void fetch("/api/v1/viewer/client-contract", {
method: "GET",
headers: { Accept: "application/json" },
headers: {
Accept: "application/json",
"X-MissionCore-UI-Build": lineage.uiBuildId,
},
cache: "no-store",
signal,
}).then(async (response) => {
@@ -0,0 +1,19 @@
import type { MissionRuntimeState } from "../runtime/contracts";
export function viewerSettingsTargetIdentity(
state: MissionRuntimeState | null,
): string {
const deviceSessionId = state?.deviceSession?.sessionId?.trim();
const sessionDeviceId = state?.deviceSession?.deviceId?.trim();
if (deviceSessionId || sessionDeviceId) {
return ["device-session", deviceSessionId, sessionDeviceId]
.filter(Boolean)
.join(":");
}
return [
state?.activeDevice?.pluginId,
state?.activeDevice?.modelId,
state?.activeDevice?.instanceId,
].filter(Boolean).join(":") || "local-runtime";
}