fix(k1): admit live rerun only after real data

This commit is contained in:
DCCONSTRUCTIONS
2026-08-20 12:54:55 +03:00
parent 321cde70e8
commit aee60f12b9
5 changed files with 150 additions and 31 deletions
@@ -275,6 +275,40 @@ export function isUsableRecordedPlaybackRange(
);
}
/**
* A live receiver is presentable only after the exact browser store exposes
* real timeline data that the backend has also confirmed publishing.
* `WebViewer.start()` and a non-null active recording id are transport setup,
* not evidence that the spatial scene can render.
*/
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);
}
/**
* Key the native receiver to its data-plane binding. Recovery authority is a
* retry fence projected from changing supervisor snapshots; it must not tear
* down a healthy WebViewer while this acquisition and URL remain unchanged.
*/
export function liveRerunReceiverBindingIdentity(
sourceUrl: string,
liveStreamId: string | null,
followLive: boolean,
): string {
return JSON.stringify([
followLive ? "live" : "recorded",
sourceUrl.trim(),
followLive ? liveStreamId?.trim() ?? "" : "",
]);
}
/** Describe progressive archive availability without gating first rendering. */
export function recordedPlaybackBufferState(
rangeNs: { min: number; max: number } | null,
@@ -938,6 +972,11 @@ export function RerunViewport({
presentationGate,
recordedArtifact !== null,
);
const liveReceiverBindingIdentity = liveRerunReceiverBindingIdentity(
sourceUrl,
liveStreamId,
followLive,
);
useEffect(() => subscribeToLiveViewerBuildFence(() => {
uiBuildStaleRef.current = true;
@@ -949,7 +988,7 @@ export function RerunViewport({
useEffect(() => {
liveRecoveryRef.current = initialLiveReceiverRecoveryState();
}, [followLive, liveRecoveryAuthorityIdentity, liveStreamId, sourceUrl]);
}, [liveReceiverBindingIdentity]);
useEffect(() => {
const normalizedSource = sourceUrl.trim();
@@ -1152,9 +1191,10 @@ export function RerunViewport({
recoveryAttempt: liveRecoveryRef.current.attempts || null,
});
}
const currentRecoveryAuthorityIdentity = liveRecoveryAuthorityRef.current;
const recovery = requestLiveReceiverRecovery(liveRecoveryRef.current, {
activeAuthorityIdentity: liveRecoveryAuthorityRef.current,
expectedAuthorityIdentity: liveRecoveryAuthorityIdentity,
activeAuthorityIdentity: currentRecoveryAuthorityIdentity,
expectedAuthorityIdentity: currentRecoveryAuthorityIdentity,
disposed,
});
liveRecoveryRef.current = recovery.state;
@@ -1245,7 +1285,7 @@ export function RerunViewport({
stalledForMs: Math.round(observed.stalledForMs),
recoveryAttempt: Math.min(
liveRecoveryRef.current.attempts + 1,
liveRecoveryAuthorityIdentity
liveRecoveryAuthorityRef.current
? Number.MAX_SAFE_INTEGER
: LIVE_RECEIVER_MAX_RECOVERY_ATTEMPTS,
),
@@ -1356,17 +1396,9 @@ export function RerunViewport({
) return;
recordingOpened = true;
if (!isRecordedSource) {
diagnosticLifecycle.markAdmitted();
if (liveRecoveryRef.current.awaitingRecovery) {
diagnosticLifecycle.post({
eventCode: "live_receiver_recovered",
streamId: liveStreamIdRef.current,
backendActivitySequence: liveActivitySequenceRef.current,
viewerRangeMaxNs: latestLiveRangeMaxNs,
recoveryAttempt: liveRecoveryRef.current.attempts,
});
}
liveRecoveryRef.current = initialLiveReceiverRecoveryState();
// Store discovery only establishes a candidate. Admission is
// committed below after this recording exposes a usable live
// range backed by an actual published frame.
}
if (
recordedBlueprintUrl &&
@@ -1479,7 +1511,11 @@ export function RerunViewport({
setRecordingBufferProgress(recordedBuffer.bufferProgress);
}
const readyToRender = followLive
? viewerStartResolved
? isLiveRerunPresentationReady(
viewerStartResolved,
rangeNs,
liveActivitySequenceRef.current,
)
: isRecordedPlaybackReady(viewerStartResolved, artifactVerified, recordedBuffer);
const presentationReady = presentationGateRef.current === "ready";
if (!followLive && (!readyToRender || !presentationReady) && playing) {
@@ -1531,6 +1567,25 @@ export function RerunViewport({
});
if (readyToRender && !readyPublished) {
readyPublished = true;
if (followLive) {
diagnosticLifecycle.post({
eventCode: "live_receiver_active_store_admitted",
streamId: liveStreamIdRef.current,
backendActivitySequence: liveActivitySequenceRef.current,
viewerRangeMaxNs: rangeNs?.max ?? null,
});
diagnosticLifecycle.markAdmitted();
if (liveRecoveryRef.current.awaitingRecovery) {
diagnosticLifecycle.post({
eventCode: "live_receiver_recovered",
streamId: liveStreamIdRef.current,
backendActivitySequence: liveActivitySequenceRef.current,
viewerRangeMaxNs: rangeNs?.max ?? null,
recoveryAttempt: liveRecoveryRef.current.attempts,
});
}
liveRecoveryRef.current = initialLiveReceiverRecoveryState();
}
if (!followLive) clearRecordedAdmissionWatchdog();
if (!followLive) setRecordingBufferProgress(1);
recordedSceneAdmitted = true;
@@ -1563,11 +1618,6 @@ export function RerunViewport({
// Rerun 0.34.1 may ingest an SDK gRPC store without forwarding its
// recording_open event to the JavaScript wrapper. The active store
// is the authoritative fallback and avoids hiding a ready canvas.
diagnosticLifecycle.post({
eventCode: "live_receiver_active_store_admitted",
streamId: liveStreamIdRef.current,
backendActivitySequence: liveActivitySequenceRef.current,
});
admitRecording({
application_id: "nodedc_mission_core_spatial",
recording_id: recordingId,
@@ -1716,10 +1766,8 @@ export function RerunViewport({
autoplayWhenReady,
expectedTimelineEndSeconds,
expectedTimelineStartSeconds,
followLive,
liveReceiverBindingIdentity,
initialPlaybackStartSeconds,
liveStreamId,
liveRecoveryAuthorityIdentity,
onPlaybackChange,
onPlaybackControllerChange,
onSelectionChange,
@@ -1729,7 +1777,6 @@ export function RerunViewport({
recordedArtifact?.sourceUrl,
recordedArtifact?.viewerSourceUrl,
retryNonce,
sourceUrl,
]);
useEffect(() => {