diff --git a/apps/control-station/src/components/FloatingObservationWindow.tsx b/apps/control-station/src/components/FloatingObservationWindow.tsx index 1db8273..4db59a0 100644 --- a/apps/control-station/src/components/FloatingObservationWindow.tsx +++ b/apps/control-station/src/components/FloatingObservationWindow.tsx @@ -123,9 +123,12 @@ export function FloatingObservationWindow({ return () => observer.disconnect(); }, [boundsRef]); - const initialRect = bounds ? initialObservationWindowRect(index, count, bounds) : null; + // Camera media admission must not wait for the viewport observer. A source + // can arrive in the same commit as a workspace transition; mounting it with + // the canonical fallback geometry keeps the browser transport alive until + // the real bounds are measured immediately afterward. + const initialRect = initialObservationWindowRect(index, count, bounds ?? undefined); const windowRect = rect ?? initialRect; - if (!windowRect) return null; return ( { } = await server.ssrLoadModule( "/src/core/observation/liveCameraRecovery.ts", )); - ({ initialObservationWindowRect, shouldCaptureWorkspacePointer } = await server.ssrLoadModule( - "/src/components/FloatingObservationWindow.tsx", - )); + ({ + FloatingObservationWindow, + initialObservationWindowRect, + shouldCaptureWorkspacePointer, + } = await server.ssrLoadModule("/src/components/FloatingObservationWindow.tsx")); ({ liveRerunRecoveryAuthorityIdentity } = await server.ssrLoadModule( "/src/core/observation/liveReceiverWatchdog.ts", )); @@ -133,6 +136,59 @@ test("observation camera windows tile from the bottom-right above the live timel assert.ok(right.y + right.height <= bounds.height - 64); }); +test("live camera window keeps its player mounted before viewport measurement", () => { + const source = { + id: "k1:sensor.camera.right", + sourceId: "sensor.camera.right", + label: "Правая камера", + description: "Live camera", + modality: "video", + availability: "streaming", + transport: "websocket", + endpointLabel: "Локальный видеопоток", + previewUrl: null, + delivery: { + id: "camera-preview-4", + kind: "mse-fmp4-websocket", + url: "/camera-preview/4", + mediaType: 'video/mp4; codecs="avc1.641028"', + }, + activation: { + groupId: "k1:device-session:camera.preview.decoder", + maxActive: 1, + selected: true, + controllable: false, + }, + binding: { + deviceId: "k1-a", + deviceSessionId: "device-session", + acquisitionId: "acquisition-quick", + }, + capabilities: { + defaultVisible: true, + overlay: true, + resizable: true, + timelineMode: "live-only", + }, + }; + const noop = () => undefined; + const markup = renderToStaticMarkup(createElement(FloatingObservationWindow, { + source, + index: 0, + count: 1, + boundsRef: { current: null }, + maximized: false, + active: true, + onRectChange: noop, + onMaximizedChange: noop, + onActivate: noop, + onClose: noop, + })); + + assert.match(markup, /