use super::*;
use mediadecode::decoder::VideoStreamDecoder;
use std::{
num::NonZeroI32,
sync::{Arc, Mutex},
};
use crate::{FfmpegBytes, FfmpegOwnedVideoStreamDecoder as FfmpegVideoStreamDecoder};
fn written_video(format: ffmpeg_next::format::Pixel, width: u32, height: u32) -> frame::Video {
let mut av = frame::Video::new(format, width, height);
unsafe {
for buf in (*av.as_mut_ptr()).buf {
if !buf.is_null() {
core::ptr::write_bytes((*buf).data, 0, (*buf).size);
}
}
}
av
}
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 = written_video(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(Debug, Clone, Copy, PartialEq, Eq)]
struct Storage {
data: usize,
buffer: usize,
}
impl Storage {
fn of_packet(packet: &Packet) -> Self {
use ffmpeg_next::packet::Ref;
unsafe {
let raw = packet.as_ptr();
Self {
data: (*raw).data as usize,
buffer: (*raw).buf.as_ref().map_or(0, |held| held.buffer as usize),
}
}
}
fn of_carrier(carrier: &crate::FfmpegBuffer) -> Self {
let held = unsafe { carrier.as_av_buffer_ref().as_ref() };
Self {
data: carrier.as_ref().as_ptr() as usize,
buffer: held.map_or(0, |held| held.buffer as usize),
}
}
}
fn demuxed_view_packets(clip: &SyntheticClip) -> (Vec<crate::VideoPacket>, Timebase) {
use ffmpeg_next as ff;
use std::sync::atomic::{AtomicUsize, Ordering};
static NEXT: AtomicUsize = AtomicUsize::new(0);
let path = std::env::temp_dir().join(format!(
"mediadecode-view-clip-{}-{}.mkv",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed),
));
let encoder_tb = ff::Rational::new(1, 25);
{
let mut output = ff::format::output(&path).expect("a Matroska output");
{
let mut stream = output.add_stream(ff::codec::Id::MPEG4).expect("a stream");
stream.set_parameters(clip.parameters.clone());
stream.set_time_base(encoder_tb);
}
output.write_header().expect("the header");
let stream_tb = output.stream(0).expect("the stream").time_base();
for packet in &clip.packets {
let mut packet = packet.clone();
packet.set_stream(0);
packet.rescale_ts(encoder_tb, stream_tb);
packet.write_interleaved(&mut output).expect("a packet");
}
output.write_trailer().expect("the trailer");
}
let mut demuxer = crate::FfmpegDemuxer::open(&path).expect("open the clip");
let tb = demuxer.tracks_impl()[0].timebase();
let mut packets = Vec::new();
while let Some(packet) = demuxer.next_packet_impl().expect("a packet or the end") {
if let mediadecode::demuxer::DemuxedPacket::Video(packet) = packet {
packets.push(packet.into_parts().1);
}
}
drop(demuxer);
let _ = std::fs::remove_file(&path);
assert_eq!(
packets.len(),
clip.packets.len(),
"every packet comes back from the container",
);
(packets, tb)
}
struct FakeHw {
width: u32,
height: u32,
doom_from_send: usize,
fail_at_send: usize,
sends: usize,
queued: VecDeque<frame::Video>,
history: Vec<Packet>,
submitted: Arc<Mutex<Vec<Storage>>>,
fail_at_receive: bool,
fail_at_eof: bool,
}
impl FakeHw {
fn inert() -> Self {
Self {
width: 0,
height: 0,
doom_from_send: usize::MAX,
fail_at_send: usize::MAX,
sends: 0,
queued: VecDeque::new(),
history: Vec::new(),
submitted: Arc::default(),
fail_at_receive: false,
fail_at_eof: false,
}
}
fn failing(width: u32, height: u32, doom_from_send: usize, fail_at_send: usize) -> Self {
Self {
width,
height,
doom_from_send,
fail_at_send,
sends: 0,
queued: VecDeque::new(),
history: Vec::new(),
submitted: Arc::default(),
fail_at_receive: false,
fail_at_eof: false,
}
}
fn failing_at_receive(width: u32, height: u32) -> Self {
let mut hw = Self::failing(width, height, 0, usize::MAX);
hw.fail_at_receive = true;
hw
}
fn failing_at_eof(width: u32, height: u32) -> Self {
let mut hw = Self::failing(width, height, 0, usize::MAX);
hw.fail_at_eof = true;
hw
}
fn never_failing(width: u32, height: u32) -> Self {
Self::failing(width, height, usize::MAX, usize::MAX)
}
fn submitted(&self) -> Arc<Mutex<Vec<Storage>>> {
Arc::clone(&self.submitted)
}
}
impl HwInner for FakeHw {
unsafe fn send_packet(&mut self, packet: &Packet) -> Result<Sent, Error> {
self
.submitted
.lock()
.expect("no lane panics while holding the record")
.push(Storage::of_packet(packet));
let idx = self.sends;
self.sends += 1;
if idx == self.fail_at_send {
return Err(Error::AllBackendsFailed(
crate::error::AllBackendsFailed::new(Vec::new(), std::mem::take(&mut self.history)),
));
}
if let Ok(cloned) = unsafe { crate::boundary::try_clone_packet(packet) } {
self.history.push(cloned);
}
if idx < self.doom_from_send {
let mut av = written_video(ffmpeg_next::format::Pixel::YUV420P, self.width, self.height);
av.set_pts(packet.pts());
self.queued.push_back(av);
}
Ok(Sent::Accepted)
}
fn receive_frame(&mut self, frame: &mut Frame) -> Result<Received, 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) => {
*unsafe { frame.as_inner_mut() } = av;
Ok(Received::Produced)
}
None => Ok(Received::NeedsInput),
}
}
fn send_eof(&mut self) -> Result<Sent, Error> {
if self.fail_at_eof {
self.fail_at_eof = false;
return Err(Error::AllBackendsFailed(
crate::error::AllBackendsFailed::new(Vec::new(), std::mem::take(&mut self.history)),
));
}
Ok(Sent::Accepted)
}
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 {
unsafe fn send_packet(&mut self, packet: &Packet) -> Result<Sent, Error> {
self.queued.push_back(packet.pts().unwrap_or(0));
Ok(Sent::Accepted)
}
fn receive_frame(&mut self, frame: &mut Frame) -> Result<Received, Error> {
match self.queued.pop_front() {
Some(pts) => {
let mut av = written_video(ffmpeg_next::format::Pixel::YUV420P, self.width, self.height);
av.set_pts(Some(pts));
*unsafe { frame.as_inner_mut() } = av;
Ok(Received::Produced)
}
None => Ok(Received::NeedsInput),
}
}
fn send_eof(&mut self) -> Result<Sent, Error> {
Err(Error::AllBackendsFailed(
crate::error::AllBackendsFailed::new(Vec::new(), Vec::new()),
))
}
fn flush(&mut self) -> Result<(), Error> {
self.queued.clear();
Ok(())
}
fn as_video_decoder(&self) -> Option<&VideoDecoder> {
None
}
}
struct FakeHwEofBackpressures;
impl HwInner for FakeHwEofBackpressures {
unsafe fn send_packet(&mut self, _: &Packet) -> Result<Sent, Error> {
Ok(Sent::Accepted)
}
fn receive_frame(&mut self, _: &mut Frame) -> Result<Received, Error> {
Ok(Received::NeedsInput)
}
fn send_eof(&mut self) -> Result<Sent, Error> {
Ok(Sent::MustDrain)
}
fn flush(&mut self) -> Result<(), Error> {
Ok(())
}
fn as_video_decoder(&self) -> Option<&VideoDecoder> {
None
}
}
#[test]
fn back_pressured_eof_does_not_commit_the_eof_transaction() {
let mut dec = unopenable_sw_decoder(Box::new(FakeHwEofBackpressures));
assert!(
!dec.eof_sent_for_test(),
"precondition: eof_sent starts false"
);
for _ in 0..3 {
assert!(
matches!(dec.send_eof(), Ok(Sent::MustDrain)),
"the seam refuses the end until its output is drained",
);
assert!(
!dec.eof_sent_for_test(),
"an end-of-stream that was not taken must not commit the transaction",
);
}
}
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(Received::Produced) => out.push(dst.pts().map(|t| t.pts()).unwrap_or(i64::MIN)),
Ok(Received::NeedsInput | Received::Ended) => break,
Err(e) => panic!("receive_frame: {e:?}"),
}
}
};
for av_pkt in &clip.packets {
let vpkt =
unsafe { boundary::video_packet_from_ffmpeg(av_pkt, mediadecode::Timebase::SECONDS) }
.expect("a wrappable payload")
.expect("packet has a buffer");
crate::accepted(dec.send_packet(&vpkt), "send_packet");
drain_frames(dec, &mut out);
}
crate::accepted(dec.send_eof(), "send_eof");
drain_frames(dec, &mut out);
out
}
#[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)),
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 probe_era_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)));
assert!(dec.is_hardware(), "must start on the HW seam");
let mut raw = Packet::copy(&[0u8; 16]);
raw.set_pts(Some(0));
let vpkt = unsafe { boundary::video_packet_from_ffmpeg(&raw, mediadecode::Timebase::SECONDS) }
.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)),
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 =
unsafe { boundary::video_packet_from_ffmpeg(av_pkt, mediadecode::Timebase::SECONDS) }
.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(Received::Produced) => {}
Ok(Received::NeedsInput | Received::Ended) => 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)),
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 =
unsafe { boundary::video_packet_from_ffmpeg(av_pkt, mediadecode::Timebase::SECONDS) }
.expect("a wrappable payload")
.expect("packet has a buffer");
match dec.send_packet(&vpkt) {
Ok(Sent::Accepted | Sent::MustDrain) => {
loop {
match dec.receive_frame(&mut dst) {
Ok(Received::Produced) => {}
Ok(Received::NeedsInput | Received::Ended) => 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 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 =
FfmpegVideoStreamDecoder::open(stream.parameters(), tb, crate::DecoderLimits::default())
.expect("open FX3 decoder");
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) =
unsafe { boundary::video_packet_from_ffmpeg(&packet, mediadecode::Timebase::SECONDS) }
.expect("a wrappable payload")
else {
continue; };
let mut attempts = 0u32;
loop {
match dec.send_packet(&vpkt) {
Ok(Sent::Accepted) => break,
Ok(Sent::MustDrain) => {
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;
}
}
}
obs.note_transition(&dec);
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(Sent::Accepted) => {
if let Err(err) = obs.drain(&mut dec, &mut dst) {
obs.abort = Some(format!("during post-EOF drain: {err}"));
}
}
Ok(Sent::MustDrain) => {
obs.abort = Some("send_eof asked for a drain after the feed loop drained".into());
}
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!(
" moved_to_sw = {} (at send #{:?}, after {:?} pictures)",
obs.moved_to_sw, obs.move_send_idx, obs.pictures_before_move
);
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!(" abort = {:?}", obs.abort);
assert!(
obs.started_on_hw,
"expected the probe to open on VideoToolbox; if it opened straight to SW the hardware \
road was never tried on this run"
);
assert!(
obs.moved_to_sw && ended_on_sw,
"expected the probe to fall back to software on the FX3 clip; observed moved={}, \
ended_on_sw={ended_on_sw}, abort={:?}",
obs.moved_to_sw,
obs.abort
);
assert_eq!(
obs.pictures_before_move,
Some(0),
"the fallback must come before the first picture — a later failure would be that \
picture's own error, not a fallback"
);
assert!(
obs.abort.is_none(),
"the drive aborted before EOF on the real H.264 codec: {:?}",
obs.abort
);
assert!(!obs.pts_out.is_empty(), "no frames were delivered at all");
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 replay must not re-emit a frame (no duplicate PTS): {:?}",
obs.pts_out
);
}
struct Fx3Observation {
started_on_hw: bool,
moved_to_sw: bool,
move_send_idx: Option<usize>,
pictures_before_move: Option<usize>,
send_idx: usize,
pts_out: Vec<i64>,
abort: Option<String>,
}
impl Fx3Observation {
fn new(started_on_hw: bool) -> Self {
Self {
started_on_hw,
moved_to_sw: false,
move_send_idx: None,
pictures_before_move: None,
send_idx: 0,
pts_out: Vec::new(),
abort: None,
}
}
fn note_transition(&mut self, dec: &FfmpegVideoStreamDecoder) {
if !self.moved_to_sw && dec.is_software() {
self.moved_to_sw = true;
self.move_send_idx = Some(self.send_idx);
self.pictures_before_move = Some(self.pts_out.len());
eprintln!(
" -> moved to software at/after send #{} ({} pictures delivered before it)",
self.send_idx,
self.pts_out.len()
);
}
}
fn drain(
&mut self,
dec: &mut FfmpegVideoStreamDecoder,
dst: &mut VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, FfmpegBytes>,
) -> Result<(), String> {
loop {
match dec.receive_frame(dst) {
Ok(Received::Produced) => {
let pts = VideoFrame::pts(dst).map(|t| t.pts()).unwrap_or(i64::MIN);
self.pts_out.push(pts);
}
Ok(Received::NeedsInput | Received::Ended) => break,
Err(e) => return Err(format!("{e:?}")),
}
}
Ok(())
}
}
#[test]
fn a_rescued_packet_never_aliases_a_view_carrier() {
use crate::{CarrierVideoStreamDecoder, View};
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 (carriers, tb) = demuxed_view_packets(&clip);
let mut dec = CarrierVideoStreamDecoder::<View>::from_hw_inner_for_test(
Box::new(FakeHw::failing(w, h, 0, fail_at)),
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 vpkt in carriers {
match dec.send_packet(&vpkt) {
Ok(Sent::Accepted | Sent::MustDrain) => {}
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 matches!(dec.receive_frame(&mut dst), Ok(Received::Produced)) {}
}
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 a_view_send_in_the_probe_window_is_zero_copy() {
use crate::{CarrierVideoStreamDecoder, View};
use ffmpeg_next::{ffi::av_buffer_get_ref_count, packet::Ref};
const PADDING: usize = ffmpeg_next::ffi::AV_INPUT_BUFFER_PADDING_SIZE as usize;
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 8, 100);
let fail_at = 4;
let (carriers, tb) = demuxed_view_packets(&clip);
let seam = FakeHw::failing(w, h, 0, fail_at);
let submitted = seam.submitted();
let mut dec = CarrierVideoStreamDecoder::<View>::from_hw_inner_for_test_as(
Box::new(seam),
clip.parameters.clone(),
tb,
DecodePath::AnyHardware,
)
.expect("build a pinned test decoder");
let mut retained: Vec<crate::VideoPacket> = Vec::new();
let mut rescued: Vec<ffmpeg_next::Packet> = Vec::new();
for vpkt in carriers.into_iter().take(fail_at + 1) {
let body = vpkt.data();
assert_eq!(body.origin(), crate::view::Origin::PacketPayload);
let capacity = unsafe { (*body.as_av_buffer_ref()).size };
assert!(
capacity
.checked_sub(body.offset() + body.len())
.is_some_and(|slack| slack >= PADDING),
"the premise: the padding behind the payload",
);
let answer = dec.send_packet(&vpkt);
retained.push(vpkt);
match answer {
Ok(Sent::Accepted) => {}
Err(VideoDecodeError::Decode(Error::AllBackendsFailed(p))) => {
rescued = p.into_unconsumed_packets();
break;
}
other => panic!("send_packet: {other:?}"),
}
}
assert_eq!(
retained.len(),
fail_at + 1,
"the exhaustion arrives on its send"
);
assert_eq!(rescued.len(), fail_at, "the history comes back as recorded");
let carriers: Vec<Storage> = retained
.iter()
.map(|p| Storage::of_carrier(p.data()))
.collect();
let sends = submitted.lock().expect("the seam is done").clone();
assert_eq!(sends.len(), carriers.len(), "one record per send");
for (send, (sent, carrier)) in sends.iter().zip(&carriers).enumerate() {
assert_eq!(
sent, carrier,
"send {send}: the submission must be the carrier's own storage, held \
by a reference to its buffer — a copy here is a second copy",
);
}
for (index, packet) in rescued.iter().enumerate() {
let kept = Storage::of_packet(packet);
assert!(
carriers
.iter()
.all(|carrier| carrier.data != kept.data && carrier.buffer != kept.buffer),
"rescued packet {index} shares a retained carrier's storage — the \
probe must record a copy",
);
let references = unsafe { av_buffer_get_ref_count((*packet.as_ptr()).buf) };
assert_eq!(
references, 1,
"rescued packet {index}: a payload of its own"
);
assert_eq!(
packet.data(),
Some(retained[index].data().as_ref()),
"rescued packet {index}: the bytes it was sent with",
);
}
for (index, carrier) in retained.iter().enumerate() {
let references = unsafe { av_buffer_get_ref_count(carrier.data().as_av_buffer_ref()) };
assert_eq!(
references, 1,
"carrier {index}: nothing kept the submission"
);
}
}
#[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;
mediaio_ffmpeg::test_support::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) =
unsafe { 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;
unsafe { mediaio_ffmpeg::test_support::cap_ffmpeg_allocations(16) };
}
let got = dec.receive_frame(&mut frame);
if capped {
mediaio_ffmpeg::test_support::uncap_ffmpeg_allocations();
}
match got {
Ok(Received::Produced) => {
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;
}
}
Ok(Received::NeedsInput | Received::Ended) => break,
Err(VideoDecodeError::Convert(e)) => {
assert!(capped, "no refusal was asked for here, got {e:?}");
assert!(
crate::decoder::parks_in_decode(&e),
"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",
);
},
);
}
fn eof_with_a_parked_frame(
hw: bool,
) -> (
crate::CarrierVideoStreamDecoder<crate::View>,
SyntheticClip,
Timebase,
) {
use crate::{CarrierVideoStreamDecoder, View, boundary::video_packet_from_ffmpeg_in};
use mediadecode::decoder::VideoStreamDecoder;
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 seam: Box<dyn HwInner> = if hw {
Box::new(FakeHw::never_failing(w, h))
} else {
Box::new(FakeHw::failing(w, h, 0, 2))
};
let mut dec =
CarrierVideoStreamDecoder::<View>::from_hw_inner_for_test(seam, clip.parameters.clone(), tb)
.expect("build test decoder");
let packet = |index: usize| {
unsafe {
video_packet_from_ffmpeg_in(
clip.packets[index].clone(),
tb,
crate::PacketLimits::default(),
)
}
.expect("a wrappable payload")
.expect("packet has a buffer")
};
let mut frame = crate::boundary::empty_video_frame();
if hw {
for index in 0..4 {
crate::accepted(dec.send_packet(&packet(index)), "send_packet");
}
} else {
for index in 0..3 {
crate::accepted(dec.send_packet(&packet(index)), "send_packet");
}
while matches!(dec.receive_frame(&mut frame), Ok(Received::Produced)) {}
for index in 3..clip.packets.len().min(6) {
let pkt = packet(index);
loop {
match dec.send_packet(&pkt).expect("no fault while feeding") {
Sent::Accepted => break,
Sent::MustDrain => {
while matches!(dec.receive_frame(&mut frame), Ok(Received::Produced)) {}
}
}
}
}
}
assert_eq!(dec.is_hardware(), hw, "the intended road");
assert!(
dec.sw_replay_frames_is_empty_for_test(),
"the replay queue must be empty, or the park below lands in it \
instead of the scratch seat this lane is about",
);
crate::accepted(dec.send_eof(), "send_eof");
assert!(
dec.eof_sent_for_test(),
"precondition: the end must be committed for this lane to mean anything",
);
unsafe { mediaio_ffmpeg::test_support::cap_ffmpeg_allocations(16) };
let refused = dec.receive_frame(&mut frame);
mediaio_ffmpeg::test_support::uncap_ffmpeg_allocations();
match refused {
Err(VideoDecodeError::Convert(e)) => assert!(
crate::decoder::parks_in_decode(&e),
"the ceiling must produce a parkable refusal, got {e:?}",
),
other => panic!("expected a parkable refusal after EOF, got {other:?}"),
}
(dec, clip, tb)
}
fn a_post_eof_send_is_a_fault_not_backpressure(hw: bool) {
use crate::boundary::video_packet_from_ffmpeg_in;
use mediadecode::decoder::VideoStreamDecoder;
let (mut dec, clip, tb) = eof_with_a_parked_frame(hw);
let packet = |index: usize| {
unsafe {
video_packet_from_ffmpeg_in(
clip.packets[index].clone(),
tb,
crate::PacketLimits::default(),
)
}
.expect("a wrappable payload")
.expect("packet has a buffer")
};
let is_after_eof = |got: &Result<Sent, VideoDecodeError>| {
matches!(
got,
Err(VideoDecodeError::Decode(Error::Ffmpeg(
ffmpeg_next::Error::Eof
)))
)
};
let sent = dec.send_packet(&packet(4));
assert!(
is_after_eof(&sent),
"a packet after a committed EOF must be the fault even with the seat \
parked — `MustDrain` here promises a retry that can never succeed; got {sent:?}",
);
let eof_again = dec.send_eof();
assert!(
is_after_eof(&eof_again),
"the same for a repeated end-of-stream; got {eof_again:?}",
);
let mut frame = crate::boundary::empty_video_frame();
let mut drained = 0u32;
for _ in 0..64 {
match dec.receive_frame(&mut frame) {
Ok(Received::Produced) => drained += 1,
Ok(Received::NeedsInput | Received::Ended) => break,
Err(e) => panic!("the parked frame must still be deliverable: {e:?}"),
}
}
assert!(drained > 0, "the parked frame was never recovered");
let sent_after = dec.send_packet(&packet(5));
assert!(
is_after_eof(&sent_after),
"draining did not make the offer acceptable, which is exactly why the \
parked answer must not have been `MustDrain`; got {sent_after:?}",
);
assert!(
is_after_eof(&dec.send_eof()),
"and the same for the repeated end-of-stream",
);
dec.flush().expect("flush");
assert!(
!dec.eof_sent_for_test(),
"flush must retract the committed end",
);
crate::accepted(dec.send_packet(&packet(0)), "flush reopened the send side");
}
#[test]
fn a_post_eof_send_is_a_fault_not_backpressure_on_the_hardware_road() {
mediaio_ffmpeg::test_support::in_subprocess(
"video::tests::a_post_eof_send_is_a_fault_not_backpressure_on_the_hardware_road",
|| a_post_eof_send_is_a_fault_not_backpressure(true),
);
}
#[test]
fn a_post_eof_send_is_a_fault_not_backpressure_on_the_software_road() {
mediaio_ffmpeg::test_support::in_subprocess(
"video::tests::a_post_eof_send_is_a_fault_not_backpressure_on_the_software_road",
|| a_post_eof_send_is_a_fault_not_backpressure(false),
);
}
#[test]
fn a_post_eof_frame_time_fallback_never_strands_the_caller_in_needs_input() {
use crate::{CarrierVideoStreamDecoder, View, boundary::video_packet_from_ffmpeg_in};
use mediadecode::decoder::VideoStreamDecoder;
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_at_receive(w, h)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
for av_pkt in &clip.packets {
let pkt =
unsafe { video_packet_from_ffmpeg_in(av_pkt.clone(), tb, crate::PacketLimits::default()) }
.expect("a wrappable payload")
.expect("packet has a buffer");
crate::accepted(dec.send_packet(&pkt), "send_packet");
}
crate::accepted(dec.send_eof(), "send_eof");
assert!(
dec.eof_sent_for_test(),
"precondition: the end must be committed before the fallback fires",
);
let mut frame = crate::boundary::empty_video_frame();
let mut pictures = 0usize;
let mut terminal = false;
for _ in 0..64 {
match dec.receive_frame(&mut frame) {
Ok(Received::Produced) => pictures += 1,
Ok(Received::NeedsInput) => panic!(
"stranded: the decoder asked for input on a session whose end is \
committed, and both send gates refuse — no legal operation can \
satisfy this answer",
),
Ok(Received::Ended) => {
terminal = true;
break;
}
Err(e) => panic!("unexpected fault while draining: {e:?}"),
}
}
assert!(terminal, "the drain never reached a terminal answer");
assert!(dec.is_software(), "the frame-time fallback did commit");
assert_eq!(
pictures,
clip.packets.len(),
"the replayed history comes out whole — the fallback is lossless",
);
let DecodeState::Sw(sw) = &mut dec.state else {
panic!("the software decoder must be the one in place");
};
let mut scratch = alloc_av_video_frame().expect("frame slot");
let raw = sw
.receive_frame(&mut scratch)
.expect_err("a drained decoder produces no further frame");
assert!(
matches!(raw, ffmpeg_next::Error::Eof),
"the software decoder never received the committed end — it answered \
{raw:?}, which reaches a caller as `NeedsInput` and cannot be satisfied",
);
for _ in 0..3 {
assert_eq!(
dec
.receive_frame(&mut frame)
.expect("no fault past the end"),
Received::Ended,
);
}
}
#[test]
fn the_post_eof_fault_is_the_one_the_substrate_gives() {
use crate::{CarrierVideoStreamDecoder, View, boundary::video_packet_from_ffmpeg_in};
use mediadecode::decoder::VideoStreamDecoder;
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, 0, 2)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
for item in clip.packets.iter().take(3) {
let pkt =
unsafe { video_packet_from_ffmpeg_in(item.clone(), tb, crate::PacketLimits::default()) }
.expect("a wrappable payload")
.expect("packet has a buffer");
crate::accepted(dec.send_packet(&pkt), "send_packet");
}
assert!(
dec.is_software(),
"the substrate under test is libavcodec's"
);
let DecodeState::Sw(sw) = &mut dec.state else {
panic!("the software decoder must be the one in the seat");
};
sw.send_eof().expect("the substrate takes the end");
let substrate = sw
.send_packet(&clip.packets[3])
.expect_err("libavcodec must refuse a packet after end-of-stream");
assert!(
matches!(substrate, ffmpeg_next::Error::Eof),
"the substrate's post-EOF refusal moved: got {substrate:?}",
);
let wrapped = unsafe { crate::decoder::software_exit(core::ptr::null(), substrate) };
assert!(
matches!(wrapped, Error::Ffmpeg(ffmpeg_next::Error::Eof)),
"the funnel changed how a post-EOF refusal is wrapped: got {wrapped:?}",
);
let synthesized = CarrierVideoStreamDecoder::<View>::after_eof();
assert!(
matches!(
synthesized,
VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Eof))
),
"the synthesized post-EOF fault drifted from the substrate's: got {synthesized:?}",
);
}
#[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;
mediaio_ffmpeg::test_support::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)),
clip.parameters.clone(),
tb,
)
.expect("build test decoder");
let packet = |index: usize| {
unsafe {
video_packet_from_ffmpeg_in(
clip.packets[index].clone(),
tb,
crate::PacketLimits::default(),
)
}
.expect("a wrappable payload")
.expect("packet has a buffer")
};
crate::accepted(dec.send_packet(&packet(0)), "send_packet");
assert!(dec.is_hardware(), "the seam under test is the hardware one");
let mut frame = crate::boundary::empty_video_frame();
unsafe { mediaio_ffmpeg::test_support::cap_ffmpeg_allocations(16) };
let refused = dec.receive_frame(&mut frame);
mediaio_ffmpeg::test_support::uncap_ffmpeg_allocations();
match refused {
Err(VideoDecodeError::Convert(e)) => assert!(
crate::decoder::parks_in_decode(&e),
"the ceiling must produce a parkable refusal, got {e:?}",
),
other => panic!("expected a parkable refusal, got {other:?}"),
}
assert!(
matches!(dec.send_packet(&packet(1)), Ok(Sent::MustDrain)),
"a send under a parked frame must be told to drain first",
);
assert!(
matches!(dec.send_eof(), Ok(Sent::MustDrain)),
"EOF under a parked frame must be told to drain first too",
);
assert_eq!(
dec.receive_frame(&mut frame).expect("the parked frame"),
Received::Produced,
);
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, Ok(Sent::MustDrain)),
"with the seat free the send must reach the seam, got {after:?}",
);
},
);
}
#[test]
fn the_software_path_opens_without_probing_anything() {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 12, 6);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = FfmpegVideoStreamDecoder::open_as(
clip.parameters.clone(),
tb,
DecoderLimits::default(),
DecodePath::Software,
)
.expect("the software path opens for a stream libavcodec can decode");
assert!(dec.is_software(), "the pin put the session on software");
assert!(!dec.is_hardware());
assert!(
dec.hardware_inner().is_none(),
"there is no hardware decoder in this session to borrow",
);
let mut dst = crate::empty_owned_video_frame();
let mut delivered = 0usize;
for av_pkt in &clip.packets {
let vpkt =
unsafe { boundary::video_packet_from_ffmpeg(av_pkt, mediadecode::Timebase::SECONDS) }
.expect("a wrappable payload")
.expect("packet has a buffer");
crate::accepted(dec.send_packet(&vpkt), "send_packet");
while let Ok(Received::Produced) = dec.receive_frame(&mut dst) {
delivered += 1;
}
}
crate::accepted(dec.send_eof(), "send_eof");
while let Ok(Received::Produced) = dec.receive_frame(&mut dst) {
delivered += 1;
}
assert_eq!(
delivered,
clip.packets.len(),
"the pinned software session decoded every packet of the clip",
);
}
#[test]
fn the_auto_path_is_the_bare_constructor() {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 8, 4);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let bare = FfmpegVideoStreamDecoder::open(clip.parameters.clone(), tb, DecoderLimits::default())
.expect(
"the bare constructor opens: it falls back to software when no backend takes the stream",
);
let named = FfmpegVideoStreamDecoder::open_as(
clip.parameters.clone(),
tb,
DecoderLimits::default(),
DecodePath::Auto,
)
.expect("the Auto arm opens wherever the bare constructor does");
assert_eq!(bare.is_software(), named.is_software());
assert_eq!(bare.is_hardware(), named.is_hardware());
}
#[test]
fn a_hardware_pin_on_an_absent_backend_fails_instead_of_falling_back() {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 8, 4);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let order = crate::backend::probe_order();
let absent = [
Backend::VideoToolbox,
Backend::Vaapi,
Backend::Cuda,
Backend::D3d11va,
]
.into_iter()
.find(|backend| !order.contains(backend))
.expect("no platform probes all four backends");
let refused = FfmpegVideoStreamDecoder::open_as(
clip.parameters.clone(),
tb,
DecoderLimits::default(),
DecodePath::Hardware(absent),
);
assert!(
refused.is_err(),
"a pin to {absent:?} must fail rather than open the software decoder the Auto arm \
would have reached for",
);
}
#[test]
fn a_hardware_pin_reports_its_probes_exhaustion_instead_of_falling_back() {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 12, 6);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let fail_at = 2;
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test_as(
Box::new(FakeHw::failing(w, h, 0, fail_at)),
clip.parameters.clone(),
tb,
DecodePath::Hardware(Backend::VideoToolbox),
)
.expect("build a pinned test decoder");
for index in 0..fail_at {
crate::accepted(dec.send_packet(&pushed(&clip, index)), "send_packet");
}
let refusal = dec
.send_packet(&pushed(&clip, fail_at))
.expect_err("the probe exhausts on this packet, so a refusal must arrive");
let VideoDecodeError::Decode(Error::AllBackendsFailed(p)) = &refusal else {
panic!(
"the pinned session must report the exhaustion with its payload intact, got {refusal:?}"
);
};
assert!(p.origin().is_probe(), "the probe's own exhaustion");
assert_eq!(
p.unconsumed_packets().len(),
fail_at,
"the packets the probe consumed ride back with the refusal",
);
assert!(
dec.is_hardware(),
"the pin holds after the refusal: nothing opened a software decoder behind it",
);
assert!(!dec.is_software());
}
#[test]
fn a_hardware_pin_reports_a_frame_time_exhaustion_too() {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 8, 4);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test_as(
Box::new(FakeHw::failing_at_receive(w, h)),
clip.parameters.clone(),
tb,
DecodePath::Hardware(Backend::VideoToolbox),
)
.expect("build a pinned test decoder");
let vpkt =
unsafe { boundary::video_packet_from_ffmpeg(&clip.packets[0], mediadecode::Timebase::SECONDS) }
.expect("a wrappable payload")
.expect("packet has a buffer");
crate::accepted(dec.send_packet(&vpkt), "send_packet");
let mut dst = crate::empty_owned_video_frame();
let refusal = dec
.receive_frame(&mut dst)
.expect_err("the seam fails the first time a frame is asked for");
assert!(
matches!(
&refusal,
VideoDecodeError::Decode(Error::AllBackendsFailed(_)),
),
"got {refusal:?}",
);
assert!(dec.is_hardware(), "the pin holds on the receive road too");
}
#[test]
fn a_hardware_pin_reports_an_exhaustion_raised_at_eof() {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 8, 4);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test_as(
Box::new(FakeHwEofFails::new(w, h)),
clip.parameters.clone(),
tb,
DecodePath::Hardware(Backend::VideoToolbox),
)
.expect("build a pinned test decoder");
let vpkt =
unsafe { boundary::video_packet_from_ffmpeg(&clip.packets[0], mediadecode::Timebase::SECONDS) }
.expect("a wrappable payload")
.expect("packet has a buffer");
crate::accepted(dec.send_packet(&vpkt), "send_packet");
let refusal = dec
.send_eof()
.expect_err("this seam raises its exhaustion from send_eof");
assert!(
matches!(
&refusal,
VideoDecodeError::Decode(Error::AllBackendsFailed(_)),
),
"got {refusal:?}",
);
assert!(dec.is_hardware(), "the pin holds on the EOF road too");
}
#[test]
fn the_auto_path_still_falls_back_where_a_pin_would_not() {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 12, 6);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let fail_at = 2;
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test_as(
Box::new(FakeHw::failing(w, h, 0, fail_at)),
clip.parameters.clone(),
tb,
DecodePath::Auto,
)
.expect("build an auto test decoder");
let mut pictures = Vec::new();
for index in 0..=fail_at {
crate::accepted(dec.send_packet(&pushed(&clip, index)), "send_packet");
drain_pictures(&mut dec, &mut pictures);
}
assert!(
dec.is_software(),
"the same seam, the same failure, and the Auto arm falls back — which is what makes the \
pin's refusal a choice rather than a breakage",
);
assert_eq!(
pictures,
(0..=fail_at as i64).collect::<Vec<_>>(),
"the probe's packets replayed, then the refused one — nothing lost",
);
}
struct ScriptedHw {
sw: super::SwDecoder,
failing_sends: Vec<(usize, ffmpeg_next::Error)>,
failing_pictures: Vec<(i64, ffmpeg_next::Error)>,
failing_eof: Option<ffmpeg_next::Error>,
sent: usize,
}
impl ScriptedHw {
fn new(parameters: &Parameters) -> Self {
Self {
sw: super::open_sw_decoder(parameters, DecoderLimits::default(), None)
.expect("FFmpeg's own decoder for the stream"),
failing_sends: Vec::new(),
failing_pictures: Vec::new(),
failing_eof: None,
sent: 0,
}
}
fn failing_send(mut self, at: usize, raw: ffmpeg_next::Error) -> Self {
self.failing_sends.push((at, raw));
self
}
fn failing_picture(mut self, pts: i64, raw: ffmpeg_next::Error) -> Self {
self.failing_pictures.push((pts, raw));
self
}
fn failing_eof(mut self, raw: ffmpeg_next::Error) -> Self {
self.failing_eof = Some(raw);
self
}
}
impl HwInner for ScriptedHw {
unsafe fn send_packet(&mut self, packet: &Packet) -> Result<Sent, Error> {
let index = self.sent;
if let Some(&(_, raw)) = self.failing_sends.iter().find(|(at, _)| *at == index) {
self.sent += 1;
return Err(Error::Ffmpeg(raw));
}
match self.sw.send_packet(packet) {
Ok(()) => {
self.sent += 1;
Ok(Sent::Accepted)
}
Err(ffmpeg_next::Error::Other { errno }) if errno == ffmpeg_next::error::EAGAIN => {
Ok(Sent::MustDrain)
}
Err(raw) => {
self.sent += 1;
Err(Error::Ffmpeg(raw))
}
}
}
fn receive_frame(&mut self, frame: &mut Frame) -> Result<Received, Error> {
match self.sw.receive_frame(unsafe { frame.as_inner_mut() }) {
Ok(()) => {
let pts = unsafe { frame.as_inner_mut() }.pts();
match self
.failing_pictures
.iter()
.find(|(at, _)| Some(*at) == pts)
{
Some(&(_, raw)) => Err(Error::Ffmpeg(raw)),
None => Ok(Received::Produced),
}
}
Err(ffmpeg_next::Error::Other { errno }) if errno == ffmpeg_next::error::EAGAIN => {
Ok(Received::NeedsInput)
}
Err(ffmpeg_next::Error::Eof) => Ok(Received::Ended),
Err(raw) => Err(Error::Ffmpeg(raw)),
}
}
fn send_eof(&mut self) -> Result<Sent, Error> {
if let Some(raw) = self.failing_eof.take() {
return Err(Error::Ffmpeg(raw));
}
match self.sw.send_eof() {
Ok(()) => Ok(Sent::Accepted),
Err(ffmpeg_next::Error::Other { errno }) if errno == ffmpeg_next::error::EAGAIN => {
Ok(Sent::MustDrain)
}
Err(raw) => Err(Error::Ffmpeg(raw)),
}
}
fn flush(&mut self) -> Result<(), Error> {
self.sw.flush();
Ok(())
}
fn as_video_decoder(&self) -> Option<&VideoDecoder> {
None
}
}
fn drain_pictures(dec: &mut FfmpegVideoStreamDecoder, pictures: &mut Vec<i64>) {
let mut dst = crate::empty_owned_video_frame();
loop {
match dec.receive_frame(&mut dst) {
Ok(Received::Produced) => pictures.push(dst.pts().map_or(i64::MIN, |t| t.pts())),
Ok(Received::NeedsInput | Received::Ended) => break,
Err(e) => panic!("receive_frame: {e:?}"),
}
}
}
fn pushed(clip: &SyntheticClip, index: usize) -> crate::OwnedVideoPacket {
unsafe {
boundary::video_packet_from_ffmpeg(&clip.packets[index], mediadecode::Timebase::SECONDS)
}
.expect("a wrappable payload")
.expect("packet has a buffer")
}
#[derive(Debug, Default)]
struct Answers {
pictures: Vec<i64>,
failures: Vec<(&'static str, usize, ffmpeg_next::Error)>,
}
fn drain_answers(
dec: &mut FfmpegVideoStreamDecoder,
answers: &mut Answers,
index: usize,
path: DecodePath,
) {
let mut dst = crate::empty_owned_video_frame();
loop {
match dec.receive_frame(&mut dst) {
Ok(Received::Produced) => answers
.pictures
.push(dst.pts().map_or(i64::MIN, |t| t.pts())),
Ok(Received::NeedsInput | Received::Ended) => break,
Err(VideoDecodeError::Decode(Error::Ffmpeg(e))) => {
answers.failures.push(("picture", index, e))
}
Err(other) => panic!(
"{path:?}, packet {index}: a picture's failure is reported as it was minted, got {other:?}"
),
}
}
}
fn feed_through_failures(
dec: &mut FfmpegVideoStreamDecoder,
clip: &SyntheticClip,
path: DecodePath,
) -> Answers {
let mut answers = Answers::default();
for index in 0..clip.packets.len() {
match dec.send_packet(&pushed(clip, index)) {
Ok(Sent::Accepted) => {}
Ok(Sent::MustDrain) => panic!("{path:?}: a decoder drained after every packet pushed back"),
Err(VideoDecodeError::Decode(Error::Ffmpeg(e))) => answers.failures.push(("send", index, e)),
Err(other) => panic!(
"{path:?}, packet {index}: a send's failure is reported as it was minted, got {other:?}"
),
}
assert!(
dec.is_hardware() && !dec.is_software(),
"{path:?}, packet {index}: nothing changes the road after the first picture",
);
drain_answers(dec, &mut answers, index, path);
assert!(
dec.is_hardware() && !dec.is_software(),
"{path:?}, packet {index}: nothing changes the road after the first picture",
);
}
let end = clip.packets.len();
match dec.send_eof() {
Ok(Sent::Accepted) => {}
Ok(Sent::MustDrain) => panic!("{path:?}: a drained decoder pushed back on the end"),
Err(VideoDecodeError::Decode(Error::Ffmpeg(e))) => {
answers.failures.push(("end", end, e));
crate::accepted(dec.send_eof(), "the end, offered again after its failure");
}
Err(other) => panic!("{path:?}: the end's failure is reported as it was minted, got {other:?}"),
}
assert!(
dec.is_hardware() && !dec.is_software(),
"{path:?}, at the end: nothing changes the road after the first picture",
);
drain_answers(dec, &mut answers, end, path);
answers
}
#[test]
fn every_failure_after_the_first_picture_is_its_pictures_own_on_every_hardware_path() {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 12, 4);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let enosys = ffmpeg_next::Error::Other {
errno: libc::ENOSYS,
};
let failing_sends = [
(2usize, ffmpeg_next::Error::External),
(5, enosys),
(9, ffmpeg_next::Error::InvalidData),
];
let failing_pictures = [
(6i64, ffmpeg_next::Error::Unknown),
(10, ffmpeg_next::Error::External),
];
for at in failing_sends
.iter()
.map(|&(at, _)| at)
.chain(failing_pictures.iter().map(|&(pts, _)| pts as usize))
{
assert!(
!clip.packets[at].is_key(),
"the scripted failures sit on P-frames"
);
}
for path in [
DecodePath::Auto,
DecodePath::AnyHardware,
DecodePath::Hardware(Backend::VideoToolbox),
] {
let seam = failing_sends
.iter()
.fold(ScriptedHw::new(&clip.parameters), |seam, &(at, raw)| {
seam.failing_send(at, raw)
});
let seam = failing_pictures
.iter()
.fold(seam, |seam, &(pts, raw)| seam.failing_picture(pts, raw));
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test_as(
Box::new(seam),
clip.parameters.clone(),
tb,
path,
)
.expect("build test decoder");
let answers = feed_through_failures(&mut dec, &clip, path);
assert_eq!(
answers.pictures,
[0, 1, 3, 4, 7, 8, 11],
"{path:?}: every other picture came out, each next packet reaching libavcodec",
);
assert_eq!(
answers.failures,
[
("send", 2, ffmpeg_next::Error::External),
("send", 5, enosys),
("picture", 6, ffmpeg_next::Error::Unknown),
("send", 9, ffmpeg_next::Error::InvalidData),
("picture", 10, ffmpeg_next::Error::External),
],
"{path:?}: each failure, as itself, on the road and the packet that met it",
);
assert!(
dec.sw_replay_frames_is_empty_for_test(),
"{path:?}: nothing was replayed into anything",
);
}
}
#[derive(Clone, Copy, Debug)]
enum FailureRoad {
Send(usize),
Picture(i64),
Eof,
}
#[test]
fn after_the_first_picture_nothing_changes_the_road_on_any_path() {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 12, 4);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let external = ffmpeg_next::Error::External;
for path in [
DecodePath::Auto,
DecodePath::AnyHardware,
DecodePath::Hardware(Backend::VideoToolbox),
] {
for road in [
FailureRoad::Send(5),
FailureRoad::Picture(5),
FailureRoad::Eof,
] {
let seam = ScriptedHw::new(&clip.parameters);
let (seam, expected) = match road {
FailureRoad::Send(at) => (seam.failing_send(at, external), ("send", at)),
FailureRoad::Picture(pts) => (
seam.failing_picture(pts, external),
("picture", pts as usize),
),
FailureRoad::Eof => (seam.failing_eof(external), ("end", clip.packets.len())),
};
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test_as(
Box::new(seam),
clip.parameters.clone(),
tb,
path,
)
.expect("build test decoder");
let answers = feed_through_failures(&mut dec, &clip, path);
let failed = match road {
FailureRoad::Send(at) => Some(at as i64),
FailureRoad::Picture(pts) => Some(pts),
FailureRoad::Eof => None,
};
let expected_pictures: Vec<i64> = (0..clip.packets.len() as i64)
.filter(|pts| Some(*pts) != failed)
.collect();
assert_eq!(
answers.pictures, expected_pictures,
"{path:?} {road:?}: every other picture came out — the next call reaches libavcodec",
);
assert_eq!(
answers.failures,
[(expected.0, expected.1, external)],
"{path:?} {road:?}: the failure, as itself, on the road that met it",
);
assert!(
dec.is_hardware() && !dec.is_software(),
"{path:?} {road:?}: nothing changes the road after the first picture",
);
assert!(
dec.sw_replay_frames_is_empty_for_test(),
"{path:?} {road:?}: nothing was replayed into anything",
);
}
}
}
#[test]
fn after_a_failure_and_a_flush_the_packets_reach_libavcodec_on_every_hardware_path() {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 12, 4);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let fail_at = 2;
let resume_at = 4;
assert!(
!clip.packets[fail_at].is_key() && clip.packets[resume_at].is_key(),
"the failure sits on a P-frame, and the seek lands on a keyframe",
);
for path in [
DecodePath::Auto,
DecodePath::AnyHardware,
DecodePath::Hardware(Backend::VideoToolbox),
] {
let mut dec = FfmpegVideoStreamDecoder::from_hw_inner_for_test_as(
Box::new(
ScriptedHw::new(&clip.parameters).failing_send(fail_at, ffmpeg_next::Error::External),
),
clip.parameters.clone(),
tb,
path,
)
.expect("build test decoder");
let mut pictures = Vec::new();
for index in 0..fail_at {
crate::accepted(dec.send_packet(&pushed(&clip, index)), "send_packet");
drain_pictures(&mut dec, &mut pictures);
}
let failed = dec.send_packet(&pushed(&clip, fail_at));
assert!(
matches!(
failed,
Err(VideoDecodeError::Decode(Error::Ffmpeg(
ffmpeg_next::Error::External
)))
),
"{path:?}: the send's own failure, as itself, got {failed:?}",
);
dec.flush().expect("flush");
let mut after = Vec::new();
for index in resume_at..clip.packets.len() {
crate::accepted(
dec.send_packet(&pushed(&clip, index)),
"after a flush the packet reaches libavcodec",
);
drain_pictures(&mut dec, &mut after);
}
crate::accepted(dec.send_eof(), "after a flush the end reaches libavcodec");
drain_pictures(&mut dec, &mut after);
assert_eq!(
pictures,
[0, 1],
"{path:?}: the pictures before the failure"
);
assert_eq!(
after,
(resume_at as i64..clip.packets.len() as i64).collect::<Vec<_>>(),
"{path:?}: after a flush the packets reach libavcodec — every picture from the keyframe on",
);
assert!(
dec.is_hardware() && !dec.is_software(),
"{path:?}: nothing changes the road after the first picture",
);
}
}
#[test]
fn any_hardware_refuses_at_open_on_a_platform_with_no_hardware_backend() {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 8, 4);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
match FfmpegVideoStreamDecoder::open_as_in(
clip.parameters.clone(),
tb,
DecoderLimits::default(),
DecodePath::AnyHardware,
&[],
) {
Ok(dec) => panic!(
"AnyHardware refuses at open where no hardware backend exists — it opened a {} session",
if dec.is_software() {
"software"
} else {
"hardware"
},
),
Err(Error::AllBackendsFailed(p)) => {
assert!(p.origin().is_probe(), "the probe's own exhaustion");
assert!(
p.attempts().is_empty(),
"no backend to try, so no attempt: {:?}",
p.attempts()
);
assert!(
p.unconsumed_packets().is_empty(),
"no packet was consumed: none had been sent"
);
}
Err(other) => panic!("expected the probe's exhaustion, got {other:?}"),
}
let auto = FfmpegVideoStreamDecoder::open_as_in(
clip.parameters.clone(),
tb,
DecoderLimits::default(),
DecodePath::Auto,
&[],
)
.expect("Auto opens software where no hardware backend exists");
assert!(
auto.is_software(),
"the control: Auto over the same order is a software session"
);
}
#[track_caller]
fn assert_the_probes_packets_come_back(
dec: &FfmpegVideoStreamDecoder,
refusal: VideoDecodeError,
clip: &SyntheticClip,
taken: usize,
road: &str,
) {
let VideoDecodeError::Decode(Error::AllBackendsFailed(p)) = &refusal else {
panic!("{road}: expected the probe's exhaustion, got {refusal:?}");
};
assert!(p.origin().is_probe(), "{road}: the probe's own exhaustion");
let body = |packet: &Packet| packet.data().unwrap_or_default().to_vec();
let rescued: Vec<Vec<u8>> = p.unconsumed_packets().iter().map(body).collect();
let sent: Vec<Vec<u8>> = clip.packets[..taken].iter().map(body).collect();
assert!(
rescued == sent,
"{road}: the packets the probe took come back, in order ({} rescued, {} sent)",
rescued.len(),
sent.len(),
);
assert!(
dec.sw_replay_frames_is_empty_for_test(),
"{road}: nothing was replayed into anything"
);
}
#[test]
fn any_hardware_hands_back_the_probes_packets_and_never_opens_software() {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 12, 6);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let taken = 3;
let any_hardware = |seam: FakeHw| {
FfmpegVideoStreamDecoder::from_hw_inner_for_test_as(
Box::new(seam),
clip.parameters.clone(),
tb,
DecodePath::AnyHardware,
)
.expect("build an AnyHardware test decoder")
};
let mut dec = any_hardware(FakeHw::failing(w, h, 0, taken));
for index in 0..taken {
crate::accepted(dec.send_packet(&pushed(&clip, index)), "send_packet");
}
let answer = dec.send_packet(&pushed(&clip, taken));
assert!(
dec.is_hardware() && !dec.is_software(),
"on a send: AnyHardware never opens software"
);
let refusal = answer.expect_err("on a send: the probe's exhaustion is reported");
assert_the_probes_packets_come_back(&dec, refusal, &clip, taken, "on a send");
let mut dec = any_hardware(FakeHw::failing_at_receive(w, h));
for index in 0..taken {
crate::accepted(dec.send_packet(&pushed(&clip, index)), "send_packet");
}
let mut dst = crate::empty_owned_video_frame();
let answer = dec.receive_frame(&mut dst);
assert!(
dec.is_hardware() && !dec.is_software(),
"on a picture: AnyHardware never opens software"
);
let refusal = answer.expect_err("on a picture: the probe's exhaustion is reported");
assert_the_probes_packets_come_back(&dec, refusal, &clip, taken, "on a picture");
let mut dec = any_hardware(FakeHw::failing_at_eof(w, h));
for index in 0..taken {
crate::accepted(dec.send_packet(&pushed(&clip, index)), "send_packet");
}
let answer = dec.send_eof();
assert!(
dec.is_hardware() && !dec.is_software(),
"at the end: AnyHardware never opens software"
);
let refusal = answer.expect_err("at the end: the probe's exhaustion is reported");
assert_the_probes_packets_come_back(&dec, refusal, &clip, taken, "at the end");
assert!(
!dec.eof_sent_for_test(),
"a refused end is not a committed one"
);
}
#[test]
fn any_hardware_never_becomes_software_on_this_platform() {
let (w, h) = (64u32, 48u32);
let clip = encode_synthetic_clip(w, h, 8, 4);
let tb = Timebase::new(1, NonZeroI32::new(25).expect("nonzero"));
let mut dec = match FfmpegVideoStreamDecoder::open_as(
clip.parameters.clone(),
tb,
DecoderLimits::default(),
DecodePath::AnyHardware,
) {
Ok(dec) => dec,
Err(Error::AllBackendsFailed(p)) => {
assert!(
p.origin().is_probe() && p.unconsumed_packets().is_empty(),
"refused at open, with no packet consumed"
);
return;
}
Err(other) => panic!("AnyHardware opens or refuses with the probe's exhaustion, got {other:?}"),
};
assert!(
dec.is_hardware() && !dec.is_software(),
"opened on hardware"
);
let mut dst = crate::empty_owned_video_frame();
for index in 0..clip.packets.len() {
let sent = dec.send_packet(&pushed(&clip, index));
assert!(
!dec.is_software(),
"packet {index}: AnyHardware never becomes software"
);
if let Err(VideoDecodeError::Decode(Error::AllBackendsFailed(p))) = &sent {
assert!(p.origin().is_probe(), "an exhaustion here is the probe's");
return;
}
while let Ok(Received::Produced) = dec.receive_frame(&mut dst) {}
assert!(
!dec.is_software(),
"packet {index}: AnyHardware never becomes software"
);
}
}