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
+12
View File
@@ -56,6 +56,7 @@ _EXTRA_FIELDS: Final = (
"stale_session_retired",
"stream_id",
"ui_build_id",
"expected_ui_build_id",
"document_instance_id",
"viewer_instance_id",
"lifecycle_generation",
@@ -63,6 +64,17 @@ _EXTRA_FIELDS: Final = (
"viewer_range_max_ns",
"stalled_for_ms",
"recovery_attempt",
"camera_queue_bytes",
"camera_queue_segments",
"camera_queue_oldest_age_ms",
"camera_queue_rejection_reason",
"camera_retry_count",
"camera_generation",
"websocket_close_code",
"transport_epoch",
"camera_append_error_name",
"camera_media_source_state",
"camera_video_error_code",
"preferred_port",
"selected_port",
)
+55 -2
View File
@@ -1,10 +1,11 @@
from __future__ import annotations
import logging
import re
from collections.abc import Callable
from typing import Literal
from fastapi import APIRouter, Response
from fastapi import APIRouter, Request, Response
from fastapi.responses import JSONResponse
from pydantic import BaseModel, ConfigDict, Field
@@ -17,12 +18,27 @@ LiveViewerEventCode = Literal[
"live_receiver_recovery_exhausted",
"live_receiver_active_store_admitted",
"live_receiver_error",
"live_camera_transport_restart_requested",
"live_camera_transport_playing",
]
LiveViewerFailureStage = Literal[
"recording-open-timeout",
"viewer-start",
"module-load",
"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",
]
LIVE_VIEWER_DIAGNOSTIC_SCHEMA = "missioncore.live-viewer-diagnostic/v2"
@@ -61,6 +77,17 @@ class LiveViewerDiagnosticEvent(BaseModel):
viewer_range_max_ns: int | None = Field(default=None, ge=0)
stalled_for_ms: int | None = Field(default=None, ge=0, le=600_000)
recovery_attempt: int | None = Field(default=None, ge=1, le=3)
camera_queue_bytes: int | None = Field(default=None, ge=0, le=16_777_216)
camera_queue_segments: int | None = Field(default=None, ge=0, le=128)
camera_retry_count: int | None = Field(default=None, ge=1, le=4)
websocket_close_code: int | None = Field(default=None, ge=0, le=4_999)
transport_epoch: int | None = Field(default=None, ge=1)
camera_append_error_name: str | None = Field(
default=None,
pattern=r"^[A-Za-z][A-Za-z0-9]{0,63}$",
)
camera_media_source_state: Literal["closed", "open", "ended"] | None = None
camera_video_error_code: int | None = Field(default=None, ge=0, le=4)
def build_viewer_diagnostics_router(
@@ -70,7 +97,7 @@ def build_viewer_diagnostics_router(
router = APIRouter(prefix="/api/v1/viewer", tags=["viewer"])
@router.get("/client-contract")
def get_live_viewer_client_contract() -> JSONResponse:
def get_live_viewer_client_contract(request: Request) -> JSONResponse:
expected = expected_ui_build_id()
if expected is None:
return JSONResponse(
@@ -81,6 +108,24 @@ def build_viewer_diagnostics_router(
},
headers={"Cache-Control": "no-store"},
)
loaded = request.headers.get(UI_BUILD_HEADER)
if (
loaded not in {None, "development", expected}
and re.fullmatch(_UI_BUILD_ID_PATTERN, loaded) is not None
):
logger.info(
"Mission Core UI build drift observed; automatic page reload suppressed: "
"loaded=%s expected=%s",
loaded,
expected,
extra={
"event_code": "ui_build_drift_reload_suppressed",
"ui_build_id": loaded,
"expected_ui_build_id": expected,
"device_write_performed": False,
"automatic_retry": False,
},
)
return JSONResponse(
content={
"schema_version": LIVE_VIEWER_CLIENT_CONTRACT_SCHEMA,
@@ -142,6 +187,14 @@ def build_viewer_diagnostics_router(
"viewer_range_max_ns": event.viewer_range_max_ns,
"stalled_for_ms": event.stalled_for_ms,
"recovery_attempt": event.recovery_attempt,
"camera_queue_bytes": event.camera_queue_bytes,
"camera_queue_segments": event.camera_queue_segments,
"camera_retry_count": event.camera_retry_count,
"websocket_close_code": event.websocket_close_code,
"transport_epoch": event.transport_epoch,
"camera_append_error_name": event.camera_append_error_name,
"camera_media_source_state": event.camera_media_source_state,
"camera_video_error_code": event.camera_video_error_code,
},
)
return Response(status_code=204, headers={UI_BUILD_HEADER: expected})