Videos from the mirrorless cameras are slow to start and seek. HLS generation stream-copies any h264 source with no rotation and a GOP within one segment. That keeps the source's resolution and bitrate, so 4K footage at 100–160 Mbps is served as ~50 MB 3-second segments.
Bitrate across the ~1,045 generated playlists on prod:
Bitrate
Playlists
< 10 Mbps
648
10–25 Mbps
340
25–50 Mbps
41
50–100 Mbps
9
100–300 Mbps
4
> 300 Mbps
3 (broken source timestamps, separate issue)
Changes
probe_video_stream_meta also reads width, height and bit_rate: the stream-level rate, falling back to format level. These are header fields, so the probe still doesn't read the whole file.
Stream copy also requires short side ≤ 1080 and bitrate ≤ HLS_COPY_MAX_BITRATE (default 20 Mbps). If the size or bitrate is unknown, the video is transcoded, the same fallback the GOP probe uses.
The transcode scale filter caps the short side at 1080 instead of the width. Before, landscape sources came out at 1080×608.
There's a new log line and span event when a video is transcoded for being too large to copy.
Testing
New unit tests for fits_hls_copy_limits: phone-size sources are copied; 4K, high-bitrate 1080p, and unknown size/bitrate are transcoded. cargo test --lib video::actors passes, and clippy shows no new warnings.
Scale filter checked with ffmpeg: 3840×2160 → 1920×1080, 2160×3840 → 1080×1920, 1440×1080 and 1280×720 unchanged.
Ran the new ffprobe query on a prod 4K segment: it returns 3840×2160 at 156 Mbps (format-level fallback), so that video would now be transcoded.
After deploy
Existing oversized playlists are not regenerated automatically. Their hash dirs under VIDEO_PATH need deleting so the queue rebuilds them.
## Problem
Videos from the mirrorless cameras are slow to start and seek. HLS generation stream-copies any h264 source with no rotation and a GOP within one segment. That keeps the source's resolution and bitrate, so 4K footage at 100–160 Mbps is served as ~50 MB 3-second segments.
Bitrate across the ~1,045 generated playlists on prod:
| Bitrate | Playlists |
|---|---|
| < 10 Mbps | 648 |
| 10–25 Mbps | 340 |
| 25–50 Mbps | 41 |
| 50–100 Mbps | 9 |
| 100–300 Mbps | 4 |
| > 300 Mbps | 3 (broken source timestamps, separate issue) |
## Changes
- `probe_video_stream_meta` also reads `width`, `height` and `bit_rate`: the stream-level rate, falling back to format level. These are header fields, so the probe still doesn't read the whole file.
- Stream copy also requires short side ≤ 1080 and bitrate ≤ `HLS_COPY_MAX_BITRATE` (default 20 Mbps). If the size or bitrate is unknown, the video is transcoded, the same fallback the GOP probe uses.
- The transcode scale filter caps the **short side** at 1080 instead of the width. Before, landscape sources came out at 1080×608.
- There's a new log line and span event when a video is transcoded for being too large to copy.
## Testing
- New unit tests for `fits_hls_copy_limits`: phone-size sources are copied; 4K, high-bitrate 1080p, and unknown size/bitrate are transcoded. `cargo test --lib video::actors` passes, and clippy shows no new warnings.
- Scale filter checked with ffmpeg: 3840×2160 → 1920×1080, 2160×3840 → 1080×1920, 1440×1080 and 1280×720 unchanged.
- Ran the new ffprobe query on a prod 4K segment: it returns 3840×2160 at 156 Mbps (format-level fallback), so that video would now be transcoded.
## After deploy
Existing oversized playlists are not regenerated automatically. Their hash dirs under `VIDEO_PATH` need deleting so the queue rebuilds them.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
HLS generation stream-copied any h264 source with no rotation and a
GOP within one segment, which keeps the source's resolution and
bitrate. Mirrorless footage (4K at 100-160 Mbps) came through as-is,
giving ~50 MB 3-second segments; playback start and seeks stalled on
anything slower than wired gigabit. On prod, 16 playlists run above
50 Mbps, versus ~10 Mbps typical for phone clips.
The stream probe now also reads width, height and bit_rate (stream
level, falling back to format level; all header fields, so the probe
stays header-only). Copy additionally requires the short side to be
<= 1080 and the bitrate <= HLS_COPY_MAX_BITRATE (default 20 Mbps).
Unknown size or bitrate transcodes, matching the GOP probe's fallback.
The transcode scale filter capped width at 1080, so landscape sources
came out at 1080x608. It now caps the short side instead: 3840x2160
-> 1920x1080, 2160x3840 -> 1080x1920, smaller sources unchanged
(verified with ffmpeg).
Existing oversized playlists need regenerating after deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
docs:HLS_COPY_MAX_BITRATE is now documented in README, .env.example and CLAUDE.md.
fix(video): keep source frame timing when transcoding to HLS. The 3 playlists over 300 Mbps were transcodes of short variable-frame-rate phone clips. ffprobe reports their r_frame_rate as 90000/1, and the default CFR mode duplicated frames to reach that rate. The transcode now uses -fps_mode vfr. Checked on 20220208_164356_01.mp4: before, 232,998 frames / 221 MB; now, 233 frames / 5.9 MB with identical timing (2.578 s). This probably also fixes the slow/fast-motion playback seen with screen recordings and phone clips recorded at varying frame rates.
Sources of the 16 oversized playlists, checked with ffprobe: all are H.264 above 20 Mbps (48–130 Mbps, from 1008×2244 up to 3840×2160), so after this PR every one is transcoded. Delete their hash dirs after deploy to regenerate them.
Added two commits:
- **docs:** `HLS_COPY_MAX_BITRATE` is now documented in README, `.env.example` and CLAUDE.md.
- **fix(video): keep source frame timing when transcoding to HLS.** The 3 playlists over 300 Mbps were transcodes of short variable-frame-rate phone clips. ffprobe reports their `r_frame_rate` as `90000/1`, and the default CFR mode duplicated frames to reach that rate. The transcode now uses `-fps_mode vfr`. Checked on `20220208_164356_01.mp4`: before, 232,998 frames / 221 MB; now, 233 frames / 5.9 MB with identical timing (2.578 s). This probably also fixes the slow/fast-motion playback seen with screen recordings and phone clips recorded at varying frame rates.
Sources of the 16 oversized playlists, checked with ffprobe: all are H.264 above 20 Mbps (48–130 Mbps, from 1008×2244 up to 3840×2160), so after this PR every one is transcoded. Delete their hash dirs after deploy to regenerate them.
Added fix(video): make NVENC honour forced HLS keyframes.
I found this while checking that -fps_mode vfr is safe for constant-frame-rate video. It is: on three CFR files (29.97, 23.976 and 50 fps), frame counts, duration and frame spacing were identical to the default mode.
The test also showed that NVENC transcodes ignore the 3-second keyframe interval. Without -forced-idr 1, NVENC inserts the forced keyframes as plain I-frames, and the HLS muxer can only split on IDR frames. Segments end up at NVENC's default 250-frame interval (8.34 s at 29.97 fps). With the flag they come out at 3.003 s, the same as the libx264 path. About 480 prod playlists have a target duration above 4 s.
Note: after deploy, those ~480 playlists only pick up 3 s segments if they're regenerated as well.
Added `fix(video): make NVENC honour forced HLS keyframes`.
I found this while checking that `-fps_mode vfr` is safe for constant-frame-rate video. It is: on three CFR files (29.97, 23.976 and 50 fps), frame counts, duration and frame spacing were identical to the default mode.
The test also showed that NVENC transcodes ignore the 3-second keyframe interval. Without `-forced-idr 1`, NVENC inserts the forced keyframes as plain I-frames, and the HLS muxer can only split on IDR frames. Segments end up at NVENC's default 250-frame interval (8.34 s at 29.97 fps). With the flag they come out at 3.003 s, the same as the libx264 path. About 480 prod playlists have a target duration above 4 s.
Note: after deploy, those ~480 playlists only pick up 3 s segments if they're regenerated as well.
-force_key_frames asks for a keyframe every 3 s, but h264_nvenc inserts
those as plain I-frames unless -forced-idr is set. The HLS muxer only
splits on IDR frames, so NVENC transcodes fell back to the encoder's
default 250-frame GOP: 8.34 s segments at 29.97 fps, ~10 s at 25 fps.
On prod ~480 playlists have a target duration above 4 s. Long segments
mean more data before playback starts and coarser seeks.
Verified on a 10 s 29.97 fps clip: segments went from 8.34 + 1.67 s to
3.003 x 3 + 1.001 s, matching the libx264 path, which was already
correct.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_generate_playlist was a private, never-called predecessor of the HLS
path in video/actors.rs. Its leftover args (-hls_list_size 100, width
1080 scale, libx264 only) no longer described how playlists are built.
Removing it also drops the now-unused std::process::Output import.
The reels encoder comment said its NVENC settings matched the HLS
transcode; reels use preset p4, HLS uses p1. Only the quality target
(cq 23) is shared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs said tests need BASE_PATH in .env. The full suite (lib, bin and
integration tests) passes with no .env and no BASE_PATH; the only thing
it needs is DATABASE_URL, and ":memory:" works. Without it, DB-backed
tests such as reels::tests::reel_capture_preserves_owner_defaults panic
with "DATABASE_URL must be set".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cameron
merged commit 78e36d8a6c into master2026-10-13 15:09:38 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Videos from the mirrorless cameras are slow to start and seek. HLS generation stream-copies any h264 source with no rotation and a GOP within one segment. That keeps the source's resolution and bitrate, so 4K footage at 100–160 Mbps is served as ~50 MB 3-second segments.
Bitrate across the ~1,045 generated playlists on prod:
Changes
probe_video_stream_metaalso readswidth,heightandbit_rate: the stream-level rate, falling back to format level. These are header fields, so the probe still doesn't read the whole file.HLS_COPY_MAX_BITRATE(default 20 Mbps). If the size or bitrate is unknown, the video is transcoded, the same fallback the GOP probe uses.Testing
fits_hls_copy_limits: phone-size sources are copied; 4K, high-bitrate 1080p, and unknown size/bitrate are transcoded.cargo test --lib video::actorspasses, and clippy shows no new warnings.After deploy
Existing oversized playlists are not regenerated automatically. Their hash dirs under
VIDEO_PATHneed deleting so the queue rebuilds them.🤖 Generated with Claude Code
Added two commits:
HLS_COPY_MAX_BITRATEis now documented in README,.env.exampleand CLAUDE.md.r_frame_rateas90000/1, and the default CFR mode duplicated frames to reach that rate. The transcode now uses-fps_mode vfr. Checked on20220208_164356_01.mp4: before, 232,998 frames / 221 MB; now, 233 frames / 5.9 MB with identical timing (2.578 s). This probably also fixes the slow/fast-motion playback seen with screen recordings and phone clips recorded at varying frame rates.Sources of the 16 oversized playlists, checked with ffprobe: all are H.264 above 20 Mbps (48–130 Mbps, from 1008×2244 up to 3840×2160), so after this PR every one is transcoded. Delete their hash dirs after deploy to regenerate them.
Added
fix(video): make NVENC honour forced HLS keyframes.I found this while checking that
-fps_mode vfris safe for constant-frame-rate video. It is: on three CFR files (29.97, 23.976 and 50 fps), frame counts, duration and frame spacing were identical to the default mode.The test also showed that NVENC transcodes ignore the 3-second keyframe interval. Without
-forced-idr 1, NVENC inserts the forced keyframes as plain I-frames, and the HLS muxer can only split on IDR frames. Segments end up at NVENC's default 250-frame interval (8.34 s at 29.97 fps). With the flag they come out at 3.003 s, the same as the libx264 path. About 480 prod playlists have a target duration above 4 s.Note: after deploy, those ~480 playlists only pick up 3 s segments if they're regenerated as well.