Commit 36c7f00
authored
fix: add post-processing step to fix MP4 duration metadata (#100)
## Summary
Fixes two issues:
1. MP4 files recorded by the recording endpoints don't display the full
video length when downloaded (duration extends as videos are played)
2. Concurrent `Stop()` calls could corrupt recordings due to multiple
SIGINTs being sent to ffmpeg
## Problem
### Duration Metadata
The recording uses **fragmented MP4** format (`-movflags
+frag_keyframe+empty_moov`) for data safety during recording. This
format:
- Writes an empty `moov` atom at the start (without duration
information)
- Streams fragments incrementally as it records
- Is great for crash safety, but causes players to calculate duration on
playback
### Concurrent Stop Race Condition
When multiple goroutines called `Stop()` concurrently (e.g., from
duplicate HTTP requests), each would send SIGINT to ffmpeg. Multiple
SIGINTs cause ffmpeg to terminate immediately without flushing buffers,
resulting in corrupted/empty MP4 files.
## Solution
### Post-Processing (Duration Fix)
Add a **post-processing step** that remuxes the fragmented MP4 into a
standard MP4 with proper duration metadata after recording stops:
- `finalizeRecording()` method - Remuxes using `ffmpeg -c copy -movflags
+faststart`
- No re-encoding (fast operation)
- Moves the `moov` atom to the start with correct duration
- Uses temp file to safely replace the original
### Concurrent-Safe Stop (Race Condition Fix)
Use `sync.Once` to ensure shutdown and finalization only run once:
- `stopOnce` - Ensures shutdown signals are only sent once (prevents
duplicate SIGINTs)
- `finalizeOnce` - Ensures finalization only runs once
- Wait for process exit before proceeding to finalization
### API Changes
- `DownloadRecording`: Returns `202 Accepted` with `Retry-After: 5` if
recording is being finalized
- `DeleteRecording`: Returns `400` if recording is being finalized; now
synchronous instead of async
## Changes
### Recorder (`server/lib/recorder/ffmpeg.go`)
- Add `finalizeRecording()` to remux fragmented MP4 to standard MP4
- Introduce `ErrRecordingFinalizing` and `finalizing` state
- Gate `Recording()`/`Delete()` to return error while finalizing
- Make shutdown/finalization idempotent via `sync.Once` (`stopOnce`,
`finalizeOnce`)
- Wait for process exit before finalizing
- `Stop()` always attempts finalization after graceful shutdown
- `ForceStop()` attempts finalization with warning-only on failure
### API (`server/cmd/api/api/api.go`)
- `DownloadRecording`: Return `202` with `Retry-After: 5` if finalizing
- `DeleteRecording`: Synchronous delete; return `400` if finalizing
### Tests (`server/lib/recorder/ffmeg_test.go`)
- Set explicit `outputPath` and adjust expectations for new finalization
behavior
## Testing
Tested with concurrent stop script:
go run ./scripts/concurrent_stop_test/main.go -url http://localhost:444
-concurrency 5 -iterations 10Results: **10/10 passed** with 5 concurrent
stop calls per iteration.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Adds FFmpeg post-processing to finalize MP4 duration metadata and
singleflight-guards Stop/finalize; API now waits for finalization on
download and returns 409 on delete during finalization.
>
> - **Recorder (FFmpeg)**:
> - Add `finalizeRecording()` remux step (faststart) and finalize
automatically on natural exits; expose `WaitForFinalization`.
> - Introduce `ErrRecordingFinalizing`; gate `Recording()`/`Delete()`
until finalization completes.
> - Use `singleflight` to dedupe concurrent `Stop()` and finalization;
`Stop()` always attempts finalize; `ForceStop()` tries finalize but
warns on failure.
> - Update `waitForCommand()` to trigger finalization;
`FFmpegManager.StopAll()` now calls `Stop()` for all.
> - **API**:
> - `StopRecording`: always invoke `Stop()` even if not recording.
> - `DownloadRecording`: if finalizing, wait and then serve; still
returns `202` when in-progress and tiny file.
> - `DeleteRecording`: make synchronous; return `409 Conflict` when
finalizing; log improvements.
> - **OpenAPI/Client**:
> - Add `409 Conflict` response to `DELETE /recording/delete`;
regenerate `oapi` client/server stubs and embedded swagger.
> - **Tests**:
> - Update recorder tests to set explicit `outputPath` and assert new
finalization/force-stop behaviors.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
d3e043f. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->1 parent 1349e74 commit 36c7f00
File tree
5 files changed
+356
-148
lines changed- server
- cmd/api/api
- lib
- oapi
- recorder
5 files changed
+356
-148
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
132 | | - | |
133 | | - | |
134 | | - | |
135 | 132 | | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
| |||
182 | 182 | | |
183 | 183 | | |
184 | 184 | | |
185 | | - | |
186 | | - | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
187 | 208 | | |
188 | 209 | | |
189 | 210 | | |
190 | 211 | | |
| 212 | + | |
191 | 213 | | |
192 | 214 | | |
193 | 215 | | |
| |||
224 | 246 | | |
225 | 247 | | |
226 | 248 | | |
227 | | - | |
228 | | - | |
229 | | - | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
230 | 255 | | |
231 | | - | |
232 | | - | |
| 256 | + | |
233 | 257 | | |
234 | | - | |
| 258 | + | |
235 | 259 | | |
| 260 | + | |
236 | 261 | | |
237 | 262 | | |
238 | 263 | | |
| |||
0 commit comments