fix(webrtc): force IDR on KeyframeRequest via forced-idr=1 + pict_type (closes #16)
WebRTC KeyframeRequest 响应延迟 2.1s(152 P 帧丢弃)→ 端到端 5-20ms。 - BitrateCommand::ForceKeyframe variant over existing bitrate_tx channel - SwEncEncode.force_keyframe_pending: bypass dedup, set AV_PICTURE_TYPE_I, clear only after avcodec_send_frame succeeds - libx264 forced-idr=1 via av_opt_set (FFmpeg-level option, not x264-native) - pict_type reset to NONE per frame (reused AVFrame leak guard) - WebRtcInner.force_keyframe_to_encode + take_force_keyframe() drain API - recreate_encoder sets force_keyframe_pending (resolution change → IDR) Verification: skipping non-IDR frame 152→0, ForceKeyframe→IDR 4-5ms stable.
This commit is contained in:
+38
-1
@@ -25,6 +25,9 @@ use crate::transform::{transpose_if_transform_transposed, Transform};
|
|||||||
pub enum BitrateCommand {
|
pub enum BitrateCommand {
|
||||||
UpdateBitrate { target_bps: u64 },
|
UpdateBitrate { target_bps: u64 },
|
||||||
UpdateResolution { width: u32, height: u32 },
|
UpdateResolution { width: u32, height: u32 },
|
||||||
|
/// Force the next encoded frame to be an IDR. Sent by the WebRTC thread
|
||||||
|
/// in response to str0m `Event::KeyframeRequest` or a resolution change.
|
||||||
|
ForceKeyframe,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
@@ -856,6 +859,7 @@ impl SwEncImport {
|
|||||||
requested = Some((width & !1, height & !1));
|
requested = Some((width & !1, height & !1));
|
||||||
}
|
}
|
||||||
BitrateCommand::UpdateBitrate { .. } => {}
|
BitrateCommand::UpdateBitrate { .. } => {}
|
||||||
|
BitrateCommand::ForceKeyframe => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -966,6 +970,10 @@ pub struct SwEncEncode {
|
|||||||
fps: u32,
|
fps: u32,
|
||||||
bitrate: u64,
|
bitrate: u64,
|
||||||
gop_size: u32,
|
gop_size: u32,
|
||||||
|
/// Set true when WebRTC requests a keyframe. Forces the next frame to
|
||||||
|
/// `AV_PICTURE_TYPE_I` and bypasses the dedup hash check. Cleared only
|
||||||
|
/// after `avcodec_send_frame` accepts the forced frame.
|
||||||
|
force_keyframe_pending: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
const FNV1A_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
|
const FNV1A_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
|
||||||
@@ -1028,6 +1036,7 @@ impl SwEncEncode {
|
|||||||
fps,
|
fps,
|
||||||
bitrate,
|
bitrate,
|
||||||
gop_size,
|
gop_size,
|
||||||
|
force_keyframe_pending: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1066,6 +1075,7 @@ impl SwEncEncode {
|
|||||||
fps,
|
fps,
|
||||||
bitrate,
|
bitrate,
|
||||||
gop_size,
|
gop_size,
|
||||||
|
force_keyframe_pending: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1103,9 +1113,15 @@ impl SwEncEncode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
BitrateCommand::UpdateResolution { .. } => {}
|
BitrateCommand::UpdateResolution { .. } => {}
|
||||||
|
BitrateCommand::ForceKeyframe => {
|
||||||
|
self.force_keyframe_pending = true;
|
||||||
|
tracing::debug!("encode thread: ForceKeyframe requested");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let force_this_frame = self.force_keyframe_pending;
|
||||||
|
|
||||||
while let Ok(change) = self.resolution_rx.try_recv() {
|
while let Ok(change) = self.resolution_rx.try_recv() {
|
||||||
self.recreate_encoder(change.width, change.height)?;
|
self.recreate_encoder(change.width, change.height)?;
|
||||||
}
|
}
|
||||||
@@ -1130,7 +1146,7 @@ impl SwEncEncode {
|
|||||||
self.frame_count = self.frame_count.saturating_add(1);
|
self.frame_count = self.frame_count.saturating_add(1);
|
||||||
let current_hash = hash_sampled_y_plane(&frame.y_data, width, height, frame.y_stride);
|
let current_hash = hash_sampled_y_plane(&frame.y_data, width, height, frame.y_stride);
|
||||||
let force_gop_frame = self.gop_size > 0 && frame_index % u64::from(self.gop_size) == 0;
|
let force_gop_frame = self.gop_size > 0 && frame_index % u64::from(self.gop_size) == 0;
|
||||||
if frame_index > 0 && !force_gop_frame && current_hash == self.last_frame_hash {
|
if frame_index > 0 && !force_gop_frame && !force_this_frame && current_hash == self.last_frame_hash {
|
||||||
tracing::debug!(frame_index, "skipping duplicate frame");
|
tracing::debug!(frame_index, "skipping duplicate frame");
|
||||||
self.last_frame_hash = current_hash;
|
self.last_frame_hash = current_hash;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -1172,8 +1188,16 @@ impl SwEncEncode {
|
|||||||
let start_ts = self.starting_timestamp.unwrap_or(0);
|
let start_ts = self.starting_timestamp.unwrap_or(0);
|
||||||
|
|
||||||
// SAFETY: yuv_frame is initialized, writable, and matches the opened encoder format.
|
// SAFETY: yuv_frame is initialized, writable, and matches the opened encoder format.
|
||||||
|
// pict_type is reset every frame: the AVFrame is reused, so without resetting to NONE
|
||||||
|
// a previously-forced I-type would leak into subsequent P-frames. With forced-idr=1
|
||||||
|
// set on the encoder, AV_PICTURE_TYPE_I produces a true IDR NALU.
|
||||||
unsafe {
|
unsafe {
|
||||||
(*self.yuv_frame).pts = pts;
|
(*self.yuv_frame).pts = pts;
|
||||||
|
(*self.yuv_frame).pict_type = if force_this_frame {
|
||||||
|
ffi::AVPictureType::AV_PICTURE_TYPE_I
|
||||||
|
} else {
|
||||||
|
ffi::AVPictureType::AV_PICTURE_TYPE_NONE
|
||||||
|
};
|
||||||
let ret = ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), self.yuv_frame);
|
let ret = ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), self.yuv_frame);
|
||||||
if ret < 0 {
|
if ret < 0 {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -1183,6 +1207,10 @@ impl SwEncEncode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if force_this_frame {
|
||||||
|
self.force_keyframe_pending = false;
|
||||||
|
}
|
||||||
|
|
||||||
self.drain_encoder(start_ts)
|
self.drain_encoder(start_ts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1215,6 +1243,7 @@ impl SwEncEncode {
|
|||||||
self.enc_height = height;
|
self.enc_height = height;
|
||||||
self.last_frame_hash = 0;
|
self.last_frame_hash = 0;
|
||||||
self.frame_count = 0;
|
self.frame_count = 0;
|
||||||
|
self.force_keyframe_pending = true;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1741,6 +1770,14 @@ fn create_software_h264_encoder(
|
|||||||
// avcodec_alloc_context3. Setting level is a simple i32 field
|
// avcodec_alloc_context3. Setting level is a simple i32 field
|
||||||
// assignment on a properly aligned struct.
|
// assignment on a properly aligned struct.
|
||||||
(*enc.as_mut_ptr()).level = 42; // H.264 Level 4.2 (up to 1440p@30)
|
(*enc.as_mut_ptr()).level = 42; // H.264 Level 4.2 (up to 1440p@30)
|
||||||
|
// SAFETY: priv_data belongs to the unopened libx264 encoder context.
|
||||||
|
// `forced-idr` is an FFmpeg-level private option (not x264-native),
|
||||||
|
// so it must be set via av_opt_set, NOT via the x264opts string.
|
||||||
|
// With forced-idr=1, setting AV_PICTURE_TYPE_I on an input frame
|
||||||
|
// produces a true IDR NALU with inline SPS/PPS (repeat_headers=1).
|
||||||
|
let key = CString::new("forced-idr").unwrap();
|
||||||
|
let val = CString::new("1").unwrap();
|
||||||
|
ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0);
|
||||||
let key = CString::new("x264opts").unwrap();
|
let key = CString::new("x264opts").unwrap();
|
||||||
let vbv_maxrate = bitrate;
|
let vbv_maxrate = bitrate;
|
||||||
let vbv_bufsize = bitrate / 4;
|
let vbv_bufsize = bitrate / 4;
|
||||||
|
|||||||
@@ -702,6 +702,10 @@ fn webrtc_thread_loop(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wrtc.take_force_keyframe() {
|
||||||
|
let _ = bitrate_tx.try_send(BitrateCommand::ForceKeyframe);
|
||||||
|
}
|
||||||
|
|
||||||
let connected = wrtc.is_connected();
|
let connected = wrtc.is_connected();
|
||||||
let was_paused = paused.load(Ordering::Relaxed);
|
let was_paused = paused.load(Ordering::Relaxed);
|
||||||
let now_paused = !connected;
|
let now_paused = !connected;
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ struct WebRtcInner {
|
|||||||
video_pt: Option<Pt>,
|
video_pt: Option<Pt>,
|
||||||
connected: bool,
|
connected: bool,
|
||||||
need_keyframe: bool,
|
need_keyframe: bool,
|
||||||
|
force_keyframe_to_encode: bool,
|
||||||
current_bwe_estimate: Option<Bitrate>,
|
current_bwe_estimate: Option<Bitrate>,
|
||||||
rtp_clock: u32,
|
rtp_clock: u32,
|
||||||
buf: Vec<u8>,
|
buf: Vec<u8>,
|
||||||
@@ -353,6 +354,17 @@ impl WebRtcState {
|
|||||||
pub fn set_need_keyframe(&mut self) {
|
pub fn set_need_keyframe(&mut self) {
|
||||||
if let Some(inner) = self.inner.as_mut() {
|
if let Some(inner) = self.inner.as_mut() {
|
||||||
inner.need_keyframe = true;
|
inner.need_keyframe = true;
|
||||||
|
inner.force_keyframe_to_encode = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_force_keyframe(&mut self) -> bool {
|
||||||
|
if let Some(inner) = self.inner.as_mut() {
|
||||||
|
let v = inner.force_keyframe_to_encode;
|
||||||
|
inner.force_keyframe_to_encode = false;
|
||||||
|
v
|
||||||
|
} else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -429,6 +441,7 @@ impl WebRtcInner {
|
|||||||
video_pt: None,
|
video_pt: None,
|
||||||
connected: false,
|
connected: false,
|
||||||
need_keyframe: false,
|
need_keyframe: false,
|
||||||
|
force_keyframe_to_encode: false,
|
||||||
current_bwe_estimate: None,
|
current_bwe_estimate: None,
|
||||||
rtp_clock: 0,
|
rtp_clock: 0,
|
||||||
buf: vec![0u8; 65535],
|
buf: vec![0u8; 65535],
|
||||||
@@ -446,6 +459,7 @@ impl WebRtcInner {
|
|||||||
.map_err(|e| anyhow::anyhow!("accept_offer: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("accept_offer: {e}"))?;
|
||||||
|
|
||||||
self.need_keyframe = true;
|
self.need_keyframe = true;
|
||||||
|
self.force_keyframe_to_encode = true;
|
||||||
tracing::info!("SDP exchange complete, waiting for ICE/DTLS...");
|
tracing::info!("SDP exchange complete, waiting for ICE/DTLS...");
|
||||||
|
|
||||||
self.discover_video_params();
|
self.discover_video_params();
|
||||||
@@ -503,6 +517,7 @@ impl WebRtcInner {
|
|||||||
tracing::info!("WebRTC connected!");
|
tracing::info!("WebRTC connected!");
|
||||||
self.connected = true;
|
self.connected = true;
|
||||||
self.need_keyframe = true;
|
self.need_keyframe = true;
|
||||||
|
self.force_keyframe_to_encode = true;
|
||||||
self.discover_video_params();
|
self.discover_video_params();
|
||||||
}
|
}
|
||||||
Event::IceConnectionStateChange(IceConnectionState::Disconnected) => {
|
Event::IceConnectionStateChange(IceConnectionState::Disconnected) => {
|
||||||
@@ -525,6 +540,7 @@ impl WebRtcInner {
|
|||||||
Event::KeyframeRequest(_) => {
|
Event::KeyframeRequest(_) => {
|
||||||
tracing::info!("received keyframe request from viewer");
|
tracing::info!("received keyframe request from viewer");
|
||||||
self.need_keyframe = true;
|
self.need_keyframe = true;
|
||||||
|
self.force_keyframe_to_encode = true;
|
||||||
}
|
}
|
||||||
Event::EgressBitrateEstimate(est) => {
|
Event::EgressBitrateEstimate(est) => {
|
||||||
let bitrate = match est {
|
let bitrate = match est {
|
||||||
|
|||||||
Reference in New Issue
Block a user