fix(video): keep source frame timing when transcoding to HLS

Variable-frame-rate sources (phone clips that change capture rate,
screen recordings) report r_frame_rate as their 1/90000 timebase. The
HLS transcode used the mpegts muxer's default CFR mode, so ffmpeg
duplicated frames to fill 90000 fps: on prod, three 2.6-5 s phone clips
became 3-second "segments" of up to 446 MB / 250k frames. Players
download the whole thing before starting and can't seek within it.

-fps_mode vfr passes source timestamps through. Verified on
20220208_164356_01.mp4: 233 frames over 2.578 s in, 233 frames over
2.578 s out, 5.9 MB vs 221 MB. The copy path already keeps timestamps
and is unchanged. Requires ffmpeg >= 5.1 (prod runs 8.0.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-10-13 10:58:13 -04:00
1 parent 052609d604
commit 83b1923e25
1 file changed
+6
+6
View File
@@ -668,6 +668,12 @@ impl Handler<GeneratePlaylistMessage> for PlaylistGenerator {
// segment starts on a keyframe — accurate seeking without // segment starts on a keyframe — accurate seeking without
// players having to decode from a prior segment. // players having to decode from a prior segment.
cmd.arg("-force_key_frames").arg("expr:gte(t,n_forced*3)"); cmd.arg("-force_key_frames").arg("expr:gte(t,n_forced*3)");
// Keep source frame timestamps. Variable-frame-rate phone
// clips and screen recordings report r_frame_rate as their
// timebase (90000/1); the mpegts muxer's default CFR mode
// then duplicates frames to fill it — a 2.6 s clip came out
// as 233k frames / 221 MB. vfr leaves timing untouched.
cmd.arg("-fps_mode").arg("vfr");
} }
// -f hls is required because the playlist is written to a .tmp // -f hls is required because the playlist is written to a .tmp