Repository navigation
fix(moq-video): encoders keyframe by time, not frame count - #8252
Conversation
Quest-Claim: 94bbb024bbf234e9
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 1 minute. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed 745560a.
No actionable findings. The timestamp-based cut supplements the retained backend GOP, honors can_cut, and re-anchors on output keyframes, including those returned by flush. The VideoToolbox change carries a forced cut across empty output, while MediaCodec already carries an unaccepted input's request. The deterministic tests cover slow, nominal/jittery, fast and backward-moving timestamps, with a real openh264 regression included.
Direction: keeping the existing GOP as a ceiling while using the shared cut path for slower inputs is a contained approach, with the no-cut backend limitation explicitly documented.
Verification limits: GitHub-only static review of the complete PR diff and relevant encoder/backend context. No builds, tests, native codec or hardware runs performed; other backends' GOP restart behavior remains unverified here. Rechecked open/non-draft state, full head and published reviews immediately before this COMMENT.
There was a problem hiding this comment.
ℹ️ Two minor notes: the moq-mux join warning this PR cites can still fire on a drifting slow source, and the moq-video changelog has no entry for a user-visible fix.
Reviewed changes
Reviewed the full diff at 745560a (6 files): the time-based Join, the dropped-cut contract on Backend::encode, the VideoToolbox carry, the doc updates, and the completed quest.
- Time-based join —
Encoder::encodenow forces a cut through the existingcutflag when the frames' timestamps advancekeyframe_interval's worth of time atConfig::framerate, andJoin::observeraises the anchor to every keyframe in the backend's output (including a flush). A slower source keys every join, a faster one keeps the backend's frame count, and a no-cut backend is never asked. - VideoToolbox cut carry — a forced cut whose frame yields no packet is reapplied to the next frame, matching MediaCodec, so a dropped cut costs one frame rather than a whole join. This also restores a caller's
Encoder::cutin that case. - Docs —
Config::join,Config::keyframe_interval,Encoder::cut,Backend::encode, and the moq-video README now describe the timestamp placement and the carry contract. - Quest —
quest/m1/keyframe-by-time.mdand its m1 entry are deleted, and no references to the old path remain in the tree orsh/gh/test-inventory.allow. - Tests — mocked-timestamp slow, nominal-jitter, fast, and jump-back cases over recording and
Countingbackends, plus a real openh264 slow-source case in the merge gate.
ℹ️ The cited moq-mux warning can still fire for a drifting slow source
The encoder now cuts within one source frame of the join, but the catalog still declares effective_join, and moq_mux's interval check warns on a strict measured > declared. A drifting camera whose rate does not divide the join evenly can still produce a keyframe-to-keyframe span a fraction of a frame over the declaration and trip the same one-shot "groups are longer than the declared join interval" warning. This may be the accepted one-frame allowance; worth confirming that leaving the warning in place is intended.
Technical details
# Residual join overrun still trips the declared-interval warning
## Affected sites
- `rs/moq-video/src/encode/producer.rs:416` — the capture pipeline declares `probe_config.effective_join()` as the interval hint (`with_interval`).
- `rs/moq-mux/src/container/producer.rs:350` — `member.report(reading.interval)` feeds the longest measured group span into the check.
- `rs/moq-mux/src/catalog/interval.rs:63` — `interval > declared` has no tolerance for the rounding the time cut accepts.
## Required outcome
- A decision on whether a sub-frame overrun should still warn. If not, either the declared hint or `check` must allow the overshoot the time cut permits.ℹ️ No moq-video changelog entry for a user-visible fix
rs/moq-video/CHANGELOG.md is hand-maintained on main, and recent fixes (e.g. #7878) added ### Fixed bullets under [Unreleased]. This PR changes encoder output (extra IDRs on slow sources, and a VideoToolbox cut that is no longer lost), so it warrants an entry.
Technical details
# Missing Unreleased changelog entry
## Affected sites
- `rs/moq-video/CHANGELOG.md:10` — `[Unreleased] / ### Fixed` has no bullet for this PR.
- `rs/moq-video/README.md:118` — already documents the behavior, so the change is user-visible.
## Required outcome
- One `### Fixed` bullet under `[Unreleased]` in `rs/moq-video/CHANGELOG.md`, e.g. that a video encoder keys by timestamps so a source slower than `Config::framerate` still opens a group every `Config::join`.(Written by DeepSeek V4.1 Flash 1017)
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

Summary
A video encoder's groups now stay within
encode::Config::joinwhen the source runs slower thanConfig::framerate, such as a camera dropping to 20 fps in low light. Before, every backend keyed everyjoin's worth of frames at the configured rate, so a 2 s join at 30 fps stretched to 3 s on a 20 fps source, and moq-mux warned that the track's groups were longer than the declared interval (HLS lists them as overruns). Found by moq-dev/studio#70.Encoder::encodenow forces a keyframe, through the samecutflagEncoder::cutand capture'sCutsalready use, on the first frame whose distance from the last keyframe, counted in frames at the configured rate and rounded to the nearest, reachesConfig::keyframe_interval():Encoder::cut'sCutUnsupporteddocs now say.Encoder::cutlost the same way. openh264 needs no carry: under a starved 2 kbps rate it skipped 269 of 300 frames, but never one with a forced IDR. NVENC and VA-API read each frame back synchronously.Docs updated:
Config::join,Config::keyframe_interval,Encoder::cut,Backend::encode(which now requires a dropped frame's cut to carry over), and the moq-video README.Choices made without asking
AVEncMPVGOPSize, MediaCodec's negativei-frame-interval) on platforms this PR cannot test; and a ceiling at some multiple of the interval, an arbitrary factor that buys nothing over the count.kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration. The encoder's time cut already covers VideoToolbox through the forced-keyframe path; a second, time-based placer inside the codec would only add a rounding rule to reconcile.join". The quest's plan said the latter, but it keys one frame late on any jittery camera at the nominal rate (a frame landing 1 ms early pushes the cut to the next one), so every group would overrun the declared interval. Rounding at the configured rate reproduces the frame count exactly at that rate and still bounds a slower source to one frame of overshoot.cut.Tests
Mocked timestamps, no sleeps, all in
rs/moq-video/src/encode/encoder.rs:a_slow_source_keys_by_time: a source drifting through 30, 20, 12.5, and 24 fps never holds a frame a join past its group's keyframe and never cuts more than half a configured frame early. A no-cut backend is never asked.a_jittery_source_at_the_configured_rate_keys_on_the_count: keys exactly on frames 30, 60, 90.a_fast_source_keys_on_the_backend_count: 60 fps on a 30 fps config with a backend keying on its own count; the encoder forces none.a_jump_back_restarts_the_join.openh264_keys_a_slow_source_by_time: the real codec keys a 20 fps source every 20 frames with no stray IDR on its 30-frame count, which shows openh264 restarts its GOP at a forced keyframe. Runs in the merge gate.Each new test fails with the time cut disabled or with the output-keyframe re-anchor removed.
just check origin/mainpasses. The VideoToolbox carry is not compiled or run here (Linux host without an Apple target); macOS CI covers it.Impact
Config::keyframe_intervalstays crate-private; no signatures change.Config::framerate. The declaredeffective_joinis unchanged.Caused by
No single PR: frame-count GOPs are as old as the encoder backends.
Suggested follow-ups
Models
Size: M
Implementer: Claude Code, Claude Opus 5.5, high effort
Reviewer: Codex, gpt-6.1-sol, xhigh effort
🤖 Generated with Claude Code
(Written by Claude Opus 5.5 with high)
Review
Rewritten by
quest reviewafter each round.01a1b9da-cf3f-72c1-a164-c54097c70502, round 2The incremental changes fix the dropped-cut issue with no new findings; builds and tests were not run as instructed.rs/moq-video/src/encode/encoder.rs:625:Retain timed cuts when their frame is dropped;VideoToolbox retains cuts after empty output at rs/moq-video/src/encode/backend/videotoolbox.rs:274 and reapplies them at line 241 until a frame is emitted, whose confirmed keyframe updates the join anchor through Join::observe.