fix(video): resync decoder after packet gaps
This commit is contained in:
@@ -182,6 +182,51 @@ fn should_log_video_sample(frame_no: u64, is_keyframe: bool) -> bool {
|
|||||||
frame_no <= 5 || is_keyframe || frame_no % 30 == 0
|
frame_no <= 5 || is_keyframe || frame_no % 30 == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VIDEO_KEYFRAME_INTERVAL_FRAMES: u32 = 30;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct VideoContinuity {
|
||||||
|
expected_seq: Option<u32>,
|
||||||
|
wait_for_keyframe: bool,
|
||||||
|
gaps: u64,
|
||||||
|
dropped_frames: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoContinuity {
|
||||||
|
fn observe_packet(&mut self, seq: u32) -> Option<u32> {
|
||||||
|
let Some(expected) = self.expected_seq else {
|
||||||
|
self.expected_seq = Some(seq.wrapping_add(1));
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let delta = seq.wrapping_sub(expected);
|
||||||
|
if delta >= 10_000 {
|
||||||
|
// Late/reordered packet from before the current expectation.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.expected_seq = Some(seq.wrapping_add(1));
|
||||||
|
let gap = (delta > 0).then_some(delta);
|
||||||
|
if gap.is_some() {
|
||||||
|
self.gaps += 1;
|
||||||
|
self.wait_for_keyframe = true;
|
||||||
|
}
|
||||||
|
gap
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_decode(&mut self, is_keyframe: bool) -> bool {
|
||||||
|
if is_keyframe {
|
||||||
|
self.wait_for_keyframe = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if self.wait_for_keyframe {
|
||||||
|
self.dropped_frames += 1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_startup_black_i420(data: &[u8], width: u32, height: u32) -> bool {
|
fn is_startup_black_i420(data: &[u8], width: u32, height: u32) -> bool {
|
||||||
let y_size = width as usize * height as usize;
|
let y_size = width as usize * height as usize;
|
||||||
let uv_size = y_size / 4;
|
let uv_size = y_size / 4;
|
||||||
@@ -1294,6 +1339,7 @@ impl CallEngine {
|
|||||||
let mut video_first_decoded_logged = false;
|
let mut video_first_decoded_logged = false;
|
||||||
let mut video_decoded_samples: u64 = 0;
|
let mut video_decoded_samples: u64 = 0;
|
||||||
let mut video_decoder_buffering_count: u64 = 0;
|
let mut video_decoder_buffering_count: u64 = 0;
|
||||||
|
let mut video_continuity = VideoContinuity::default();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if !recv_r.load(Ordering::Relaxed) {
|
if !recv_r.load(Ordering::Relaxed) {
|
||||||
@@ -1310,6 +1356,20 @@ impl CallEngine {
|
|||||||
// a JPEG-encoded frame to the WebView. Done before audio path so
|
// a JPEG-encoded frame to the WebView. Done before audio path so
|
||||||
// we don't drop into the audio decoder branches.
|
// we don't drop into the audio decoder branches.
|
||||||
if pkt.header.media_type == wzp_proto::MediaType::Video {
|
if pkt.header.media_type == wzp_proto::MediaType::Video {
|
||||||
|
if let Some(gap) = video_continuity.observe_packet(pkt.header.seq) {
|
||||||
|
crate::emit_call_debug(
|
||||||
|
&recv_app,
|
||||||
|
"video:seq_gap",
|
||||||
|
serde_json::json!({
|
||||||
|
"t_ms": recv_t0.elapsed().as_millis() as u64,
|
||||||
|
"seq": pkt.header.seq,
|
||||||
|
"gap": gap,
|
||||||
|
"gaps": video_continuity.gaps,
|
||||||
|
"stream_id": pkt.header.stream_id,
|
||||||
|
"platform": "android",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
if !video_first_recv_logged {
|
if !video_first_recv_logged {
|
||||||
video_first_recv_logged = true;
|
video_first_recv_logged = true;
|
||||||
crate::emit_call_debug(
|
crate::emit_call_debug(
|
||||||
@@ -1356,6 +1416,25 @@ impl CallEngine {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if !video_continuity.should_decode(is_kf) {
|
||||||
|
if video_continuity.dropped_frames <= 5
|
||||||
|
|| video_continuity.dropped_frames % 30 == 0
|
||||||
|
{
|
||||||
|
crate::emit_call_debug(
|
||||||
|
&recv_app,
|
||||||
|
"video:drop_until_keyframe",
|
||||||
|
serde_json::json!({
|
||||||
|
"t_ms": recv_t0.elapsed().as_millis() as u64,
|
||||||
|
"codec": format!("{:?}", codec_id),
|
||||||
|
"frame_no": video_reassembled_samples,
|
||||||
|
"dropped_frames": video_continuity.dropped_frames,
|
||||||
|
"platform": "android",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
video_reassembler.evict_stale(pkt.header.timestamp, 5_000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if video_decoder_codec != Some(codec_id) {
|
if video_decoder_codec != Some(codec_id) {
|
||||||
crate::emit_call_debug(
|
crate::emit_call_debug(
|
||||||
&recv_app,
|
&recv_app,
|
||||||
@@ -2054,7 +2133,7 @@ impl CallEngine {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if frames_since_keyframe >= 150 {
|
if frames_since_keyframe >= VIDEO_KEYFRAME_INTERVAL_FRAMES {
|
||||||
encoder.request_keyframe();
|
encoder.request_keyframe();
|
||||||
crate::emit_call_debug(
|
crate::emit_call_debug(
|
||||||
&vid_app,
|
&vid_app,
|
||||||
@@ -2635,6 +2714,7 @@ impl CallEngine {
|
|||||||
let mut video_first_decoded_logged = false;
|
let mut video_first_decoded_logged = false;
|
||||||
let mut video_decoded_samples: u64 = 0;
|
let mut video_decoded_samples: u64 = 0;
|
||||||
let mut video_decoder_buffering_count: u64 = 0;
|
let mut video_decoder_buffering_count: u64 = 0;
|
||||||
|
let mut video_continuity = VideoContinuity::default();
|
||||||
let mut decoded_frames: u64 = 0;
|
let mut decoded_frames: u64 = 0;
|
||||||
let mut decode_errs: u64 = 0;
|
let mut decode_errs: u64 = 0;
|
||||||
let mut last_written: usize = 0;
|
let mut last_written: usize = 0;
|
||||||
@@ -2656,6 +2736,20 @@ impl CallEngine {
|
|||||||
Ok(Ok(Some(pkt))) => {
|
Ok(Ok(Some(pkt))) => {
|
||||||
// Route video packets to the reassembler before any audio processing.
|
// Route video packets to the reassembler before any audio processing.
|
||||||
if pkt.header.media_type == wzp_proto::MediaType::Video {
|
if pkt.header.media_type == wzp_proto::MediaType::Video {
|
||||||
|
if let Some(gap) = video_continuity.observe_packet(pkt.header.seq) {
|
||||||
|
crate::emit_call_debug(
|
||||||
|
&recv_app,
|
||||||
|
"video:seq_gap",
|
||||||
|
serde_json::json!({
|
||||||
|
"t_ms": recv_t0.elapsed().as_millis() as u64,
|
||||||
|
"seq": pkt.header.seq,
|
||||||
|
"gap": gap,
|
||||||
|
"gaps": video_continuity.gaps,
|
||||||
|
"stream_id": pkt.header.stream_id,
|
||||||
|
"platform": "desktop",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
if !video_first_recv_logged_desktop {
|
if !video_first_recv_logged_desktop {
|
||||||
video_first_recv_logged_desktop = true;
|
video_first_recv_logged_desktop = true;
|
||||||
crate::emit_call_debug(
|
crate::emit_call_debug(
|
||||||
@@ -2702,6 +2796,25 @@ impl CallEngine {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if !video_continuity.should_decode(is_kf) {
|
||||||
|
if video_continuity.dropped_frames <= 5
|
||||||
|
|| video_continuity.dropped_frames % 30 == 0
|
||||||
|
{
|
||||||
|
crate::emit_call_debug(
|
||||||
|
&recv_app,
|
||||||
|
"video:drop_until_keyframe",
|
||||||
|
serde_json::json!({
|
||||||
|
"t_ms": recv_t0.elapsed().as_millis() as u64,
|
||||||
|
"codec": format!("{:?}", codec_id),
|
||||||
|
"frame_no": video_reassembled_samples,
|
||||||
|
"dropped_frames": video_continuity.dropped_frames,
|
||||||
|
"platform": "desktop",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
video_reassembler.evict_stale(pkt.header.timestamp, 5_000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Lazy-init or switch decoder on codec change.
|
// Lazy-init or switch decoder on codec change.
|
||||||
if video_decoder_codec != Some(codec_id) {
|
if video_decoder_codec != Some(codec_id) {
|
||||||
crate::emit_call_debug(
|
crate::emit_call_debug(
|
||||||
@@ -3246,7 +3359,7 @@ impl CallEngine {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if frames_since_keyframe >= 150 {
|
if frames_since_keyframe >= VIDEO_KEYFRAME_INTERVAL_FRAMES {
|
||||||
encoder.request_keyframe();
|
encoder.request_keyframe();
|
||||||
crate::emit_call_debug(
|
crate::emit_call_debug(
|
||||||
&vid_app,
|
&vid_app,
|
||||||
|
|||||||
Reference in New Issue
Block a user