fix(k1): stabilize live recovery and media admission
This commit is contained in:
@@ -26,6 +26,7 @@ export interface CameraPlaybackRecoveryContext {
|
||||
now: number;
|
||||
documentVisible: boolean;
|
||||
networkOnline: boolean;
|
||||
transportHealthy?: boolean;
|
||||
}
|
||||
|
||||
export interface CameraPlaybackRecoveryDecision {
|
||||
@@ -186,7 +187,8 @@ export function reduceCameraPlaybackRecovery(
|
||||
} else if (event.type === "page-restore") {
|
||||
candidate = event.persisted;
|
||||
} else if (event.type === "heartbeat") {
|
||||
candidate = now - current.lastObservedAt >= CAMERA_WAKE_GAP_MS;
|
||||
candidate = context.transportHealthy !== true
|
||||
&& now - current.lastObservedAt >= CAMERA_WAKE_GAP_MS;
|
||||
}
|
||||
|
||||
const outsideCooldown = current.lastReopenAt === null
|
||||
|
||||
@@ -31,7 +31,6 @@ export interface LiveReceiverOpenWatchdogState {
|
||||
|
||||
export type LiveReceiverOpenWatchdogSignal =
|
||||
| "wait-for-store"
|
||||
| "refresh-receiver"
|
||||
| "restart-receiver";
|
||||
|
||||
export interface LiveReceiverOpenWatchdogResult {
|
||||
@@ -60,10 +59,11 @@ export interface LiveReceiverRecoveryRequest {
|
||||
export const LIVE_RECEIVER_STALL_THRESHOLD_MS = 5_000;
|
||||
export const LIVE_RECEIVER_MAX_RECOVERY_ATTEMPTS = 3;
|
||||
// The bridge publishes its URL only after StoreInfo, blueprint and static
|
||||
// scene data have been flushed. A receiver that still has not admitted that
|
||||
// store after one operator-visible four-second window is wedged, not merely
|
||||
// slow; keeping it for 48 seconds made a healthy live scan look blank.
|
||||
export const LIVE_RECEIVER_OPEN_MAX_AGE_MS = 4_000;
|
||||
// scene data have been flushed. Poll at the original four-second boundary,
|
||||
// but never discard a receiver while this exact acquisition is still proving
|
||||
// fresh publication progress: doing so throws away its partial store replay and
|
||||
// can keep a healthy long-running stream blank indefinitely.
|
||||
export const LIVE_RECEIVER_OPEN_CHECK_INTERVAL_MS = 4_000;
|
||||
const LIVE_RECEIVER_RECOVERY_DELAYS_MS = [400, 1_000, 2_000, 5_000] as const;
|
||||
|
||||
export function liveReceiverRecoveryRetryDelay(attempt: number): number {
|
||||
@@ -216,11 +216,10 @@ export function initialLiveReceiverOpenWatchdogState(
|
||||
/**
|
||||
* Keep one still-opening Rerun receiver alive while the backend is proving
|
||||
* fresh publication progress. Recreating the WASM receiver on a fixed timer
|
||||
* can repeatedly discard an otherwise healthy late StoreInfo replay. Rolling
|
||||
* patience is nevertheless bounded: a receiver that has not admitted a store
|
||||
* by the absolute open-age limit is refreshed without consuming the recovery
|
||||
* budget. A true lack of backend progress delegates to the bounded restart
|
||||
* policy. Recovery debt is cleared only after viewer admission, never merely
|
||||
* can repeatedly discard an otherwise healthy late StoreInfo replay. Preserve
|
||||
* that receiver for as long as the backend proves fresh publication. A true
|
||||
* lack of backend progress delegates to the bounded restart policy on the next
|
||||
* check. Recovery debt is cleared only after viewer admission, never merely
|
||||
* because the backend counter advanced.
|
||||
*/
|
||||
export function advanceLiveReceiverOpenWatchdog(
|
||||
@@ -228,7 +227,6 @@ export function advanceLiveReceiverOpenWatchdog(
|
||||
recoveryState: LiveReceiverRecoveryState,
|
||||
backendActivitySequence: number | null,
|
||||
nowMs = Date.now(),
|
||||
maxOpenAgeMs = LIVE_RECEIVER_OPEN_MAX_AGE_MS,
|
||||
): LiveReceiverOpenWatchdogResult {
|
||||
const sequence = validBackendActivitySequence(backendActivitySequence);
|
||||
const previous = current.lastBackendActivitySequence;
|
||||
@@ -247,9 +245,7 @@ export function advanceLiveReceiverOpenWatchdog(
|
||||
return {
|
||||
state,
|
||||
recoveryState,
|
||||
signal: openForMs >= maxOpenAgeMs
|
||||
? "refresh-receiver"
|
||||
: "wait-for-store",
|
||||
signal: "wait-for-store",
|
||||
openForMs,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,6 +129,26 @@ export function admitLiveDefaultPresentations(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A restored workspace may hide cameras that the device plugin has not selected.
|
||||
* It must still admit the exact selected delivery in a fresh browser document,
|
||||
* or a replacement delivery for a camera already presented in this acquisition.
|
||||
* An explicit close remains a separate acquisition-scoped fence in
|
||||
* `admitLiveDefaultPresentations` and de-selects the source in the plugin first.
|
||||
*/
|
||||
export function restoredLayoutMayAdmitLiveDefault(
|
||||
sources: readonly ObservationSourceDescriptor[],
|
||||
presentedAcquisitionSources: ReadonlySet<string>,
|
||||
): boolean {
|
||||
const selectedSources = sources.filter(automaticLivePresentationIdentity);
|
||||
if (selectedSources.length === 0) return false;
|
||||
if (presentedAcquisitionSources.size === 0) return true;
|
||||
return selectedSources.some((source) => {
|
||||
const lineage = livePresentationCloseFence(source);
|
||||
return Boolean(lineage && presentedAcquisitionSources.has(lineage));
|
||||
});
|
||||
}
|
||||
|
||||
function catalogIdentity(sources: readonly ObservationSourceDescriptor[]): string {
|
||||
return sources
|
||||
.map((source) => [
|
||||
@@ -170,6 +190,7 @@ export function useObservationLayout(
|
||||
const restoredLayoutAuthorityRef = useRef(false);
|
||||
const admittedLivePresentationIdentitiesRef = useRef(new Set<string>());
|
||||
const closedLiveAcquisitionSourcesRef = useRef(new Set<string>());
|
||||
const presentedLiveAcquisitionSourcesRef = useRef(new Set<string>());
|
||||
const initializedCatalog = useRef<string | null>(null);
|
||||
const sourceIdList = sources.map((source) => source.id).sort();
|
||||
const sourceIdsIdentity = sourceIdList.join("\u0000");
|
||||
@@ -182,6 +203,12 @@ export function useObservationLayout(
|
||||
|
||||
const commitVisibleIds = useCallback((next: readonly string[]) => {
|
||||
const unique = [...new Set(next)];
|
||||
const visible = new Set(unique);
|
||||
for (const source of sourcesRef.current) {
|
||||
if (!visible.has(source.id) || !automaticLivePresentationIdentity(source)) continue;
|
||||
const lineage = livePresentationCloseFence(source);
|
||||
if (lineage) presentedLiveAcquisitionSourcesRef.current.add(lineage);
|
||||
}
|
||||
visibleIdsRef.current = unique;
|
||||
setVisibleIds(unique);
|
||||
}, []);
|
||||
@@ -352,11 +379,18 @@ export function useObservationLayout(
|
||||
.filter((candidate): candidate is string => candidate !== null)
|
||||
.sort());
|
||||
|
||||
useEffect(() => {
|
||||
if (restoredLayoutAuthorityRef.current) return;
|
||||
const admitAutomaticLivePresentations = useCallback(() => {
|
||||
const currentSources = sourcesRef.current;
|
||||
if (
|
||||
restoredLayoutAuthorityRef.current
|
||||
&& !restoredLayoutMayAdmitLiveDefault(
|
||||
currentSources,
|
||||
presentedLiveAcquisitionSourcesRef.current,
|
||||
)
|
||||
) return;
|
||||
const admission = admitLiveDefaultPresentations(
|
||||
visibleIdsRef.current,
|
||||
sources,
|
||||
currentSources,
|
||||
admittedLivePresentationIdentitiesRef.current,
|
||||
closedLiveAcquisitionSourcesRef.current,
|
||||
);
|
||||
@@ -367,7 +401,11 @@ export function useObservationLayout(
|
||||
commitVisibleIds(admission.visibleIds);
|
||||
clearPresentation(admission.removedIds, false);
|
||||
persistLiveLayout();
|
||||
}, [clearPresentation, commitVisibleIds, persistLiveLayout, selectedDeliveryIdentity]);
|
||||
}, [clearPresentation, commitVisibleIds, persistLiveLayout]);
|
||||
|
||||
useEffect(() => {
|
||||
admitAutomaticLivePresentations();
|
||||
}, [admitAutomaticLivePresentations, selectedDeliveryIdentity]);
|
||||
|
||||
const markPending = useCallback((source: ObservationSourceDescriptor, pending: boolean) => {
|
||||
const groupId = source.activation?.groupId;
|
||||
@@ -545,7 +583,10 @@ export function useObservationLayout(
|
||||
});
|
||||
restoredLayoutAuthorityRef.current = true;
|
||||
applyDesiredSnapshot(desiredSnapshotRef.current, "reset");
|
||||
}, [applyDesiredSnapshot]);
|
||||
// The selected delivery may already have arrived before the saved layout.
|
||||
// Re-run admission here so restore ordering cannot strand its camera window.
|
||||
admitAutomaticLivePresentations();
|
||||
}, [admitAutomaticLivePresentations, applyDesiredSnapshot]);
|
||||
|
||||
const visibleSourceIds = useMemo(() => new Set(visibleIds), [visibleIds]);
|
||||
const pendingSourceIds = useMemo(() => new Set(pendingIds), [pendingIds]);
|
||||
|
||||
Reference in New Issue
Block a user