fix(k1): admit live rerun only after real data
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user