From 687c050c28325d48a2199dcdb9f3d4cd6c679348 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sun, 23 Aug 2026 12:47:54 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B3=D0=BE=D0=BD=D0=BA=D0=B8=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=83=D1=81=D0=BA=D0=B0=20live-=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/MseFmp4WebSocketPlayer.tsx | 7 +++++-- .../test/observationSources.test.mjs | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/apps/control-station/src/components/MseFmp4WebSocketPlayer.tsx b/apps/control-station/src/components/MseFmp4WebSocketPlayer.tsx index b9865b0..374a8cc 100644 --- a/apps/control-station/src/components/MseFmp4WebSocketPlayer.tsx +++ b/apps/control-station/src/components/MseFmp4WebSocketPlayer.tsx @@ -470,8 +470,6 @@ export function MseFmp4WebSocketPlayer({ : "Подключение к локальному видеопотоку"); const mediaSource = new MediaSource(); objectUrl = URL.createObjectURL(mediaSource); - video.src = objectUrl; - startupWatchdog?.armFirstMedia(); const onSourceOpen = () => { if (!transportIsCurrent() || failed) return; @@ -562,7 +560,12 @@ export function MseFmp4WebSocketPlayer({ }); }; + // Register before assigning the object URL. `video.src` is the action that + // lets the browser open this MediaSource; a fast sequential MSE lease must + // not fire `sourceopen` in the gap before its only listener exists. mediaSource.addEventListener("sourceopen", onSourceOpen, { once: true }); + startupWatchdog?.armFirstMedia(); + video.src = objectUrl; const disposeTransport = () => { if (disposed) return; diff --git a/apps/control-station/test/observationSources.test.mjs b/apps/control-station/test/observationSources.test.mjs index a9cc814..c0f334a 100644 --- a/apps/control-station/test/observationSources.test.mjs +++ b/apps/control-station/test/observationSources.test.mjs @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; import { after, before, test } from "node:test"; import { createElement } from "react"; @@ -184,6 +185,24 @@ test("floating observation interaction captures resize and movable header pointe ); }); +test("live camera registers sourceopen before assigning the MSE object URL", async () => { + const source = await readFile( + new URL("../src/components/MseFmp4WebSocketPlayer.tsx", import.meta.url), + "utf8", + ); + const listenerIndex = source.indexOf( + 'mediaSource.addEventListener("sourceopen", onSourceOpen, { once: true });', + ); + const sourceAssignmentIndex = source.indexOf("video.src = objectUrl;"); + + assert.ok(listenerIndex >= 0, "sourceopen listener must remain explicit"); + assert.ok(sourceAssignmentIndex >= 0, "MediaSource object URL assignment must remain explicit"); + assert.ok( + listenerIndex < sourceAssignmentIndex, + "sourceopen listener must exist before video.src can open the MediaSource", + ); +}); + test("recorded observation timeline clamps relative seek time without epoch precision in the UI", () => { const range = normalizeTimelineRange({ min: 0,