You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -67,7 +67,11 @@ Trim involves six distinct state variables and a carefully ordered two-phase IPC
67
67
When `handleLoadedMetadata` fires on the reloaded video, it checks `resumePositionRef.current` and seeks to that position (clamped to the new duration). If `resumeAfterTrimRef.current` is true, it also resumes playback — so the video restarts from approximately where it was before the trim.
68
68
69
69
### The path-equality guard on media change
70
-
When `media` changes, the effect that resets state checks `pathChanged = media?.path !== lastMediaPathRef.current`. If the path is **the same** (which happens when `onTrimmed` triggers a re-render with the updated clip metadata), `resumeAfterTrimRef` and `resumePositionRef` are preserved so `handleLoadedMetadata` can still consume them.
70
+
When `media` changes, the effect that resets state checks `pathChanged = media?.path !== lastMediaPathRef.current`. If the path is **the same** (which happens when `onTrimmed` → `fetchClips` → `setSelectedClip` returns a fresh object for the same clip), the following states are intentionally **not** reset:
71
+
72
+
-`currentTime` and `duration` — `handleLoadedMetadata` fires exactly once per video load. If it already fired and set the correct trimmed values before `setSelectedClip` triggered the media-change effect, zeroing these would permanently corrupt them (no second `loadedmetadata` event arrives because `src` is unchanged). So they are only reset when `pathChanged = true`.
73
+
-`resumeAfterTrimRef` and `resumePositionRef` — preserved so `handleLoadedMetadata` can restore the seek position.
74
+
-`trimFinalizePath`, `suppressVideoSrc`, `trimPending`, `virtualTrimStart/End` — **must not be reset on same-path re-renders**. If the user starts a second trim quickly, `fetchClips` from the first `handleTrimmed` call can return and trigger a `pathChanged = false` re-render while the second trim's finalize effect is still awaiting the API. Resetting `trimFinalizePath` or `suppressVideoSrc` would change a dep of the finalize effect, running its cleanup (`cancelled = true`), causing the API response to be discarded. The video would then reload without incrementing `videoReloadToken`, serving the browser-cached pre-trim URL. See [[edge-case-trim-finalize-cancelled-by-media-rerender]].
71
75
72
76
---
73
77
@@ -89,4 +93,6 @@ This means the user can still scrub and play the "trimmed" portion while waiting
# Edge Case: Trim Leaves Duration = 0 After Reload (Race Condition)
9
+
10
+
**Trigger:** User trims a clip; `handleLoadedMetadata` fires quickly (local file serves fast) before `onTrimmed` → `setSelectedClip` completes.
11
+
12
+
**Symptom:** After trim, the video is correctly replaced on disk and reloads with the trimmed content, but the UI shows `duration = 0`, the progress bar is stuck at 0/0, and trying to enter trim mode again sets `clipEnd = 0` → Trim button disabled. The clip appears "unchanged."
13
+
14
+
**Root Cause:**`useEffect([media])` unconditionally called `setDuration(0)` and `setCurrentTime(0)` every time `media` changed — including when `onTrimmed` triggers `setSelectedClip` with the same-path clip after a successful trim. `handleLoadedMetadata` fires exactly once per video load. If it already fired and set `duration = trimmedDuration` before the media-change effect ran, the `setDuration(0)` call permanently zeroed the duration (the video src is unchanged, so no second `loadedmetadata` event arrives to correct it).
15
+
16
+
**Fix:**`setCurrentTime(0)` and `setDuration(0)` are now gated inside `if (pathChanged)`, exactly like the resume refs. When `pathChanged = false` (same-path re-render after trim), these values are left as-is — `handleLoadedMetadata` has either already set the correct trimmed value, or will set it when the load completes.
17
+
18
+
**Prevention:** Any state that is set by a one-shot DOM event (`loadedmetadata`, `canplaythrough`, etc.) must NOT be reset unconditionally in a media-change effect that also fires on same-path re-renders. Always gate with `pathChanged`.
**Trigger:** Trim succeeds on the backend (file is renamed, size is smaller) but the UI shows the original duration after reload.
11
+
12
+
**Symptom:** After trim, `handleLoadedMetadata` reports the pre-trim duration (e.g. 60.928s) even though `fetchClips` returns the refreshed clip with a smaller `size_bytes`. The timeline appears unchanged. The media data *is* actually trimmed — if the video plays past the real end it simply stops — but the container header still advertises the old length.
13
+
14
+
**Root Cause:**
15
+
16
+
FFmpeg, when doing a stream-copy (`-c copy`) with input seeking (`-ss` before `-i`), may carry the source container's original `mvhd.duration` value into the output rather than computing it from the actual output samples. This happens because the moov atom is written early (as a placeholder) before all samples are known. The duration field in that placeholder reflects the source file's duration.
17
+
18
+
The symptom is the combination of: correct media data in mdat + wrong `mvhd.duration` / `tkhd.duration` fields in the moov atom. Players that trust the container header (like Chromium's `<video>`) report the wrong duration.
19
+
20
+
**Fix:** Add `-movflags +faststart` to the FFmpeg trim command:
With `+faststart`, FFmpeg writes all the media data first (mdat), then writes the moov atom last — at which point the exact output duration is known from the actual samples — and finally moves moov to the front of the file. Because moov is computed *after* all samples are written, the duration fields are always correct.
27
+
28
+
**Side effect:**`+faststart` slightly increases FFmpeg's working time (requires a second pass over the moov to move it). For clips up to a few GB this is negligible; the rename step that follows takes longer anyway (EPERM retries on Windows).
29
+
30
+
**Detection:** Compare `refreshedClip.size_bytes` (smaller than original) against `videoDuration` from `handleLoadedMetadata`. If size decreased but duration is unchanged, this is the bug.
# Edge Case: Trim 2 Finalize Cancelled by Trim 1's handleTrimmed Re-render
9
+
10
+
**Trigger:** User trims a clip twice in quick succession — specifically, they start trim 2 fast enough that trim 1's `handleTrimmed → fetchClips` HTTP response arrives while trim 2's finalize phase is still in-flight.
11
+
12
+
**Symptom:** The video appears to reload (suppressVideoSrc toggled), but shows the old trimmed content (trim 1 result, not trim 2). The backend succeeds for both trims (both renames complete). The frontend discards the trim 2 result silently.
13
+
14
+
**Root Cause:**
15
+
16
+
The `media-change effect` (`useEffect([media])`) unconditionally called `setTrimFinalizePath(null)` and `setSuppressVideoSrc(false)` — even when `pathChanged = false` (same clip path, new object reference).
17
+
18
+
When trim 1 completes, it calls `onTrimmed → handleTrimmed → fetchClips()`. When `fetchClips()` returns, it calls `setSelectedClip(refreshedClip)`. This causes `media` to change (new object reference, same path), firing the `media-change effect` with `pathChanged = false`.
19
+
20
+
If trim 2's finalize effect is still awaiting the API response at that moment, the media-change effect sets `trimFinalizePath = null` — which is a dep of the finalize effect. The finalize effect cleanup runs, setting `cancelled = true`. When the finalize API response arrives, the `if (cancelled)` guard discards it. `videoReloadToken` never increments.
21
+
22
+
Meanwhile, `suppressVideoSrc` was restored to `false` by the media-change effect (not by the finalize success handler), so the video element reappears. But because the token didn't change, the video src URL is identical to the previous trim's URL. Chromium's HTTP cache may serve the stale response, so the user sees the trim-1 result instead of the trim-2 result.
23
+
24
+
**Fix:** Moved `setTrimFinalizePath(null)`, `setSuppressVideoSrc(false)`, `setTrimPending(false)`, `setVirtualTrimStart(null)`, and `setVirtualTrimEnd(null)` inside the `if (pathChanged)` block in the media-change effect. When the same clip re-renders with a new object reference (`pathChanged = false`), these values are left as-is — any in-progress finalize can complete uninterrupted.
25
+
26
+
**Prevention:** State that is actively used by an in-flight async operation (finalize effect) must not be reset by side-effects that fire for unrelated reasons (same-path media object re-renders). Gate all trim-operation state inside `if (pathChanged)`.
Copy file name to clipboardExpand all lines: docs/wiki/pages/modules/recordingService.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -28,6 +28,7 @@ sources: 0
28
28
-**`trimState` map** — persists in memory per process. If the main process restarts between `trimClip` and `finalizeTrim`, the state is lost and `finalizeTrim` will throw. The `.tmp.mp4` orphan must be cleaned up manually.
29
29
-**Multi-track clip produces individual + mixed audio streams** — the mux step maps `1:a` (all streams from the audio temp), so the output MP4 contains both a mixed track and individual per-track streams.
30
30
-**`avoid_negative_ts make_zero`** — used on all copy-cut operations to prevent playback issues in players that don't handle negative DTS.
31
+
-**`-movflags +faststart` on trim** — required so that FFmpeg computes the moov atom from actual output samples rather than copying the source container's duration. Without it, stream-copy trims can produce an MP4 whose `mvhd.duration` still reflects the pre-trim length even though the media data is correctly shortened. See [[edge-case-trim-ffmpeg-stream-copy-duration]].
31
32
-**5-second scan cache** — `scanRecordings()` and `scanClips()` return stale data for up to 5 seconds after an external change (e.g., a file moved from outside the app). Mutations via the app invalidate immediately.
32
33
-**`activeFFmpeg` map** — tracks all running FFmpeg child processes so `killAllProcesses()` (called on app quit) can clean them up and delete their partial output files.
0 commit comments