Oracle pitfalls addressed (all 6 from comment #348)
AV_PICTURE_TYPE_I needs forced-idr=1 — added av_opt_set(priv_data, "forced-idr", "1") in create_software_h264_encoder before avcodec_open2. It's an FFmpeg-level option (not x264-native), so it goes via av_opt_set, NOT in the x264opts string.
key_frame = 1 is output-only — not used. We set pict_type (input-side) instead.
Resolution change implies IDR — recreate_encoder now sets force_keyframe_pending = true at the end. Belt-and-suspenders (fresh encoder's first frame is IDR by default).
Reused AVFrame leaks pict_type — pict_type reset to AV_PICTURE_TYPE_NONE on every non-forced frame via else branch. This is the critical trap: without the reset, a previously-forced I-type would leak into subsequent P-frames.
Pending survives compositor stalls — force_keyframe_pending is only cleared after avcodec_send_frame succeeds. If the encoder is paused (viewer not yet connected), the flag persists across frames until the pause lifts.
repeat_headers=1 already present — not duplicated. SPS/PPS inline on IDR already works.
Key design decisions
Reused bitrate_tx channel (Oracle-approved) — no new channel, no AtomicBool. The encode thread owns SwEncEncode exclusively; bitrate_rx is the synchronization mechanism.
Separate flag from need_keyframe — need_keyframe gates write_h264's skip-non-IDR behavior (cleared when IDR observed). force_keyframe_to_encode is drained by take_force_keyframe() (cleared when forwarded to encode thread). Different lifecycles, different consumers.
try_send drops if full — bounded(4) channel, encode thread drains all pending per frame. Multiple ForceKeyframe commands are idempotent (just set bool=true repeatedly).
Files changed
src/avhw.rs — BitrateCommand::ForceKeyframe variant; SwEncEncode.force_keyframe_pending field; forced-idr=1 in encoder init; drain/dedup-bypass/pict_type/clear logic in encode_cpu_frame; force_keyframe_pending = true in recreate_encoder
src/webrtc.rs — WebRtcInner.force_keyframe_to_encode field; take_force_keyframe() drain method; set flag in Event::KeyframeRequest, Event::Connected, handle_sdp_offer, set_need_keyframe
src/state_portal.rs — webrtc_thread_loop: drain take_force_keyframe() after poll_and_feed(), push ForceKeyframe via bitrate_tx.try_send
rust-analyzer not installed in toolchain (pre-existing env issue); cargo build clean is authoritative
Expected runtime impact
The 2.1s keyframe delay (152 skipped non-IDR frames at ~70ms wall-clock each) should drop to ~1 frame (~33ms at 30fps). The force_this_frame bypass means the FIRST frame after a KeyframeRequest is guaranteed to be encoded (not deduped) AND marked as IDR.
Remaining: needs a live capture session on Wayland+VAAPI to confirm the skipped-non-IDR count drops from 152 to 0. Hardware test deferred (not CI-gatable).
## Implemented: Immediate IDR on KeyframeRequest
**Changes:** 3 files, ~40 lines. `cargo build` clean (0 errors), `cargo test` 34/34 passed (transform 26, backend_detect 3, fps_limit 5).
### Signal flow
```
str0m Event::KeyframeRequest
→ WebRtcInner.force_keyframe_to_encode = true (webrtc.rs)
→ webrtc_thread_loop: wrtc.take_force_keyframe()
→ bitrate_tx.try_send(ForceKeyframe) (state_portal.rs)
→ encode_cpu_frame: drain bitrate_rx
→ SwEncEncode.force_keyframe_pending = true (avhw.rs)
→ force_this_frame captured AFTER drain
→ bypass dedup hash check
→ set AV_PICTURE_TYPE_I on yuv_frame
→ avcodec_send_frame → libx264 emits IDR (forced-idr=1)
→ write_h264 sees IDR → clears need_keyframe
```
### Oracle pitfalls addressed (all 6 from comment #348)
1. **`AV_PICTURE_TYPE_I` needs `forced-idr=1`** — added `av_opt_set(priv_data, "forced-idr", "1")` in `create_software_h264_encoder` before `avcodec_open2`. It's an FFmpeg-level option (not x264-native), so it goes via `av_opt_set`, NOT in the `x264opts` string.
2. **`key_frame = 1` is output-only** — not used. We set `pict_type` (input-side) instead.
3. **Resolution change implies IDR** — `recreate_encoder` now sets `force_keyframe_pending = true` at the end. Belt-and-suspenders (fresh encoder's first frame is IDR by default).
4. **Reused AVFrame leaks pict_type** — `pict_type` reset to `AV_PICTURE_TYPE_NONE` on every non-forced frame via `else` branch. This is the critical trap: without the reset, a previously-forced I-type would leak into subsequent P-frames.
5. **Pending survives compositor stalls** — `force_keyframe_pending` is only cleared after `avcodec_send_frame` succeeds. If the encoder is paused (viewer not yet connected), the flag persists across frames until the pause lifts.
6. **`repeat_headers=1` already present** — not duplicated. SPS/PPS inline on IDR already works.
### Key design decisions
- **Reused `bitrate_tx` channel** (Oracle-approved) — no new channel, no `AtomicBool`. The encode thread owns `SwEncEncode` exclusively; `bitrate_rx` is the synchronization mechanism.
- **Separate flag from `need_keyframe`** — `need_keyframe` gates `write_h264`'s skip-non-IDR behavior (cleared when IDR observed). `force_keyframe_to_encode` is drained by `take_force_keyframe()` (cleared when forwarded to encode thread). Different lifecycles, different consumers.
- **`try_send` drops if full** — bounded(4) channel, encode thread drains all pending per frame. Multiple `ForceKeyframe` commands are idempotent (just set bool=true repeatedly).
### Files changed
- `src/avhw.rs` — `BitrateCommand::ForceKeyframe` variant; `SwEncEncode.force_keyframe_pending` field; `forced-idr=1` in encoder init; drain/dedup-bypass/pict_type/clear logic in `encode_cpu_frame`; `force_keyframe_pending = true` in `recreate_encoder`
- `src/webrtc.rs` — `WebRtcInner.force_keyframe_to_encode` field; `take_force_keyframe()` drain method; set flag in `Event::KeyframeRequest`, `Event::Connected`, `handle_sdp_offer`, `set_need_keyframe`
- `src/state_portal.rs` — `webrtc_thread_loop`: drain `take_force_keyframe()` after `poll_and_feed()`, push `ForceKeyframe` via `bitrate_tx.try_send`
### Verification
- `cargo build` — 0 errors, 0 new warnings (pre-existing transform.rs dead-code warnings unchanged)
- `cargo test transform` — 26 passed
- `cargo test backend_detect` — 3 passed
- `cargo test fps_limit` — 5 passed
- rust-analyzer not installed in toolchain (pre-existing env issue); `cargo build` clean is authoritative
### Expected runtime impact
The 2.1s keyframe delay (152 skipped non-IDR frames at ~70ms wall-clock each) should drop to ~1 frame (~33ms at 30fps). The `force_this_frame` bypass means the FIRST frame after a `KeyframeRequest` is guaranteed to be encoded (not deduped) AND marked as IDR.
**Remaining:** needs a live capture session on Wayland+VAAPI to confirm the skipped-non-IDR count drops from 152 to 0. Hardware test deferred (not CI-gatable).
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.
现象
日志中
14:48:36.585视频端请求关键帧:根因
src/webrtc.rs:614-620只置need_keyframe=true后被动等待编码器自然产生 IDR:代码库中没有任何强制 IDR 机制:
frame->pict_type = AV_PICTURE_TYPE_Iforced-idr选项设置默认 GOP =
fps*2= 120 帧 = 正好对应日志中的 2 秒(src/state_portal.rs:204-210)。影响
WebRTC 中 keyframe 请求意味着"我丢了帧/解码器崩了/新观众加入,需要立即 IDR"。等待 2 秒 = 视频冻结 2 秒,每次丢包恢复都触发,对体验是灾难。
修复方向
在
SwEncEncode(src/avhw.rs)中:force_keyframe()方法,设置内部AtomicBoolencode_cpu_frame()入口检查该标志,若为 true:av_opt_set(codec_ctx, "forced-idr", "1", 0))webrtc.rs传到 encode 线程关联
set_need_keyframe()(src/state_portal.rs:738),同样踩这个坑复现
连接视频端,断网/恢复模拟丢包,或在浏览器 DevTools 触发
RTCRtpSender.getParameters()后 force PLI。观察received keyframe request到got IDR keyframe的间隔。Oracle 审核结论(force-IDR 修复方向)
裁决:approve-with-changes — 方向正确,但必须把 force-IDR 请求交给 encode 线程拥有,且必须在 dedup 之前绕过去。
AVFrame.pict_type = AV_PICTURE_TYPE_I必须配合 libx264 私有选项forced-idr=1,不要依赖key_frame = 1。关键修正
codec 创建时一次性设置
forced-idr=1(在avcodec_open2之前):这样
pict_type = I才能保证产出真正的 IDR,而不是 non-IDR I 帧。force-keyframe 必须在 dedup 之前消费——这是原方案的最大漏洞。
src/avhw.rs:1133的if current_hash == self.last_frame_hash { return Ok(()); }在avcodec_send_frame之前就 short-circuit 了。如果合成器停滞期间客户端发来 keyframe request(见 #15 / #18,60% 时间是 filler),filler 帧的 hash 永远等于上一帧 → 永远跳过 → 永远不出 IDR。self.yuv_frame是复用的:pict_type必须每帧显式 reset 到AV_PICTURE_TYPE_NONE,否则一次 force 之后所有后续帧都会被标成 I。force 标志只有在
avcodec_send_frame成功后才清零——避免 send 失败时丢请求。webrtc.rs的need_keyframe清除时机不变(webrtc.rs:626):仍然在write_h264实际观察到 IDR 离开编码器时清。推荐的通道方案
方案 2:扩展现有
BitrateCommand枚举加ForceKeyframe变体。理由:
bitrate_rx.try_recv()控制平面force_keyframe_pending: bool)编码线程侧代码草图
libx264 + FFmpeg 的坑
AV_PICTURE_TYPE_I请求 intra 帧;让 libx264 真正产 IDR(而非 non-IDR I)需要forced-idr=1私有选项。AVFrame.key_frame是输出/元数据概念,在 input 帧上设置会被忽略——不要用。AV_CODEC_FLAG2_FAST_MANAGEMENT这种东西;AV_CODEC_FLAG2_FAST与此无关。repeat-headers=1(或在打包/信令层提供 parameter sets),否则新连入的 viewer 无法解码首帧 IDR。force_keyframe_pending必须能持续存活到那时。WebRTC 侧改动
need_keyframe仍在write_h264观察 IDR 时清(保持现有语义)。延迟下界分析
从
KeyframeRequest到 IDR 上线的最坏路径:input_rx.recv()在 encode 线程的等待时间(bounded(1) 通道 + capture/filler 节奏)encode_cpu_frame处理时间(~3.7ms p95,见末尾 stats)drain_encoder+webrtc_txchannel sendwrite_h264在 webrtc 线程主导项是
input_rx等待时间。filler 在 ~30fps 节奏下推入 encode 线程,所以下界约 33ms。bounded(1)在常态下不是瓶颈,但在 burst 时会丢请求——ForceKeyframe用try_send失败也无害(合并语义)。期望效果
测试策略
KeyframeRequestevent,断言bitrate_tx收到ForceKeyframewrite_h264skipping non-IDR frame计数 = 0(在触发 keyframe request 的测试场景下)不要做的事
BitrateCommand扩展更干净webrtc.rs提前清need_keyframe——保持观察到 IDR 才清的语义Implemented: Immediate IDR on KeyframeRequest
Changes: 3 files, ~40 lines.
cargo buildclean (0 errors),cargo test34/34 passed (transform 26, backend_detect 3, fps_limit 5).Signal flow
Oracle pitfalls addressed (all 6 from comment #348)
AV_PICTURE_TYPE_Ineedsforced-idr=1— addedav_opt_set(priv_data, "forced-idr", "1")increate_software_h264_encoderbeforeavcodec_open2. It's an FFmpeg-level option (not x264-native), so it goes viaav_opt_set, NOT in thex264optsstring.key_frame = 1is output-only — not used. We setpict_type(input-side) instead.recreate_encodernow setsforce_keyframe_pending = trueat the end. Belt-and-suspenders (fresh encoder's first frame is IDR by default).pict_typereset toAV_PICTURE_TYPE_NONEon every non-forced frame viaelsebranch. This is the critical trap: without the reset, a previously-forced I-type would leak into subsequent P-frames.force_keyframe_pendingis only cleared afteravcodec_send_framesucceeds. If the encoder is paused (viewer not yet connected), the flag persists across frames until the pause lifts.repeat_headers=1already present — not duplicated. SPS/PPS inline on IDR already works.Key design decisions
bitrate_txchannel (Oracle-approved) — no new channel, noAtomicBool. The encode thread ownsSwEncEncodeexclusively;bitrate_rxis the synchronization mechanism.need_keyframe—need_keyframegateswrite_h264's skip-non-IDR behavior (cleared when IDR observed).force_keyframe_to_encodeis drained bytake_force_keyframe()(cleared when forwarded to encode thread). Different lifecycles, different consumers.try_senddrops if full — bounded(4) channel, encode thread drains all pending per frame. MultipleForceKeyframecommands are idempotent (just set bool=true repeatedly).Files changed
src/avhw.rs—BitrateCommand::ForceKeyframevariant;SwEncEncode.force_keyframe_pendingfield;forced-idr=1in encoder init; drain/dedup-bypass/pict_type/clear logic inencode_cpu_frame;force_keyframe_pending = trueinrecreate_encodersrc/webrtc.rs—WebRtcInner.force_keyframe_to_encodefield;take_force_keyframe()drain method; set flag inEvent::KeyframeRequest,Event::Connected,handle_sdp_offer,set_need_keyframesrc/state_portal.rs—webrtc_thread_loop: draintake_force_keyframe()afterpoll_and_feed(), pushForceKeyframeviabitrate_tx.try_sendVerification
cargo build— 0 errors, 0 new warnings (pre-existing transform.rs dead-code warnings unchanged)cargo test transform— 26 passedcargo test backend_detect— 3 passedcargo test fps_limit— 5 passedcargo buildclean is authoritativeExpected runtime impact
The 2.1s keyframe delay (152 skipped non-IDR frames at ~70ms wall-clock each) should drop to ~1 frame (~33ms at 30fps). The
force_this_framebypass means the FIRST frame after aKeyframeRequestis guaranteed to be encoded (not deduped) AND marked as IDR.Remaining: needs a live capture session on Wayland+VAAPI to confirm the skipped-non-IDR count drops from 152 to 0. Hardware test deferred (not CI-gatable).
验证结果:修复完全生效
Release 二进制捕获会话(3372 行日志,5 次 keyframe 请求):
skipping non-IDR frame(丢弃 P 帧)5 次 keyframe 请求延迟分解
结论
skipping non-IDR frame从 152 → 0,彻底消除 P 帧浪费ForceKeyframe → IDR稳定在 4–5ms,编码器即时响应 forced-idr=1次要发现(非 #16 范畴)
部分 keyframe request → ForceKeyframe 仍有 430–611ms 延迟(第 1、3 次),原因是
take_force_keyframe()只在webrtc_thread_loop迭代时调用,循环周期受poll_and_feed()阻塞影响。属于 #19(编码器自旋/WebRTC 线程调度)的范畴,另行处理。