fix(k1): stabilize live recovery and media admission

This commit is contained in:
DCCONSTRUCTIONS
2026-08-22 00:03:01 +03:00
parent 7217244886
commit eaad9deda1
29 changed files with 1645 additions and 199 deletions
@@ -41,7 +41,9 @@ export interface CameraStartupWatchdog {
* Supervise only the disposable browser transport. One fixed first-media
* deadline covers both an MSE that never opens and an open-but-silent socket.
* The first non-empty fragment then starts a separate first-playable-frame
* deadline; later fragments deliberately do not extend it.
* deadline. Every subsequent fragment extends that deadline: a browser that is
* still receiving the authoritative stream must not destroy its partial MSE
* decode solely because the main thread or decoder needed longer to start.
*/
export function createCameraStartupWatchdog({
schedule,
@@ -83,7 +85,7 @@ export function createCameraStartupWatchdog({
arm("first-media", firstMediaTimeoutMs);
},
markMediaReceived() {
if (mediaReceived || playing) return;
if (playing) return;
mediaReceived = true;
arm("first-playable-frame", firstPlayableFrameTimeoutMs);
},
@@ -189,6 +191,7 @@ export function MseFmp4WebSocketPlayer({
const leaseRetryRef = useRef(resetCameraLeaseRetryBudget(delivery.id));
const activeAuthorityRef = useRef(recoveryAuthorityIdentity);
const recoveryPendingAuthorityRef = useRef<string | null>(null);
const transportHealthyRef = useRef(false);
const transportEpochRef = useRef(0);
const transportAuthorityRef = useRef(recoveryAuthorityIdentity);
const activeTransportDisposeRef = useRef<(() => void) | null>(null);
@@ -243,6 +246,7 @@ export function MseFmp4WebSocketPlayer({
let receivedMedia = false;
let failed = false;
let startupWatchdog: CameraStartupWatchdog | null = null;
transportHealthyRef.current = false;
const recovering = Boolean(
activeAuthorityRef.current
&& recoveryPendingAuthorityRef.current === activeAuthorityRef.current,
@@ -254,6 +258,7 @@ export function MseFmp4WebSocketPlayer({
if (!transportIsCurrent() || failed) return;
startupWatchdog?.clear();
failed = true;
transportHealthyRef.current = false;
recoveryPendingAuthorityRef.current = null;
setStatus("error");
setMessage(copy);
@@ -280,6 +285,7 @@ export function MseFmp4WebSocketPlayer({
}
startupWatchdog?.clear();
failed = true;
transportHealthyRef.current = false;
queue.length = 0;
queuedBytes = 0;
const retry = consumeCameraLeaseRetry(leaseRetryRef.current, delivery.id);
@@ -320,6 +326,7 @@ export function MseFmp4WebSocketPlayer({
const onPlaying = () => {
if (!transportIsCurrent() || failed) return;
startupWatchdog?.markPlaying();
transportHealthyRef.current = true;
leaseRetryRef.current = resetCameraLeaseRetryBudget(delivery.id);
recoveryPendingAuthorityRef.current = null;
setStatus("playing");
@@ -464,6 +471,7 @@ export function MseFmp4WebSocketPlayer({
const disposeTransport = () => {
if (disposed) return;
disposed = true;
transportHealthyRef.current = false;
startupWatchdog?.clear();
if (retryTimer !== undefined) window.clearTimeout(retryTimer);
queue.length = 0;
@@ -518,6 +526,7 @@ export function MseFmp4WebSocketPlayer({
now: Date.now(),
documentVisible: document.visibilityState === "visible",
networkOnline: navigator.onLine !== false,
transportHealthy: transportHealthyRef.current,
});
recovery = decision.state;
if (!decision.reopen) return;
@@ -8,7 +8,7 @@ import {
initialLiveReceiverRecoveryState,
initialLiveReceiverWatchdogState,
LIVE_RECEIVER_MAX_RECOVERY_ATTEMPTS,
LIVE_RECEIVER_OPEN_MAX_AGE_MS,
LIVE_RECEIVER_OPEN_CHECK_INTERVAL_MS,
requestLiveReceiverRecovery,
} from "../core/observation/liveReceiverWatchdog";
import {
@@ -1091,25 +1091,6 @@ export function RerunViewport({
};
const clearRecordedAdmissionWatchdog = () => recordedOpenWatchdog?.clear();
const clearLiveRecordingOpenTimer = diagnosticLifecycle.clearAdmissionTimeout;
function refreshOpeningLiveReceiver(openForMs: number) {
if (disposed || recordingOpened) return;
diagnosticLifecycle.post({
eventCode: "live_receiver_restart_requested",
failureStage: "recording-open-timeout",
streamId: liveStreamIdRef.current,
backendActivitySequence: liveActivitySequenceRef.current,
viewerRangeMaxNs: latestLiveRangeMaxNs,
stalledForMs: Math.round(openForMs),
recoveryAttempt: liveRecoveryRef.current.attempts || null,
});
setStatus("loading");
onStatusChange?.(
"loading",
"Живой визуализатор обновляет приёмник продолжающегося потока.",
);
disposeViewer?.();
setRetryNonce((nonce) => nonce + 1);
}
const armLiveRecordingOpenTimer = () => {
clearLiveRecordingOpenTimer();
diagnosticLifecycle.armAdmissionTimeout(() => {
@@ -1129,17 +1110,9 @@ export function RerunViewport({
armLiveRecordingOpenTimer();
return;
}
if (observed.signal === "refresh-receiver") {
// Publication is healthy, so this is presentation-only maintenance:
// refresh the aged native receiver without spending (or clearing)
// recovery debt.
recordingOpenTimedOut = true;
refreshOpeningLiveReceiver(observed.openForMs);
return;
}
recordingOpenTimedOut = true;
requestLiveRecovery("recording-open-timeout");
}, LIVE_RECEIVER_OPEN_MAX_AGE_MS);
}, LIVE_RECEIVER_OPEN_CHECK_INTERVAL_MS);
};
const clearLiveRecordingDiscoveryTimer = diagnosticLifecycle.clearAdmissionInterval;
const clearLiveRecoveryRetryTimer = () => {