fix(k1): harden live handoff and camera recovery
This commit is contained in:
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user