use super::*;
use mediadecode::decoder::VideoStreamDecoder;
use std::num::NonZeroI32;
use crate::{FfmpegBytes, FfmpegOwnedVideoStreamDecoder as FfmpegVideoStreamDecoder};
struct SyntheticClip {
parameters: ffmpeg_next::codec::Parameters,
packets: Vec<Packet>,
}
fn encode_synthetic_clip(width: u32, height: u32, frames: usize, gop: u32) -> SyntheticClip {
use ffmpeg_next as ff;
ff::init().expect("ffmpeg init");
let codec = ff::codec::encoder::find(ff::codec::Id::MPEG4).expect("mpeg4 encoder present");
let ctx = ff::codec::context::Context::new_with_codec(codec);
let mut enc = ctx.encoder().video().expect("video encoder context");
enc.set_width(width);
enc.set_height(height);
enc.set_format(ff::format::Pixel::YUV420P);
enc.set_time_base(ff::Rational::new(1, 25));
enc.set_gop(gop);
enc.set_max_b_frames(0);
enc.set_bit_rate(500_000);
let mut opened = enc.open_as(codec).expect("open encoder");
let parameters = ff::codec::Parameters::from(&opened);
let mut packets: Vec<Packet> = Vec::new();
let drain = |opened: &mut ff::codec::encoder::Video, out: &mut Vec<Packet>| {
loop {
let mut pkt = Packet::empty();
match opened.receive_packet(&mut pkt) {
Ok(()) => out.push(pkt),
Err(_) => break,
}
}
};
let mut frame = ff::frame::Video::new(ff::format::Pixel::YUV420P, width, height);
for i in 0..frames as i64 {
let ystride = frame.stride(0);
{
let data = frame.data_mut(0);
for y in 0..height as usize {
for x in 0..width as usize {
data[y * ystride + x] = ((x + y + i as usize * 4) & 0xff) as u8;
}
}
}
let cstride = frame.stride(1);
for p in 1..3usize {
let data = frame.data_mut(p);
for y in 0..(height as usize / 2) {
for x in 0..(width as usize / 2) {
data[y * cstride + x] = (128 + ((x as i64 - i) & 0x3f)) as u8;
}
}
}
frame.set_pts(Some(i));
opened.send_frame(&frame).expect("send_frame");
drain(&mut opened, &mut packets);
}
opened.send_eof().expect("encoder send_eof");
drain(&mut opened, &mut packets);
assert!(
packets.len() >= 8,
"synthetic clip needs enough packets ({} too few)",
packets.len()
);
assert!(packets[0].is_key(), "first packet must be a keyframe");
SyntheticClip {
parameters,
packets,
}
}
#[derive(Clone, Copy)]
enum FailShape {
PostCommit,
ProbeEra,
}
struct FakeHw {
width: u32,
height: u32,
doom_from_send: usize,
fail_at_send: usize,
shape: FailShape,
sends: usize,
queued: VecDeque<frame::Video>,
history: Vec<Packet>,
fail_at_receive: bool,
}
impl FakeHw {
fn inert() -> Self {
Self {
width: 0,
height: 0,
doom_from_send: usize::MAX,
fail_at_send: usize::MAX,
shape: FailShape::PostCommit,
sends: 0,
queued: VecDeque::new(),
history: Vec::new(),
fail_at_receive: false,
}
}
fn failing(
width: u32,
height: u32,
doom_from_send: usize,
fail_at_send: usize,
shape: FailShape,
) -> Self {
Self {
width,
height,
doom_from_send,
fail_at_send,
shape,
sends: 0,
queued: VecDeque::new(),
history: Vec::new(),
fail_at_receive: false,
}
}
fn failing_at_receive(width: u32, height: u32) -> Self {
let mut hw = Self::failing(width, height, 0, usize::MAX, FailShape::ProbeEra);
hw.fail_at_receive = true;
hw
}
fn never_failing(width: u32, height: u32) -> Self {
Self::failing(width, height, usize::MAX, usize::MAX, FailShape::PostCommit)
}
}
impl HwInner for FakeHw {
fn records_submissions(&self) -> bool {
true
}
fn send_packet(&mut self, packet: &Packet) -> Result<(), Error> {
let idx = self.sends;
self.sends += 1;
if idx == self.fail_at_send {
return match self.shape {
FailShape::PostCommit => Err(Error::AllBackendsFailed(
crate::error::AllBackendsFailed::new_post_commit(Vec::new()),
)),
FailShape::ProbeEra => Err(Error::AllBackendsFailed(
crate::error::AllBackendsFailed::new(Vec::new(), std::mem::take(&mut self.history)),
)),
};
}
if let Ok(cloned) = crate::decoder::try_clone_packet(packet) {
self.history.push(cloned);
}
if idx < self.doom_from_send {
let mut av = frame::Video::new(ffmpeg_next::format::Pixel::YUV420P, self.width, self.height);
av.set_pts(packet.pts());
self.queued.push_back(av);
}
Ok(())
}
fn receive_frame(&mut self, frame: &mut Frame) -> Result<(), Error> {
if self.fail_at_receive {
self.fail_at_receive = false;
return Err(Error::AllBackendsFailed(
crate::error::AllBackendsFailed::new(Vec::new(), std::mem::take(&mut self.history)),
));
}
match self.queued.pop_front() {
Some(av) => {
*frame.as_inner_mut() = av;
Ok(())
}
None => Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
errno: ffmpeg_next::error::EAGAIN,
})),
}
}
fn send_eof(&mut self) -> Result<(), Error> {
Ok(())
}
fn flush(&mut self) -> Result<(), Error> {
self.queued.clear();
Ok(())
}
fn as_video_decoder(&self) -> Option<&VideoDecoder> {
None
}
}
struct FakeHwEofFails {
width: u32,
height: u32,
queued: VecDeque<i64>,
}
impl FakeHwEofFails {
fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
queued: VecDeque::new(),
}
}
}
impl HwInner for FakeHwEofFails {
fn records_submissions(&self) -> bool {
false
}
fn send_packet(&mut self, packet: &Packet) -> Result<(), Error> {
self.queued.push_back(packet.pts().unwrap_or(0));
Ok(())
}
fn receive_frame(&mut self, frame: &mut Frame) -> Result<(), Error> {
match self.queued.pop_front() {
Some(pts) => {
let mut av =
frame::Video::new(ffmpeg_next::format::Pixel::YUV420P, self.width, self.height);
av.set_pts(Some(pts));
*frame.as_inner_mut() = av;
Ok(())
}
None => Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
errno: ffmpeg_next::error::EAGAIN,
})),
}
}
fn send_eof(&mut self) -> Result<(), Error> {
Err(Error::AllBackendsFailed(
crate::error::AllBackendsFailed::new_post_commit(Vec::new()),
))
}
fn flush(&mut self) -> Result<(), Error> {
self.queued.clear();
Ok(())
}
fn as_video_decoder(&self) -> Option<&VideoDecoder> {
None
}
}
fn drive(dec: &mut FfmpegVideoStreamDecoder, clip: &SyntheticClip) -> Vec<i64> {
let mut out: Vec<i64> = Vec::new();
let mut dst = crate::empty_owned_video_frame();
let mut drain_frames = |dec: &mut FfmpegVideoStreamDecoder, out: &mut Vec<i64>| {
loop {
match dec.receive_frame(&mut dst) {
Ok(()) => out.push(dst.pts().map(|t| t.pts()).unwrap_or(i64::MIN)),
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })))
if errno == ffmpeg_next::error::EAGAIN =>
{
break;
}
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Eof))) => break,
Err(e) => panic!("receive_frame: {e:?}"),
}
}
};
for av_pkt in &clip.packets {
let vpkt = boundary::video_packet_from_ffmpeg(av_pkt)
.expect("a wrappable payload")
.expect("packet has a buffer");
dec.send_packet(&vpkt).expect("send_packet");
drain_frames(dec, &mut out);
}
dec.send_eof().expect("send_eof");
drain_frames(dec, &mut out);
out
}
fn nth_keyframe(clip: &SyntheticClip, n: usize) -> usize {
clip
.packets
.iter()
.enumerate()
.filter(|(_, p)| p.is_key())
.nth(n - 1)
.map(|(i, _)| i)
.unwrap_or_else(|| panic!("clip must have at least {n} keyframes (multi-GOP)"))
}
#[test]
fn post_commit_failure_degrades_and_resyncs_at_next_keyframe() {
let (w, h) = (128u32, 96u32);
let clip = encode_synthetic_clip(w, h, 24, 6);
let second_key = nth_keyframe(&clip, 2);
let third_key = nth_keyframe(&clip, 3);
let fail_at = second_key + 2;
assert!(
fail_at < third_key && !clip.packets[fail_at].is_key(),
"fail target must be a mid-GOP P-frame before the next keyframe"
);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test(
Box::new(FakeHw::failing(
w,
h,
fail_at,
fail_at,
FailShape::PostCommit,
)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
assert!(dec.is_hardware(), "must start on the HW seam");
let mut pts_out: Vec<i64> = Vec::new();
let mut dst = crate::empty_owned_video_frame();
let mut drain = |dec: &mut FfmpegVideoStreamDecoder, out: &mut Vec<i64>| loop {
match dec.receive_frame(&mut dst) {
Ok(()) => out.push(dst.pts().map(|t| t.pts()).unwrap_or(i64::MIN)),
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })))
if errno == ffmpeg_next::error::EAGAIN =>
{
break;
}
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Eof))) => break,
Err(e) => panic!("receive_frame: {e:?}"),
}
};
for av_pkt in clip.packets.iter().take(third_key) {
let vpkt = boundary::video_packet_from_ffmpeg(av_pkt)
.expect("a wrappable payload")
.expect("packet has a buffer");
dec.send_packet(&vpkt).expect("send_packet");
drain(&mut dec, &mut pts_out);
}
assert!(
dec.is_software(),
"post-commit HW failure must trigger the SW fallback"
);
assert!(
dec.degraded_resync_pending_for_test(),
"no keyframe fed across the gap yet — the resync guard must still be pending \
(a concealed P-frame must not clear it)"
);
assert!(
!dec.degraded_keyframe_seen_for_test(),
"no keyframe has crossed the gap, so the keyframe-seen anchor must be unset"
);
for av_pkt in clip.packets.iter().skip(third_key) {
let vpkt = boundary::video_packet_from_ffmpeg(av_pkt)
.expect("a wrappable payload")
.expect("packet has a buffer");
dec.send_packet(&vpkt).expect("send_packet");
drain(&mut dec, &mut pts_out);
}
dec.send_eof().expect("send_eof");
drain(&mut dec, &mut pts_out);
assert!(
!dec.degraded_resync_pending_for_test(),
"the keyframe-anchored resync must have cleared the guard before EOF"
);
assert!(
!pts_out.contains(&i64::MIN),
"no delivered frame may have a missing PTS: {pts_out:?}"
);
let third_key_pts = clip.packets[third_key].pts().expect("keyframe has pts");
let total = clip.packets.len() as i64;
let unique: std::collections::HashSet<i64> = pts_out.iter().copied().collect();
assert_eq!(
unique.len(),
pts_out.len(),
"no duplicate PTS — the degrade path must not re-emit a frame: {pts_out:?}"
);
for &pts in &pts_out {
assert!(
(0..total).contains(&pts),
"delivered PTS {pts} is outside the source range 0..{total}: {pts_out:?}"
);
}
for pts in 0..fail_at as i64 {
assert!(
unique.contains(&pts),
"HW delivered PTS {pts} before failing; it must be present: {pts_out:?}"
);
}
for pts in third_key_pts..total {
assert!(
unique.contains(&pts),
"SW must resync at the next keyframe and decode the remainder; PTS {pts} \
(>= resync keyframe {third_key_pts}) is missing — no resync: {pts_out:?}"
);
}
for pts in 0..total {
if !unique.contains(&pts) {
assert!(
(fail_at as i64..third_key_pts).contains(&pts),
"PTS {pts} was dropped but lies OUTSIDE the accepted [fail, keyframe) \
gap [{fail_at}, {third_key_pts}); only the bounded gap may be lost: \
{pts_out:?}"
);
}
}
assert!(
(third_key_pts - fail_at as i64) <= 6,
"the accepted gap must be bounded by ~one GOP; was {}",
third_key_pts - fail_at as i64
);
}
#[test]
fn fake_hw_without_failure_stays_on_hardware() {
let (w, h) = (128u32, 96u32);
let clip = encode_synthetic_clip(w, h, 12, 6);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test(
Box::new(FakeHw::never_failing(w, h)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
let pts_out = drive(&mut dec, &clip);
assert!(dec.is_hardware(), "no failure => stays on the HW seam");
assert_eq!(
pts_out.len(),
clip.packets.len(),
"HW path must deliver one frame per packet"
);
}
#[test]
fn probe_era_failure_replays_history_losslessly() {
let (w, h) = (128u32, 96u32);
let clip = encode_synthetic_clip(w, h, 16, 6);
let fail_at = 5;
assert!(fail_at < clip.packets.len(), "fail target in range");
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test(
Box::new(FakeHw::failing(w, h, 0, fail_at, FailShape::ProbeEra)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
assert!(dec.is_hardware(), "must start on the HW seam");
let pts_out = drive(&mut dec, &clip);
assert!(
dec.is_software(),
"probe-era HW failure must trigger the SW fallback"
);
assert!(
!pts_out.contains(&i64::MIN),
"no delivered frame may have a missing PTS: {pts_out:?}"
);
let mut sorted = pts_out.clone();
sorted.sort_unstable();
let expected: Vec<i64> = (0..clip.packets.len() as i64).collect();
assert_eq!(
sorted, expected,
"a probe-era fallback must lose no frames — every source PTS delivered \
exactly once: {pts_out:?}"
);
}
fn unopenable_sw_decoder(hw: Box<dyn HwInner>) -> FfmpegVideoStreamDecoder {
ffmpeg_next::init().expect("ffmpeg init");
let params = ffmpeg_next::codec::Parameters::new();
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
FfmpegVideoStreamDecoder::from_hw_inner_for_test(hw, params, tb).expect("build test decoder")
}
#[test]
fn post_commit_sw_open_failure_stays_on_hw_transactionally() {
let (w, h) = (64u32, 64u32);
let mut dec = unopenable_sw_decoder(Box::new(FakeHw::failing(w, h, 0, 0, FailShape::PostCommit)));
assert!(dec.is_hardware(), "must start on the HW seam");
let mut raw = Packet::new(16);
raw.set_pts(Some(0));
let vpkt = boundary::video_packet_from_ffmpeg(&raw)
.expect("a wrappable payload")
.expect("packet has a buffer");
let err = dec
.send_packet(&vpkt)
.expect_err("SW-open failure must surface an error");
match err {
VideoDecodeError::Decode(Error::FallbackFailed(_)) => {}
other => panic!("expected FallbackFailed on SW-open failure, got {other:?}"),
}
assert!(
dec.is_hardware(),
"a failed fallback (SW could not open) must leave the decoder on its prior \
HW state — transactional rollback, not a half-committed SW"
);
}
fn corrupt_packet_payload(pkt: &mut Packet) {
if let Some(d) = pkt.data_mut() {
for b in d.iter_mut() {
*b = 0;
}
}
}
#[test]
fn sw_replay_drain_surfaces_non_transient_decode_error() {
let (w, h) = (128u32, 96u32);
let mut clip = encode_synthetic_clip(w, h, 12, 100);
let p1 = clip
.packets
.iter()
.position(|p| !p.is_key())
.expect("clip has P-frames");
assert!(
p1 + 2 < clip.packets.len(),
"need packets after the corrupt one"
);
corrupt_packet_payload(&mut clip.packets[p1]);
let fail_at = p1 + 3;
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test(
Box::new(FakeHw::failing(w, h, 0, fail_at, FailShape::ProbeEra)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
let mut dst = crate::empty_owned_video_frame();
let mut err = None;
for av_pkt in &clip.packets {
let vpkt = boundary::video_packet_from_ffmpeg(av_pkt)
.expect("a wrappable payload")
.expect("packet has a buffer");
if let Err(e) = dec.send_packet(&vpkt) {
err = Some(e);
break;
}
loop {
match dec.receive_frame(&mut dst) {
Ok(()) => {}
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })))
if errno == ffmpeg_next::error::EAGAIN =>
{
break;
}
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Eof))) => break,
Err(e) => {
err = Some(e);
break;
}
}
}
if err.is_some() {
break;
}
}
let err = err.expect("the corrupt replayed packet must surface an error, not be swallowed");
match err {
VideoDecodeError::Decode(Error::FallbackFailed(f)) => {
assert!(
!f.unconsumed_packets().is_empty(),
"FallbackFailed must carry the replay packets for recovery"
);
assert!(
matches!(f.source(), Error::Ffmpeg(ffmpeg_next::Error::InvalidData)),
"the surfaced error must be the SW InvalidData decode failure; got {:?}",
f.source()
);
}
other => panic!("expected FallbackFailed surfacing InvalidData, got {other:?}"),
}
assert!(
dec.is_hardware(),
"a failed fallback must leave the decoder on its prior (HW) state, not \
commit SW over swallowed corruption"
);
}
#[test]
fn sw_replay_deferred_error_surfaces_fallback_failed_at_commit() {
let (w, h) = (128u32, 96u32);
let mut clip = encode_synthetic_clip(w, h, 12, 100);
let fail_at = 5;
assert!(
fail_at >= 2 && fail_at < clip.packets.len(),
"need a multi-packet history with room for a corrupt tail"
);
let corrupt_idx = fail_at - 1;
assert!(
!clip.packets[corrupt_idx].is_key(),
"the corrupt last-history packet must be a P-frame (a corrupt keyframe \
could fail SW's send_packet instead of receive_frame)"
);
corrupt_packet_payload(&mut clip.packets[corrupt_idx]);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test(
Box::new(FakeHw::failing(w, h, 0, fail_at, FailShape::ProbeEra)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
let mut surfaced = None;
let mut dst = crate::empty_owned_video_frame();
for av_pkt in clip.packets.iter().take(fail_at + 1) {
let vpkt = boundary::video_packet_from_ffmpeg(av_pkt)
.expect("a wrappable payload")
.expect("packet has a buffer");
match dec.send_packet(&vpkt) {
Ok(()) => {
loop {
match dec.receive_frame(&mut dst) {
Ok(()) => {}
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })))
if errno == ffmpeg_next::error::EAGAIN =>
{
break;
}
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Eof))) => break,
Err(e) => {
surfaced = Some(e);
break;
}
}
}
}
Err(e) => {
surfaced = Some(e);
break;
}
}
if surfaced.is_some() {
break;
}
}
let err = surfaced.expect(
"the deferred InvalidData must surface at the fallback commit boundary, not be \
committed over",
);
match err {
VideoDecodeError::Decode(Error::FallbackFailed(f)) => {
assert!(
!f.unconsumed_packets().is_empty(),
"FallbackFailed must retain the rescued replay packets for recovery"
);
assert!(
matches!(f.source(), Error::Ffmpeg(ffmpeg_next::Error::InvalidData)),
"the surfaced error must be the deferred SW InvalidData; got {:?}",
f.source()
);
}
other => panic!("expected FallbackFailed surfacing the deferred InvalidData, got {other:?}"),
}
assert!(
dec.is_hardware(),
"a deferred-error fallback caught at the commit boundary must leave the \
decoder on its prior HW state — nothing committed"
);
}
#[test]
fn failed_eof_fallback_restores_eof_sent_and_stays_on_hw() {
let (w, h) = (64u32, 64u32);
let mut dec = unopenable_sw_decoder(Box::new(FakeHwEofFails::new(w, h)));
assert!(dec.is_hardware(), "must start on the HW seam");
assert!(
!dec.eof_sent_for_test(),
"precondition: eof_sent starts false"
);
let err = dec
.send_eof()
.expect_err("a failed EOF fallback must surface an error");
match err {
VideoDecodeError::Decode(Error::FallbackFailed(_)) => {}
other => panic!("expected FallbackFailed on the failed EOF fallback, got {other:?}"),
}
assert!(
dec.is_hardware(),
"a failed EOF fallback (SW could not open) must leave the decoder on its \
prior HW state — transactional rollback"
);
assert!(
!dec.eof_sent_for_test(),
"eof_sent must be RESTORED to its prior value (false) after a failed EOF \
fallback — a stale true would inject EOF into a later SW fallback"
);
let err2 = dec.send_eof().expect_err(
"the still-HW decoder must re-attempt (and re-fail) EOF, not no-op off stale state",
);
assert!(
matches!(err2, VideoDecodeError::Decode(Error::FallbackFailed(_))),
"second send_eof must again drive the fallback (proving no stale-EOF short-circuit)"
);
assert!(
!dec.eof_sent_for_test(),
"still rolled back after the retry"
);
}
#[test]
fn post_commit_fallback_never_resyncing_escalates_at_eof() {
let (w, h) = (128u32, 96u32);
let clip = encode_synthetic_clip(w, h, 12, 6);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test(
Box::new(FakeHwEofFails::new(w, h)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
let mut dst = crate::empty_owned_video_frame();
let mut delivered = 0usize;
let mut escalation = None;
let mut drain = |dec: &mut FfmpegVideoStreamDecoder,
delivered: &mut usize,
escalation: &mut Option<VideoDecodeError>| {
loop {
match dec.receive_frame(&mut dst) {
Ok(()) => *delivered += 1,
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })))
if errno == ffmpeg_next::error::EAGAIN =>
{
break;
}
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Eof))) => break,
Err(e @ VideoDecodeError::PostCommitNeverResynced(_)) => {
*escalation = Some(e);
break;
}
Err(e) => panic!("unexpected error draining frames: {e:?}"),
}
}
};
for av_pkt in &clip.packets {
let vpkt = boundary::video_packet_from_ffmpeg(av_pkt)
.expect("a wrappable payload")
.expect("packet has a buffer");
dec.send_packet(&vpkt).expect("send_packet");
drain(&mut dec, &mut delivered, &mut escalation);
assert!(
escalation.is_none(),
"no escalation while still on the HW path"
);
}
assert!(dec.is_hardware(), "still HW until the EOF-time failure");
assert_eq!(
delivered,
clip.packets.len(),
"HW must deliver the whole stream before the EOF-time failure"
);
dec
.send_eof()
.expect("send_eof drives the fallback but itself succeeds");
assert!(
dec.is_software(),
"the EOF-time failure fell back to software"
);
assert!(
dec.degraded_resync_pending_for_test(),
"post-commit fallback at EOF must enter degraded-resync mode (SW opened cold)"
);
drain(&mut dec, &mut delivered, &mut escalation);
let esc = escalation.expect(
"a post-commit fallback whose SW decoder reaches EOF without resyncing must \
ESCALATE, not silently swallow the tail as a clean end-of-stream",
);
let VideoDecodeError::PostCommitNeverResynced(p) = esc else {
panic!("expected PostCommitNeverResynced, got {esc:?}");
};
let packets_lost = p.packets_lost();
assert_eq!(
packets_lost, 0,
"no packets crossed to SW on the EOF-entry path; the lost tail was HW-side"
);
assert!(
dec.is_software(),
"the decoder did fall back to software (it just never resynced)"
);
assert!(
!dec.degraded_resync_pending_for_test(),
"the degraded-resync flag must be cleared after the escalation fires"
);
let mut after = crate::empty_owned_video_frame();
match dec.receive_frame(&mut after) {
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Eof))) => {}
other => panic!("a poll after the escalation must be plain EOF, got {other:?}"),
}
}
#[test]
fn post_commit_gap_counter_tallies_then_clears_on_resync() {
let (w, h) = (128u32, 96u32);
let clip = encode_synthetic_clip(w, h, 24, 6);
let second_key = nth_keyframe(&clip, 2);
let third_key = nth_keyframe(&clip, 3);
let fail_at = second_key + 2;
assert!(
fail_at < third_key && !clip.packets[fail_at].is_key(),
"fail target must be a mid-GOP P-frame before the next keyframe"
);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test(
Box::new(FakeHw::failing(
w,
h,
fail_at,
fail_at,
FailShape::PostCommit,
)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
let mut dst = crate::empty_owned_video_frame();
for av_pkt in clip.packets.iter().take(fail_at + 1) {
let vpkt = boundary::video_packet_from_ffmpeg(av_pkt)
.expect("a wrappable payload")
.expect("packet has a buffer");
dec.send_packet(&vpkt).expect("send_packet");
}
assert!(
dec.is_software(),
"the mid-GOP failure fell back to software"
);
assert!(
dec.degraded_resync_pending_for_test(),
"the gap is still open (no resync frame drained yet)"
);
assert_eq!(
dec.degraded_packets_since_fallback_for_test(),
1,
"the forwarded current packet must be tallied as crossing the gap"
);
let mut try_poll = |dec: &mut FfmpegVideoStreamDecoder| -> bool {
match dec.receive_frame(&mut dst) {
Ok(()) => true,
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })))
if errno == ffmpeg_next::error::EAGAIN =>
{
false
}
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Eof))) => false,
Err(e) => panic!("unexpected drain error: {e:?}"),
}
};
while try_poll(&mut dec) {}
assert!(
dec.degraded_resync_pending_for_test(),
"a concealed frame from the forwarded P-frame must NOT clear the guard — no \
keyframe has crossed the gap yet"
);
assert!(
!dec.degraded_keyframe_seen_for_test(),
"no keyframe fed yet, so the keyframe-seen anchor must be unset"
);
for av_pkt in clip.packets[(fail_at + 1)..third_key].iter() {
let vpkt = boundary::video_packet_from_ffmpeg(av_pkt)
.expect("a wrappable payload")
.expect("packet has a buffer");
dec.send_packet(&vpkt).expect("send_packet");
while try_poll(&mut dec) {}
assert!(
dec.degraded_resync_pending_for_test() && !dec.degraded_keyframe_seen_for_test(),
"concealed P-frame frames before the keyframe must not clear the guard or \
set the keyframe anchor"
);
}
assert!(third_key < clip.packets.len(), "clip has a third keyframe");
let key_vpkt = boundary::video_packet_from_ffmpeg(&clip.packets[third_key])
.expect("a wrappable payload")
.expect("packet has a buffer");
dec.send_packet(&key_vpkt).expect("send_packet");
assert!(
dec.degraded_keyframe_seen_for_test(),
"feeding the keyframe across the gap must record it as the resync anchor"
);
let mut resynced = !dec.degraded_resync_pending_for_test();
while !resynced && try_poll(&mut dec) {
resynced = !dec.degraded_resync_pending_for_test();
}
for av_pkt in clip.packets[(third_key + 1)..].iter() {
if resynced {
break;
}
let vpkt = boundary::video_packet_from_ffmpeg(av_pkt)
.expect("a wrappable payload")
.expect("packet has a buffer");
dec.send_packet(&vpkt).expect("send_packet");
while !resynced && try_poll(&mut dec) {
resynced = !dec.degraded_resync_pending_for_test();
}
}
assert!(
resynced,
"SW must resync once the keyframe is fed and produce a frame after it"
);
assert!(
!dec.degraded_resync_pending_for_test(),
"the keyframe-anchored resync must clear the pending flag"
);
assert_eq!(
dec.degraded_packets_since_fallback_for_test(),
0,
"resync must reset the gap counter"
);
}
#[test]
fn post_commit_concealed_p_frame_does_not_clear_resync_escalates_at_eof() {
let (w, h) = (128u32, 96u32);
let clip = encode_synthetic_clip(w, h, 24, 6);
let second_key = nth_keyframe(&clip, 2);
let third_key = nth_keyframe(&clip, 3);
let fail_at = second_key + 2;
assert!(
fail_at < third_key && !clip.packets[fail_at].is_key(),
"fail target must be a mid-GOP P-frame before the next keyframe"
);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test(
Box::new(FakeHw::failing(
w,
h,
fail_at,
fail_at,
FailShape::PostCommit,
)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
let mut dst = crate::empty_owned_video_frame();
let mut concealed_frames = 0usize;
let mut escalation: Option<VideoDecodeError> = None;
let mut drain = |dec: &mut FfmpegVideoStreamDecoder,
concealed: &mut usize,
escalation: &mut Option<VideoDecodeError>| loop {
match dec.receive_frame(&mut dst) {
Ok(()) => *concealed += 1,
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })))
if errno == ffmpeg_next::error::EAGAIN =>
{
break;
}
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Eof))) => break,
Err(e @ VideoDecodeError::PostCommitNeverResynced(_)) => {
*escalation = Some(e);
break;
}
Err(e) => panic!("unexpected drain error: {e:?}"),
}
};
for av_pkt in clip.packets.iter().take(third_key) {
let vpkt = boundary::video_packet_from_ffmpeg(av_pkt)
.expect("a wrappable payload")
.expect("packet has a buffer");
dec.send_packet(&vpkt).expect("send_packet");
drain(&mut dec, &mut concealed_frames, &mut escalation);
assert!(escalation.is_none(), "no escalation before EOF");
if dec.is_software() {
assert!(
dec.degraded_resync_pending_for_test(),
"a concealed P-frame must not clear the keyframe-gated resync guard"
);
assert!(
!dec.degraded_keyframe_seen_for_test(),
"no keyframe was fed, so the keyframe-seen anchor must stay unset"
);
}
}
assert!(
dec.is_software(),
"the post-commit failure fell back to software"
);
assert!(
concealed_frames > 0,
"the cold mpeg4 SW decoder must have concealed at least one frame from the \
lone P-frames (otherwise this test does not exercise the 'frame delivered \
but no keyframe' path)"
);
assert!(
dec.degraded_resync_pending_for_test(),
"after feeding only P-frames the guard must still be pending — the concealed \
frames did NOT count as a resync"
);
dec.send_eof().expect("send_eof on the SW path");
drain(&mut dec, &mut concealed_frames, &mut escalation);
let esc = escalation.expect(
"concealed P-frames must NOT have cleared the guard, so reaching EOF without a \
keyframe must ESCALATE with PostCommitNeverResynced",
);
let VideoDecodeError::PostCommitNeverResynced(p) = esc else {
panic!("expected PostCommitNeverResynced, got {esc:?}");
};
let packets_lost = p.packets_lost();
assert!(
packets_lost >= 1,
"every forwarded gap packet (current P-frame + the GOP-2 tail) must be \
tallied as lost; got {packets_lost}"
);
assert!(
!dec.degraded_resync_pending_for_test(),
"the guard is cleared after the escalation fires"
);
}
#[test]
fn post_commit_retains_no_replay_frames() {
let (w, h) = (128u32, 96u32);
let clip = encode_synthetic_clip(w, h, 24, 6);
let second_key = nth_keyframe(&clip, 2);
let third_key = nth_keyframe(&clip, 3);
let fail_at = second_key + 2;
assert!(
fail_at < third_key && !clip.packets[fail_at].is_key(),
"fail target must be a mid-GOP P-frame before the next keyframe"
);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test(
Box::new(FakeHw::failing(
w,
h,
fail_at,
fail_at,
FailShape::PostCommit,
)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
assert!(
dec.sw_replay_frames_is_empty_for_test(),
"no replay frames before any fallback"
);
for av_pkt in clip.packets.iter().take(fail_at + 1) {
let vpkt = boundary::video_packet_from_ffmpeg(av_pkt)
.expect("a wrappable payload")
.expect("packet has a buffer");
dec.send_packet(&vpkt).expect("send_packet");
assert!(
dec.sw_replay_frames_is_empty_for_test(),
"the post-commit path must retain ZERO replay frames — nothing is drained \
into the replay queue, so there is no deferred conversion (finding 1)"
);
}
assert!(
dec.is_software(),
"the mid-GOP failure fell back to software"
);
assert!(
dec.degraded_resync_pending_for_test(),
"post-commit fallback entered degraded mode (sanity)"
);
let mut dst = crate::empty_owned_video_frame();
for av_pkt in clip.packets.iter().skip(fail_at + 1) {
let vpkt = boundary::video_packet_from_ffmpeg(av_pkt)
.expect("a wrappable payload")
.expect("packet has a buffer");
dec.send_packet(&vpkt).expect("send_packet");
loop {
match dec.receive_frame(&mut dst) {
Ok(()) => {}
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })))
if errno == ffmpeg_next::error::EAGAIN =>
{
break;
}
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Eof))) => break,
Err(e) => panic!("unexpected drain error: {e:?}"),
}
}
assert!(
dec.sw_replay_frames_is_empty_for_test(),
"the post-commit path never populates the replay queue"
);
}
}
#[test]
fn inert_seam_builds_on_hardware() {
ffmpeg_next::init().expect("ffmpeg init");
let params = ffmpeg_next::codec::Parameters::new();
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test(Box::new(FakeHw::inert()), params, tb)
.expect("build test decoder");
assert!(dec.is_hardware(), "inert seam starts on the HW path");
assert!(!dec.is_software());
}
#[test]
#[ignore = "requires a Sony FX3 H.264 High 4:2:2 10-bit fixture (user-provided); \
set MEDIADECODE_FX3_SAMPLE to its path"]
fn fx3_high_422_10bit_falls_back_to_software_and_decodes_whole_stream() {
use ffmpeg_next::{format, media};
let Some(path) = std::env::var_os("MEDIADECODE_FX3_SAMPLE") else {
eprintln!(
"skipping: set MEDIADECODE_FX3_SAMPLE to the Sony FX3 H.264 422-10bit fixture path to run \
this experiment"
);
return;
};
ffmpeg_next::init().expect("ffmpeg init");
let mut input = format::input(&path).expect("open FX3 input");
let stream = input
.streams()
.best(media::Type::Video)
.expect("video stream");
let stream_index = stream.index();
let (expected_w, expected_h) = unsafe {
let p = stream.parameters();
((*p.as_ptr()).width as u32, (*p.as_ptr()).height as u32)
};
let tb = Timebase::new(1, NonZeroI32::new(24).expect("nonzero"));
let mut dec = match FfmpegVideoStreamDecoder::open(
stream.parameters(),
tb,
crate::DecoderLimits::default(),
) {
Ok(d) => d,
Err(Error::AllBackendsFailed(p)) => {
eprintln!(
"skipping: no hardware backend available at open ({} attempts) — the post-commit \
degrade path needs a HW backend that COMMITS then fails at runtime",
p.attempts().len()
);
return;
}
Err(e) => panic!("open FX3 decoder: {e:?}"),
};
let mut obs = Fx3Observation::new(dec.is_hardware());
eprintln!(
"FX3 experiment: {expected_w}x{expected_h}; started_on_hw={} (is_software={})",
obs.started_on_hw,
dec.is_software()
);
let mut dst = crate::empty_owned_video_frame();
'feed: for (s, packet) in input.packets() {
if s.index() != stream_index {
continue;
}
let is_key = packet.is_key();
let pkt_pts = packet.pts();
let Some(vpkt) = boundary::video_packet_from_ffmpeg(&packet).expect("a wrappable payload")
else {
continue; };
let mut attempts = 0u32;
loop {
match dec.send_packet(&vpkt) {
Ok(()) => break,
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })))
if errno == ffmpeg_next::error::EAGAIN =>
{
if let Err(err) = obs.drain(&mut dec, &mut dst) {
obs.abort = Some(format!("during send #{} EAGAIN-drain: {err}", obs.send_idx));
break 'feed;
}
attempts += 1;
assert!(
attempts <= 64,
"send_packet stuck on EAGAIN at send #{}",
obs.send_idx
);
}
Err(e) => {
obs.abort = Some(format!(
"send_packet #{} (key={is_key}, pts={pkt_pts:?}) errored: {e:?}",
obs.send_idx
));
break 'feed;
}
}
}
if let Err(err) = obs.drain(&mut dec, &mut dst) {
obs.abort = Some(format!(
"after send #{} (key={is_key}): {err}",
obs.send_idx
));
break 'feed;
}
obs.send_idx += 1;
}
if obs.abort.is_none() {
match dec.send_eof() {
Ok(()) => {
if let Err(err) = obs.drain(&mut dec, &mut dst) {
obs.abort = Some(format!("during post-EOF drain: {err}"));
}
}
Err(VideoDecodeError::PostCommitNeverResynced(p)) => {
obs.escalated_never_resynced = Some(p.packets_lost());
}
Err(e) => obs.abort = Some(format!("send_eof errored: {e:?}")),
}
}
let ended_on_sw = dec.is_software();
let unique: std::collections::HashSet<i64> = obs.pts_out.iter().copied().collect();
eprintln!("FX3 experiment RESULT:");
eprintln!(" started_on_hw = {}", obs.started_on_hw);
eprintln!(
" transitioned_to_sw = {} (at send #{:?})",
obs.transitioned_to_sw, obs.transition_send_idx
);
eprintln!(" ended_on_sw = {ended_on_sw}");
eprintln!(
" frames_delivered = {} (unique pts = {})",
obs.pts_out.len(),
unique.len()
);
eprintln!(" delivered_pts = {:?}", obs.pts_out);
eprintln!(
" resync_pending@end = {}",
dec.degraded_resync_pending_for_test()
);
eprintln!(
" never_resynced_esc = {:?}",
obs.escalated_never_resynced
);
eprintln!(" abort = {:?}", obs.abort);
assert!(
obs.started_on_hw,
"expected to start on the VideoToolbox HW path; if it opened straight to SW the post-commit \
path was never exercised on this run"
);
assert!(
obs.transitioned_to_sw && ended_on_sw,
"expected a transparent mid-stream HW->SW fallback on the real FX3 clip (VideoToolbox cannot \
decode H.264 High 4:2:2 10-bit at runtime); observed transition={}, ended_on_sw={ended_on_sw}, \
abort={:?}",
obs.transitioned_to_sw,
obs.abort
);
assert!(
obs.abort.is_none(),
"the degrade-and-continue path aborted before EOF on the real H.264 codec: {:?} — this would \
be Codex R7's finding reproducing on a real (non-lenient) codec",
obs.abort
);
assert!(
obs.escalated_never_resynced.is_none(),
"the cold SW decoder never resynced at a keyframe before EOF (PostCommitNeverResynced, {:?} \
packets lost) — the whole tail was dropped; Codex R7's finding 2 (HW swallowed the keyframe / \
cold SW never saw it) reproduces on real H.264",
obs.escalated_never_resynced
);
assert!(
!dec.degraded_resync_pending_for_test(),
"a post-commit resync was still pending at EOF — SW never proved a keyframe-anchored resync"
);
assert!(
!obs.pts_out.is_empty(),
"no frames were delivered at all — neither HW prefix nor SW remainder"
);
assert!(
!obs.pts_out.contains(&i64::MIN),
"every delivered frame must carry a real PTS: {:?}",
obs.pts_out
);
assert_eq!(
unique.len(),
obs.pts_out.len(),
"the degrade path must not re-emit a frame (no duplicate PTS): {:?}",
obs.pts_out
);
}
struct Fx3Observation {
started_on_hw: bool,
transitioned_to_sw: bool,
transition_send_idx: Option<usize>,
send_idx: usize,
pts_out: Vec<i64>,
abort: Option<String>,
escalated_never_resynced: Option<u64>,
}
impl Fx3Observation {
fn new(started_on_hw: bool) -> Self {
Self {
started_on_hw,
transitioned_to_sw: false,
transition_send_idx: None,
send_idx: 0,
pts_out: Vec::new(),
abort: None,
escalated_never_resynced: None,
}
}
fn note_transition(&mut self, dec: &FfmpegVideoStreamDecoder, frame_pending: bool) {
if !self.transitioned_to_sw && dec.is_software() {
self.transitioned_to_sw = true;
self.transition_send_idx = Some(self.send_idx);
let detail = if frame_pending {
format!("frames delivered so far: {}", self.pts_out.len())
} else {
"no frame yet — cold SW awaiting resync keyframe".to_string()
};
eprintln!(
" -> HW->SW transition observed at/after send #{} ({detail})",
self.send_idx
);
}
}
fn drain(
&mut self,
dec: &mut FfmpegVideoStreamDecoder,
dst: &mut VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, FfmpegBytes>,
) -> Result<(), String> {
loop {
match dec.receive_frame(dst) {
Ok(()) => {
self.note_transition(dec, true);
let pts = VideoFrame::pts(dst).map(|t| t.pts()).unwrap_or(i64::MIN);
self.pts_out.push(pts);
}
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })))
if errno == ffmpeg_next::error::EAGAIN =>
{
self.note_transition(dec, false);
break;
}
Err(VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Eof))) => break,
Err(VideoDecodeError::PostCommitNeverResynced(p)) => {
let packets_lost = p.packets_lost();
self.escalated_never_resynced = Some(packets_lost);
eprintln!(
" -> PostCommitNeverResynced at EOF: {packets_lost} packets fed to SW produced no \
frame (no keyframe crossed the gap)"
);
break;
}
Err(e) => return Err(format!("{e:?}")),
}
}
Ok(())
}
}
#[test]
fn the_cold_fallback_forwards_keep_the_allocator_refusal() {
use crate::{DecoderLimits, FrameLimits, error::FrameMedium};
let clip = encode_synthetic_clip(640, 480, 12, 3);
let limits = DecoderLimits::new().with_frame(FrameLimits::new().with_max_frame_bytes(64 * 1024));
let named = |e: &Error| match e {
Error::FrameBudgetExceeded(p) => Some(*p),
_ => None,
};
let mut sw = super::open_sw_decoder(&clip.parameters, limits).expect("open sw");
let state = sw.state();
let refusal = sw
.send_packet(&clip.packets[0])
.map_err(|e| crate::decoder::software_exit(state, e))
.expect_err("a 460 KB frame passed a 64 KiB ceiling");
let payload = named(&refusal).expect("the packet arm lost the allocator refusal");
assert_eq!(payload.medium(), FrameMedium::Video);
assert_eq!(payload.limit(), 64 * 1024);
assert!(payload.bytes() > payload.limit());
let mut sw = super::open_sw_decoder(&clip.parameters, limits).expect("open sw");
let state = sw.state();
let _ = sw.send_packet(&clip.packets[0]);
if let Err(e) = sw
.send_eof()
.map_err(|e| crate::decoder::software_exit(state, e))
{
let payload = named(&e).expect("the EOF arm lost the allocator refusal");
assert_eq!(payload.limit(), 64 * 1024);
}
let generous = DecoderLimits::new()
.with_frame(FrameLimits::new().with_max_frame_bytes(crate::DEFAULT_MAX_FRAME_BYTES));
let mut sw = super::open_sw_decoder(&clip.parameters, generous).expect("open sw");
let state = sw.state();
sw.send_packet(&clip.packets[0])
.map_err(|e| crate::decoder::software_exit(state, e))
.expect("an affordable frame must be accepted");
}
#[test]
fn a_rescued_packet_never_aliases_a_view_carrier() {
use crate::{CarrierVideoStreamDecoder, View, boundary::video_packet_from_ffmpeg_in};
use ffmpeg_next::packet::Ref;
use mediadecode::decoder::VideoStreamDecoder;
let (w, h) = (128u32, 96u32);
let mut clip = encode_synthetic_clip(w, h, 12, 100);
let p1 = clip
.packets
.iter()
.position(|p| !p.is_key())
.expect("clip has P-frames");
assert!(
p1 + 2 < clip.packets.len(),
"need packets after the corrupt one"
);
corrupt_packet_payload(&mut clip.packets[p1]);
let fail_at = p1 + 3;
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = CarrierVideoStreamDecoder::<View>::from_hw_inner_for_test(
Box::new(FakeHw::failing(w, h, 0, fail_at, FailShape::ProbeEra)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
let mut retained: Vec<crate::VideoPacket> = Vec::new();
let mut dst = crate::boundary::empty_video_frame();
let mut rescued: Vec<ffmpeg_next::Packet> = Vec::new();
for av_pkt in &clip.packets {
let Some(vpkt) =
video_packet_from_ffmpeg_in(av_pkt.clone(), tb, crate::PacketLimits::default())
.expect("a wrappable payload")
else {
continue;
};
match dec.send_packet(&vpkt) {
Ok(()) => {}
Err(VideoDecodeError::Decode(Error::FallbackFailed(f))) => {
retained.push(vpkt);
rescued = f.into_unconsumed_packets();
break;
}
Err(e) => panic!("send_packet: {e:?}"),
}
retained.push(vpkt);
while dec.receive_frame(&mut dst).is_ok() {}
}
assert!(
!rescued.is_empty(),
"the failed fallback must surface a rescue history to check",
);
let carriers: Vec<usize> = retained
.iter()
.map(|p| p.data().as_ref().as_ptr() as usize)
.collect();
for packet in &rescued {
let address = unsafe { (*packet.as_ptr()).data as usize };
assert!(
!carriers.contains(&address),
"a rescued packet addresses a retained view carrier's storage — \
`data_mut` on it would be an aliasing write",
);
}
let before: Vec<Vec<u8>> = retained
.iter()
.map(|p| p.data().as_ref().to_vec())
.collect();
{
for packet in &mut rescued {
if let Some(slot) = packet.data_mut() {
for byte in slot.iter_mut() {
*byte ^= 0xFF;
}
}
}
}
for (packet, expected) in retained.iter().zip(before) {
assert_eq!(
packet.data().as_ref(),
expected.as_slice(),
"writing a rescued packet reached a retained view carrier",
);
}
}
#[test]
fn the_receive_time_fallback_queue_survives_a_failed_carrier() {
use crate::{CarrierVideoStreamDecoder, View, boundary::video_packet_from_ffmpeg_in};
use mediadecode::decoder::VideoStreamDecoder;
crate::fault_subprocess::in_subprocess(
"video::tests::the_receive_time_fallback_queue_survives_a_failed_carrier",
|| {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 8, 100);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let drive = |cap_when_queued: bool| -> (Vec<Vec<u8>>, bool) {
let mut dec = CarrierVideoStreamDecoder::<View>::from_hw_inner_for_test(
Box::new(FakeHw::failing_at_receive(w, h)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
let mut frame = crate::boundary::empty_video_frame();
let mut planes = Vec::new();
let mut queue_was_hit = false;
let mut armed = false;
for av_pkt in &clip.packets {
let Some(vpkt) =
video_packet_from_ffmpeg_in(av_pkt.clone(), tb, crate::PacketLimits::default())
.expect("a wrappable payload")
else {
continue;
};
if dec.send_packet(&vpkt).is_err() {
break;
}
}
loop {
let capped = armed;
if capped {
armed = false;
queue_was_hit = true;
crate::fault_subprocess::cap_ffmpeg_allocations(16);
}
let got = dec.receive_frame(&mut frame);
if capped {
crate::fault_subprocess::uncap_ffmpeg_allocations();
}
match got {
Ok(()) => {
planes.push(frame.planes()[0].data_ref().as_ref().to_vec());
if cap_when_queued && !queue_was_hit && !dec.sw_replay_frames_is_empty_for_test() {
armed = true;
}
}
Err(VideoDecodeError::Convert(e)) => {
assert!(capped, "no refusal was asked for here, got {e:?}");
assert!(
e.parks_in_decode(),
"the ceiling must produce a parkable refusal, got {e:?}",
);
continue;
}
Err(_) => break,
}
}
(planes, queue_was_hit)
};
let (reference, _) = drive(false);
assert!(
reference.len() >= 2,
"the receive-time fallback must deliver replayed frames to test with",
);
let (recovered, queue_was_hit) = drive(true);
assert!(
queue_was_hit,
"the ceiling never reached the replay queue — this lane would \
assert nothing",
);
assert_eq!(
recovered, reference,
"a transient refusal must cost no replayed frame at all",
);
},
);
}
#[test]
fn a_parked_hardware_frame_is_delivered_before_any_fallback() {
use crate::{CarrierVideoStreamDecoder, View, boundary::video_packet_from_ffmpeg_in};
use mediadecode::decoder::VideoStreamDecoder;
crate::fault_subprocess::in_subprocess(
"video::tests::a_parked_hardware_frame_is_delivered_before_any_fallback",
|| {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 8, 100);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = CarrierVideoStreamDecoder::<View>::from_hw_inner_for_test(
Box::new(FakeHw::failing(w, h, usize::MAX, 1, FailShape::PostCommit)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
let packet = |index: usize| {
video_packet_from_ffmpeg_in(
clip.packets[index].clone(),
tb,
crate::PacketLimits::default(),
)
.expect("a wrappable payload")
.expect("packet has a buffer")
};
dec.send_packet(&packet(0)).expect("send_packet");
assert!(dec.is_hardware(), "the seam under test is the hardware one");
let mut frame = crate::boundary::empty_video_frame();
crate::fault_subprocess::cap_ffmpeg_allocations(16);
let refused = dec.receive_frame(&mut frame);
crate::fault_subprocess::uncap_ffmpeg_allocations();
match refused {
Err(VideoDecodeError::Convert(e)) => assert!(
e.parks_in_decode(),
"the ceiling must produce a parkable refusal, got {e:?}",
),
other => panic!("expected a parkable refusal, got {other:?}"),
}
assert!(
matches!(
dec.send_packet(&packet(1)),
Err(VideoDecodeError::FramePending)
),
"a send under a parked frame must be refused by name",
);
assert!(
matches!(dec.send_eof(), Err(VideoDecodeError::FramePending)),
"EOF under a parked frame must be refused by name too",
);
dec.receive_frame(&mut frame).expect("the parked frame");
assert!(
dec.is_hardware(),
"no fallback can have happened while the frame was parked",
);
assert!(
!frame.planes()[0].data_ref().as_ref().is_empty(),
"the delivered frame must carry the decoded planes",
);
let after = dec.send_packet(&packet(1));
assert!(
!matches!(after, Err(VideoDecodeError::FramePending)),
"with the seat free the send must reach the seam, got {after:?}",
);
},
);
}
#[test]
fn a_parked_recovery_frame_still_clears_the_resync_guard() {
use crate::{CarrierVideoStreamDecoder, View, boundary::video_packet_from_ffmpeg_in};
use mediadecode::decoder::VideoStreamDecoder;
crate::fault_subprocess::in_subprocess(
"video::tests::a_parked_recovery_frame_still_clears_the_resync_guard",
|| {
let (w, h) = (128u32, 96u32);
let clip = encode_synthetic_clip(w, h, 24, 6);
let second_key = nth_keyframe(&clip, 2);
let third_key = nth_keyframe(&clip, 3);
let fail_at = second_key + 2;
assert!(fail_at < third_key && !clip.packets[fail_at].is_key());
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = CarrierVideoStreamDecoder::<View>::from_hw_inner_for_test(
Box::new(FakeHw::failing(
w,
h,
fail_at,
fail_at,
FailShape::PostCommit,
)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
let packet = |index: usize| {
video_packet_from_ffmpeg_in(
clip.packets[index].clone(),
tb,
crate::PacketLimits::default(),
)
.expect("a wrappable payload")
.expect("packet has a buffer")
};
let mut dst = crate::boundary::empty_video_frame();
for index in 0..=fail_at {
dec.send_packet(&packet(index)).expect("send_packet");
}
assert!(
dec.is_software(),
"the mid-GOP failure fell back to software"
);
assert!(dec.degraded_resync_pending_for_test(), "the gap is open");
let drain = |dec: &mut CarrierVideoStreamDecoder<View>,
dst: &mut crate::VideoFrame|
-> bool { dec.receive_frame(dst).is_ok() };
while drain(&mut dec, &mut dst) {}
for index in (fail_at + 1)..third_key {
dec.send_packet(&packet(index)).expect("send_packet");
while drain(&mut dec, &mut dst) {}
}
dec
.send_packet(&packet(third_key))
.expect("send the keyframe");
assert!(
dec.degraded_keyframe_seen_for_test(),
"the keyframe crossed the gap and is the resync anchor",
);
assert!(
dec.degraded_resync_pending_for_test(),
"no post-keyframe frame has been delivered yet",
);
let mut parked = false;
for attempt in 0..64 {
crate::fault_subprocess::cap_ffmpeg_allocations(16);
let got = dec.receive_frame(&mut dst);
crate::fault_subprocess::uncap_ffmpeg_allocations();
match got {
Err(VideoDecodeError::Convert(e)) if e.parks_in_decode() => {
parked = true;
break;
}
Ok(()) => {
assert!(
dec.degraded_resync_pending_for_test(),
"the guard cleared before the parked delivery — nothing left to test",
);
}
Err(_) => {
let index = third_key + 1 + attempt;
if index >= clip.packets.len() {
break;
}
dec.send_packet(&packet(index)).expect("send_packet");
}
}
}
assert!(parked, "the ceiling must park the recovery frame");
assert!(
dec.degraded_resync_pending_for_test(),
"a parked frame has not been delivered, so the guard still stands",
);
dec
.receive_frame(&mut dst)
.expect("the parked recovery frame");
assert!(
!dec.degraded_resync_pending_for_test(),
"the retried delivery must clear the keyframe-anchored resync guard",
);
dec.send_eof().expect("send_eof");
loop {
match dec.receive_frame(&mut dst) {
Ok(()) => {}
Err(VideoDecodeError::PostCommitNeverResynced(p)) => {
panic!("false escalation after a resync that did happen: {p:?}");
}
Err(_) => break,
}
}
},
);
}