From 771e28eddc5808ee00668d21a0fe2ec20c1d4daf Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 10 Jun 2026 23:30:39 +0200 Subject: [PATCH 01/11] chore: quick marks --- .../modals/TestDataPreviewModal.tsx | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/frontend/src/ts/components/modals/TestDataPreviewModal.tsx b/frontend/src/ts/components/modals/TestDataPreviewModal.tsx index 74819d38e9d4..548e4d940a8f 100644 --- a/frontend/src/ts/components/modals/TestDataPreviewModal.tsx +++ b/frontend/src/ts/components/modals/TestDataPreviewModal.tsx @@ -738,6 +738,14 @@ function PreviewContent(props: { setEventToMark(next); }; + const createMarkAndAssignToEvent = (eventIndex: number): void => { + if (!isSynced()) return; + const videoMs = testMsToVideoMs(currentMs()); + const id = generateMarkId(); + setMarks([...marks(), { id, videoMs }]); + assignMarkToEvent(eventIndex, id); + }; + const [videoPlayState, setVideoPlayState] = createSignal(false); const [videoCurrentMs, setVideoCurrentMs] = createSignal(0); const [videoFrameTimeMs, setVideoFrameTimeMs] = createSignal( @@ -972,9 +980,18 @@ function PreviewContent(props: { /> - 0}> + eventIndexForMark(m.id) === undefined) + .length > 0 + } + >
- + eventIndexForMark(m.id) === undefined, + )} + > {(mark) => { const assignedIdx = (): number | undefined => eventIndexForMark(mark.id); @@ -1177,17 +1194,26 @@ function PreviewContent(props: { From 281648fa4b6fdc88016b4ed55f72b1492bda3fe8 Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 10 Jun 2026 23:57:36 +0200 Subject: [PATCH 02/11] chore: refactor playback controls and improve video synchronization handling --- .../modals/TestDataPreviewModal.tsx | 542 ++++++++++-------- 1 file changed, 287 insertions(+), 255 deletions(-) diff --git a/frontend/src/ts/components/modals/TestDataPreviewModal.tsx b/frontend/src/ts/components/modals/TestDataPreviewModal.tsx index 548e4d940a8f..9046446e489a 100644 --- a/frontend/src/ts/components/modals/TestDataPreviewModal.tsx +++ b/frontend/src/ts/components/modals/TestDataPreviewModal.tsx @@ -544,30 +544,22 @@ function PreviewContent(props: { scrollRowIntoView(eventsScrollEl, currentEventIndex()); }); - const [playing, setPlaying] = createSignal(false); + const [timelinePlaying, setTimelinePlaying] = createSignal(false); let rafId: number | undefined; let lastFrame: number | undefined; - const stopPlay = (): void => { + const playing = createMemo(() => + isSynced() ? videoPlayState() : timelinePlaying(), + ); + + const stopTimelinePlay = (): void => { if (rafId !== undefined) cancelAnimationFrame(rafId); rafId = undefined; lastFrame = undefined; - setPlaying(false); + setTimelinePlaying(false); }; const tick = (now: number): void => { - const el = videoEl(); - if (el !== undefined && isSynced() && Number.isFinite(el.currentTime)) { - const next = videoMsToTestMs(el.currentTime * 1000); - if (next >= timelineMaxMs()) { - setCurrentMs(timelineMaxMs()); - stopPlay(); - return; - } - setCurrentMs(next); - rafId = requestAnimationFrame(tick); - return; - } if (lastFrame === undefined) { lastFrame = now; rafId = requestAnimationFrame(tick); @@ -578,7 +570,7 @@ function PreviewContent(props: { const next = currentMs() + dt; if (next >= timelineMaxMs()) { setCurrentMs(timelineMaxMs()); - stopPlay(); + stopTimelinePlay(); return; } setCurrentMs(Math.round(next)); @@ -586,20 +578,37 @@ function PreviewContent(props: { }; const togglePlay = (): void => { - if (playing()) { - stopPlay(); + if (isSynced()) { + const el = videoEl(); + if (el === undefined) return; + if (el.paused) { + if (currentMs() >= timelineMaxMs()) setCurrentMs(timelineMinMs()); + void el.play().catch(() => undefined); + } else { + el.pause(); + } + return; + } + if (timelinePlaying()) { + stopTimelinePlay(); } else { if (currentMs() >= timelineMaxMs()) setCurrentMs(timelineMinMs()); - setPlaying(true); + setTimelinePlaying(true); lastFrame = undefined; rafId = requestAnimationFrame(tick); } }; - onCleanup(stopPlay); + onCleanup(stopTimelinePlay); + + const stopAllPlay = (): void => { + stopTimelinePlay(); + const el = videoEl(); + if (el !== undefined && isSynced() && !el.paused) el.pause(); + }; const step = (delta: number): void => { - stopPlay(); + stopAllPlay(); const next = Math.max( timelineMinMs(), Math.min(timelineMaxMs(), currentMs() + delta), @@ -608,17 +617,17 @@ function PreviewContent(props: { }; const goToStart = (): void => { - stopPlay(); + stopAllPlay(); setCurrentMs(timelineMinMs()); }; const goToEnd = (): void => { - stopPlay(); + stopAllPlay(); setCurrentMs(timelineMaxMs()); }; const goNextEvent = (): void => { - stopPlay(); + stopAllPlay(); const events = filteredEvents(); let bestMs: number | null = null; for (const e of events) { @@ -630,7 +639,7 @@ function PreviewContent(props: { }; const goPrevEvent = (): void => { - stopPlay(); + stopAllPlay(); const events = filteredEvents(); let bestMs: number | null = null; for (const e of events) { @@ -666,6 +675,7 @@ function PreviewContent(props: { const el = videoEl(); if (el === undefined) return; if (!isSynced()) return; + if (videoPlayState()) return; const t = testMsToVideoMs(currentMs()) / 1000; if (!Number.isFinite(t) || t < 0) return; if (Number.isFinite(el.duration) && t > el.duration) return; @@ -674,32 +684,6 @@ function PreviewContent(props: { } }); - createEffect(() => { - if (!isSynced()) return; - const vps = videoPlayState(); - if (vps !== playing()) { - if (vps) { - if (currentMs() >= timelineMaxMs()) setCurrentMs(timelineMinMs()); - setPlaying(true); - lastFrame = undefined; - rafId = requestAnimationFrame(tick); - } else { - stopPlay(); - } - } - }); - - createEffect(() => { - const el = videoEl(); - if (el === undefined) return; - if (!isSynced()) return; - if (playing()) { - void el.play().catch(() => undefined); - } else { - el.pause(); - } - }); - const addMark = (sync?: SyncKind): void => { const el = videoEl(); if (el === undefined) return; @@ -746,6 +730,21 @@ function PreviewContent(props: { assignMarkToEvent(eventIndex, id); }; + const createSyncMarkAndAssignToEvent = ( + eventIndex: number, + sync: SyncKind, + ): void => { + if (marks().some((m) => m.sync === sync)) return; + const el = videoEl(); + if (el === undefined) return; + const frameMs = videoFrameTimeMs(); + const currentMsFromEl = el.currentTime * 1000; + const videoMs = frameMs ?? currentMsFromEl; + const id = generateMarkId(); + setMarks([...marks(), { id, videoMs, sync }]); + assignMarkToEvent(eventIndex, id); + }; + const [videoPlayState, setVideoPlayState] = createSignal(false); const [videoCurrentMs, setVideoCurrentMs] = createSignal(0); const [videoFrameTimeMs, setVideoFrameTimeMs] = createSignal( @@ -820,24 +819,206 @@ function PreviewContent(props: { }; return ( -
- -
- SYNCED — video locked to timeline -
-
-
+
+ {/* HEADER */} +
-
+ {/* VIDEO VIEWER PANEL */} +
+
+
Viewer
+
+ + +
+
+ + No video loaded +
+ } + > +
+ +
+ seekVideoMs(Number(e.currentTarget.value))} + class="w-full" + /> +
+
+ {currentFrameIndex() !== null + ? `${currentFrameIndex()} (${(videoFrameTimeMs() ?? 0).toFixed(2)}ms @ ${videoFps().toFixed(2)}fps)` + : "—"} +
+
+
+
+ + +
+
Marks
+ +
+ )} + +
+
+ +
+ + {/* TIMELINE PANEL */} +
-
Time
+
Timeline
{currentMs().toFixed(2)} / {maxMs} ms
@@ -915,205 +1096,27 @@ function PreviewContent(props: { videoBar={videoBarRange()} marks={timelineMarks()} /> - {/* setCurrentMs(Number(e.currentTarget.value))} - class="w-full" - /> */}
-
-
-
Video
- - - -
- ); - }} - -
- - - - seekVideoMs(Number(e.currentTarget.value))} - class="w-full" - /> -
-
-
-
- -
-
Simulated input
-
+
{simulatedInput()}
-
-
Words
+ {/* INSPECTOR: words */} +
+
Words
(wordsScrollEl = el)} - class="bg-bg-secondary max-h-64 overflow-auto rounded" + class="max-h-64 overflow-auto rounded bg-bg" > - + @@ -1143,9 +1146,10 @@ function PreviewContent(props: { -
+ {/* INSPECTOR: events */} +
-
+
Events ({filteredEvents().length}/{props.ctx.events.length})
@@ -1164,10 +1168,10 @@ function PreviewContent(props: {
(eventsScrollEl = el)} - class="bg-bg-secondary max-h-96 overflow-auto rounded" + class="max-h-96 overflow-auto rounded bg-bg" >
# target
- + @@ -1200,11 +1204,21 @@ function PreviewContent(props: { assignMarkToEvent(originalIndex, null); } else if (v === "__new__") { createMarkAndAssignToEvent(originalIndex); - e.currentTarget.value = - eventToMark()[originalIndex] ?? ""; + } else if (v === "__new_start__") { + createSyncMarkAndAssignToEvent( + originalIndex, + "start", + ); + } else if (v === "__new_end__") { + createSyncMarkAndAssignToEvent( + originalIndex, + "end", + ); } else { assignMarkToEvent(originalIndex, v); } + e.currentTarget.value = + eventToMark()[originalIndex] ?? ""; }} > @@ -1221,6 +1235,24 @@ function PreviewContent(props: { )} + + + + + +
time type