diff --git a/apps/control-station/src/components/MseFmp4WebSocketPlayer.tsx b/apps/control-station/src/components/MseFmp4WebSocketPlayer.tsx index 374a8cc..278004a 100644 --- a/apps/control-station/src/components/MseFmp4WebSocketPlayer.tsx +++ b/apps/control-station/src/components/MseFmp4WebSocketPlayer.tsx @@ -261,6 +261,18 @@ export function MseFmp4WebSocketPlayer({ }); activeDiagnosticRef.current = diagnosticLifecycle; diagnosticLifecycle.verifyBuild(); + diagnosticLifecycle.post({ + eventCode: "live_camera_player_effect_started", + streamId: delivery.id, + transportEpoch, + }); + if (!recoveryAuthorityIdentity) { + diagnosticLifecycle.post({ + eventCode: "live_camera_playback_authority_missing", + streamId: delivery.id, + transportEpoch, + }); + } const transportIsCurrent = () => cameraTransportCallbackIsCurrent( transportEpochRef.current, transportEpoch, @@ -473,6 +485,11 @@ export function MseFmp4WebSocketPlayer({ const onSourceOpen = () => { if (!transportIsCurrent() || failed) return; + diagnosticLifecycle.post({ + eventCode: "live_camera_media_source_open", + streamId: delivery.id, + transportEpoch, + }); try { sourceBuffer = mediaSource.addSourceBuffer(mediaType); } catch { @@ -525,6 +542,11 @@ export function MseFmp4WebSocketPlayer({ socket.binaryType = "arraybuffer"; socket.addEventListener("open", () => { if (!transportIsCurrent() || failed) return; + diagnosticLifecycle.post({ + eventCode: "live_camera_websocket_open", + streamId: delivery.id, + transportEpoch, + }); startupWatchdog?.armFirstMedia(); setStatus("buffering"); setMessage(recovering diff --git a/apps/control-station/src/core/observation/liveViewerDiagnostics.ts b/apps/control-station/src/core/observation/liveViewerDiagnostics.ts index d582db0..46b865e 100644 --- a/apps/control-station/src/core/observation/liveViewerDiagnostics.ts +++ b/apps/control-station/src/core/observation/liveViewerDiagnostics.ts @@ -7,6 +7,10 @@ export type LiveViewerDiagnosticEventCode = | "live_receiver_error" | "live_camera_source_projected" | "live_camera_window_admitted" + | "live_camera_player_effect_started" + | "live_camera_playback_authority_missing" + | "live_camera_media_source_open" + | "live_camera_websocket_open" | "live_camera_transport_restart_requested" | "live_camera_transport_playing"; diff --git a/apps/control-station/test/observationSources.test.mjs b/apps/control-station/test/observationSources.test.mjs index c0f334a..243e80a 100644 --- a/apps/control-station/test/observationSources.test.mjs +++ b/apps/control-station/test/observationSources.test.mjs @@ -203,6 +203,24 @@ test("live camera registers sourceopen before assigning the MSE object URL", asy ); }); +test("live camera reports each browser startup boundary without device actions", async () => { + const source = await readFile( + new URL("../src/components/MseFmp4WebSocketPlayer.tsx", import.meta.url), + "utf8", + ); + for (const eventCode of [ + "live_camera_player_effect_started", + "live_camera_playback_authority_missing", + "live_camera_media_source_open", + "live_camera_websocket_open", + ]) { + assert.ok( + source.includes(`eventCode: "${eventCode}"`), + `missing bounded player diagnostic ${eventCode}`, + ); + } +}); + test("recorded observation timeline clamps relative seek time without epoch precision in the UI", () => { const range = normalizeTimelineRange({ min: 0, diff --git a/src/k1link/web/viewer_diagnostics_api.py b/src/k1link/web/viewer_diagnostics_api.py index a29e516..a73847d 100644 --- a/src/k1link/web/viewer_diagnostics_api.py +++ b/src/k1link/web/viewer_diagnostics_api.py @@ -20,6 +20,10 @@ LiveViewerEventCode = Literal[ "live_receiver_error", "live_camera_source_projected", "live_camera_window_admitted", + "live_camera_player_effect_started", + "live_camera_playback_authority_missing", + "live_camera_media_source_open", + "live_camera_websocket_open", "live_camera_transport_restart_requested", "live_camera_transport_playing", ] diff --git a/tests/test_viewer_diagnostics_api.py b/tests/test_viewer_diagnostics_api.py index a13f2fa..8ba6fd9 100644 --- a/tests/test_viewer_diagnostics_api.py +++ b/tests/test_viewer_diagnostics_api.py @@ -247,6 +247,10 @@ def test_live_viewer_diagnostic_endpoint_accepts_only_bounded_events( for event_code in ( "live_camera_source_projected", "live_camera_window_admitted", + "live_camera_player_effect_started", + "live_camera_playback_authority_missing", + "live_camera_media_source_open", + "live_camera_websocket_open", ): boundary_event = LiveViewerDiagnosticEvent( schema_version="missioncore.live-viewer-diagnostic/v2", @@ -256,6 +260,12 @@ def test_live_viewer_diagnostic_endpoint_accepts_only_bounded_events( viewer_instance_id="00000000-0000-4000-8000-000000000004", lifecycle_generation=1, stream_id="camera-preview-2", + transport_epoch=( + 1 + if event_code + not in {"live_camera_source_projected", "live_camera_window_admitted"} + else None + ), ) with caplog.at_level( logging.INFO,