use std::{
collections::BTreeMap,
hash::{DefaultHasher, Hash, Hasher},
path::PathBuf,
};
use mediadecode::{
PixelFormat, Received, Sent, Timebase,
decoder::VideoStreamDecoder,
demuxer::{DemuxedPacket, Demuxer, TrackKind},
packet::PacketFlags,
};
use mediadecode_ffmpeg::{
DecodePath, DecoderLimits, DecoderOptions, DemuxerName, DemuxerOpen, Discard,
FfmpegOwnedDemuxer as FfmpegDemuxer, FfmpegOwnedVideoStreamDecoder as FfmpegVideoStreamDecoder,
OwnedVideoFrame as VideoFrame, OwnedVideoPacket as VideoPacket, VideoStreamDecoderOpen,
empty_owned_video_frame,
};
const FIXTURES_ENV: &str = "VIDEO_FIXTURES";
const DEFAULT_FIXTURES: &str = "/Volumes/rollroll/fixtures/video-fixtures";
fn fixture(name: &str) -> Option<PathBuf> {
let root =
std::env::var_os(FIXTURES_ENV).map_or_else(|| PathBuf::from(DEFAULT_FIXTURES), PathBuf::from);
let path = root.join("h264").join(name);
if path.is_file() {
Some(path)
} else {
eprintln!(
"skip: the fixture h264/{name} is absent at {} — set {FIXTURES_ENV} to a checkout of \
findit-studio/video-fixtures",
path.display()
);
None
}
}
fn declared() -> DemuxerOpen {
DemuxerOpen::new().with_plaintext_demuxers(vec![
DemuxerName::registered("mov,mp4,m4a,3gp,3g2,mj2").expect("the ISOBMFF demuxer"),
])
}
fn video_packets(path: &PathBuf) -> (Timebase, ffmpeg_next::codec::Parameters, Vec<VideoPacket>) {
video_packets_with(path, declared())
}
fn video_packets_with(
path: &PathBuf,
request: DemuxerOpen,
) -> (Timebase, ffmpeg_next::codec::Parameters, Vec<VideoPacket>) {
let mut demuxer = FfmpegDemuxer::open_from(path, request).expect("open the fixture");
let index = demuxer
.tracks()
.iter()
.position(|track| track.kind() == TrackKind::Video)
.expect("a video track");
let track = &demuxer.tracks()[index];
let timebase = track.timebase();
let parameters = track.extra().clone_parameters().expect("parameters");
let mut packets = Vec::new();
while let Some(packet) = demuxer.next_packet().expect("read") {
if let DemuxedPacket::Video(video) = packet
&& video.track().get() == index
{
packets.push(video.into_packet());
}
}
(timebase, parameters, packets)
}
fn disposable(packet: &VideoPacket) -> bool {
packet.flags().contains(PacketFlags::DISPOSABLE)
}
#[test]
fn the_fx3_clip_marks_sixteen_of_its_twenty_four_pictures_disposable() {
let Some(path) = fixture("12_sony_fx3_xavc.mp4") else {
return;
};
let (_, _, packets) = video_packets(&path);
assert_eq!(packets.len(), 24);
let marked = packets.iter().filter(|packet| disposable(packet)).count();
assert_eq!(marked, 16, "the bitstream's two in three");
let (_, _, unread) = video_packets_with(&path, DemuxerOpen::new());
assert_eq!(unread.len(), 24);
assert!(
unread.iter().all(|packet| !disposable(packet)),
"by default, the container's marks alone: none"
);
for packet in packets
.iter()
.filter(|packet| packet.flags().contains(PacketFlags::KEY))
{
assert!(!disposable(packet), "a keyframe marked disposable");
}
}
#[test]
fn the_hlg_clip_marks_one_hundred_and_eighty_of_its_two_hundred_and_seventy_one() {
let Some(path) = fixture("13_sony_hlg_xavc.mp4") else {
return;
};
let (_, _, packets) = video_packets(&path);
assert_eq!(packets.len(), 271);
assert_eq!(
packets
.iter()
.filter(|packet| packet.flags().contains(PacketFlags::DISCARD))
.count(),
20
);
assert_eq!(
packets.iter().filter(|packet| disposable(packet)).count(),
180
);
}
fn picture_hash(frame: &VideoFrame) -> u64 {
assert_eq!(
*frame.pixel_format(),
PixelFormat::Yuv422p10Le,
"the fixture's format"
);
let (width, height) = (frame.width() as usize, frame.height() as usize);
let mut hasher = DefaultHasher::new();
for (index, plane) in frame.planes().iter().enumerate() {
let row = if index == 0 { width * 2 } else { width };
let stride = plane.stride() as usize;
let data: &[u8] = plane.data_ref().as_ref();
for line in 0..height {
data[line * stride..line * stride + row].hash(&mut hasher);
}
}
hasher.finish()
}
fn session_pictures<T>(
request: VideoStreamDecoderOpen,
timebase: Timebase,
parameters: &ffmpeg_next::codec::Parameters,
packets: &[&VideoPacket],
mut read: impl FnMut(&VideoFrame) -> T,
) -> BTreeMap<i64, T> {
let mut decoder = FfmpegVideoStreamDecoder::open_from(
request,
parameters.clone(),
timebase,
DecoderLimits::default(),
)
.expect("open the session");
assert_eq!(decoder.options(), request.options());
let mut frame = empty_owned_video_frame();
let mut pictures = BTreeMap::new();
let mut take = |decoder: &mut FfmpegVideoStreamDecoder, frame: &mut VideoFrame| {
while let Received::Produced = decoder.receive_frame(frame).expect("decode cleanly") {
let pts = frame.pts().expect("a picture's pts").pts();
assert!(
pictures.insert(pts, read(frame)).is_none(),
"two pictures at {pts}"
);
}
};
for packet in packets {
loop {
match decoder.send_packet(packet).expect("decode cleanly") {
Sent::Accepted => break,
Sent::MustDrain => take(&mut decoder, &mut frame),
}
}
take(&mut decoder, &mut frame);
}
while let Sent::MustDrain = decoder.send_eof().expect("decode cleanly") {
take(&mut decoder, &mut frame);
}
take(&mut decoder, &mut frame);
pictures
}
fn decode(
timebase: Timebase,
parameters: &ffmpeg_next::codec::Parameters,
packets: &[&VideoPacket],
options: DecoderOptions,
) -> BTreeMap<i64, u64> {
session_pictures(
VideoStreamDecoderOpen::new()
.with_path(DecodePath::Software)
.with_options(options),
timebase,
parameters,
packets,
picture_hash,
)
}
#[test]
fn the_admitted_packets_alone_decode_exactly_the_reference_pictures() {
let Some(path) = fixture("12_sony_fx3_xavc.mp4") else {
return;
};
let (timebase, parameters, packets) = video_packets(&path);
let every: Vec<&VideoPacket> = packets.iter().collect();
let admitted: Vec<&VideoPacket> = packets
.iter()
.filter(|packet| !disposable(packet))
.collect();
assert_eq!(admitted.len(), 8);
let whole = decode(timebase, ¶meters, &every, DecoderOptions::new());
assert_eq!(whole.len(), 24, "every picture of the full decode");
let references = decode(timebase, ¶meters, &admitted, DecoderOptions::new());
let expected: Vec<i64> = {
let mut pts: Vec<i64> = admitted
.iter()
.map(|packet| packet.pts().expect("a packet's pts").pts())
.collect();
pts.sort_unstable();
pts
};
assert_eq!(references.keys().copied().collect::<Vec<_>>(), expected);
for (pts, hash) in &references {
assert_eq!(
whole.get(pts),
Some(hash),
"the picture at {pts} decoded otherwise"
);
}
}
#[test]
fn ffmpegs_non_reference_skip_keeps_the_same_eight_pictures() {
let Some(path) = fixture("12_sony_fx3_xavc.mp4") else {
return;
};
let (timebase, parameters, packets) = video_packets(&path);
let every: Vec<&VideoPacket> = packets.iter().collect();
let admitted: Vec<&VideoPacket> = packets
.iter()
.filter(|packet| !disposable(packet))
.collect();
let skipping = decode(
timebase,
¶meters,
&every,
DecoderOptions::new().with_skip_frame(Discard::NonReference),
);
let references = decode(timebase, ¶meters, &admitted, DecoderOptions::new());
assert_eq!(skipping, references);
}
#[cfg(target_vendor = "apple")]
fn delivered(mut decoder: mediadecode_ffmpeg::VideoDecoder, packets: &[VideoPacket]) -> Vec<i64> {
use mediadecode_ffmpeg::{Frame, VideoDecoder};
let mut frame = Frame::empty().expect("a frame");
let mut pictures = Vec::new();
let mut take = |decoder: &mut VideoDecoder, frame: &mut Frame| {
while let Received::Produced = decoder.receive_frame(frame).expect("decode cleanly") {
pictures.push(frame.pts().expect("a picture's pts"));
}
};
for packet in packets {
loop {
match decoder.send_packet(packet).expect("decode cleanly") {
Sent::Accepted => break,
Sent::MustDrain => take(&mut decoder, &mut frame),
}
}
take(&mut decoder, &mut frame);
}
while let Sent::MustDrain = decoder.send_eof().expect("decode cleanly") {
take(&mut decoder, &mut frame);
}
take(&mut decoder, &mut frame);
pictures.sort_unstable();
pictures
}
#[cfg(target_vendor = "apple")]
fn reference_pictures(packets: &[VideoPacket]) -> Vec<i64> {
let mut pts: Vec<i64> = packets
.iter()
.filter(|packet| {
nal_units(packet.data().as_ref())
.into_iter()
.any(|unit| matches!(unit[0] & 0x1F, 1 | 5) && unit[0] & 0x60 != 0)
})
.map(|packet| packet.pts().expect("a packet's pts").pts())
.collect();
pts.sort_unstable();
pts
}
#[cfg(target_vendor = "apple")]
#[test]
fn the_standalone_decoder_skipping_non_references_keeps_the_reference_pictures() {
use mediadecode_ffmpeg::{Backend, VideoDecoder, VideoDecoderOpen};
let Some(path) = fixture("01_pandas_muted.mp4") else {
return;
};
let (timebase, parameters, packets) = video_packets(&path);
assert_eq!(packets.len(), 300);
assert_eq!(
packets.iter().filter(|packet| disposable(packet)).count(),
0,
"the reorder depth is not fixed"
);
let admitted = reference_pictures(&packets);
assert_eq!(admitted.len(), 155, "145 of the 300 non-reference");
let whole = delivered(
VideoDecoder::open_timed(parameters.clone(), timebase).expect("open the decoder"),
&packets,
);
assert_eq!(whole.len(), 300);
let skipping = VideoDecoderOpen::new()
.with_timebase(Some(timebase))
.with_options(DecoderOptions::new().with_skip_frame(Discard::NonReference));
let probing =
VideoDecoder::open_from(skipping, parameters.clone()).expect("open the probing decoder");
assert_eq!(probing.options(), skipping.options());
assert_eq!(delivered(probing, &packets), admitted, "the probe");
let pinned = VideoDecoder::open_from(
skipping.with_backend(Some(Backend::VideoToolbox)),
parameters,
)
.expect("open the pinned decoder");
assert_eq!(pinned.options(), skipping.options());
assert_eq!(delivered(pinned, &packets), admitted, "the pinned backend");
}
#[cfg(target_vendor = "apple")]
#[test]
fn the_session_skipping_non_references_keeps_the_reference_pictures_on_every_path() {
use mediadecode_ffmpeg::Backend;
let Some(path) = fixture("01_pandas_muted.mp4") else {
return;
};
let (timebase, parameters, packets) = video_packets(&path);
let every: Vec<&VideoPacket> = packets.iter().collect();
let admitted = reference_pictures(&packets);
assert_eq!(admitted.len(), 155, "145 of the 300 non-reference");
let skipping = DecoderOptions::new().with_skip_frame(Discard::NonReference);
for path in [
DecodePath::Auto,
DecodePath::AnyHardware,
DecodePath::Hardware(Backend::VideoToolbox),
DecodePath::Software,
] {
let pictures = session_pictures(
VideoStreamDecoderOpen::new()
.with_path(path)
.with_options(skipping),
timebase,
¶meters,
&every,
|_| (),
);
assert_eq!(
pictures.keys().copied().collect::<Vec<_>>(),
admitted,
"{path:?}"
);
}
}
#[cfg(target_vendor = "apple")]
#[test]
fn skipping_every_picture_drains_cleanly_on_every_road() {
use mediadecode_ffmpeg::{Backend, VideoDecoder, VideoDecoderOpen};
let Some(path) = fixture("01_pandas_muted.mp4") else {
return;
};
let (timebase, parameters, packets) = video_packets(&path);
let every: Vec<&VideoPacket> = packets.iter().collect();
let all = DecoderOptions::new().with_skip_frame(Discard::All);
let request = VideoDecoderOpen::new()
.with_timebase(Some(timebase))
.with_options(all);
let probing = VideoDecoder::open_from(request, parameters.clone()).expect("open on the probe");
assert_eq!(delivered(probing, &packets), Vec::<i64>::new(), "the probe");
let pinned = VideoDecoder::open_from(
request.with_backend(Some(Backend::VideoToolbox)),
parameters.clone(),
)
.expect("open on the pin");
assert_eq!(delivered(pinned, &packets), Vec::<i64>::new(), "the pin");
let auto = FfmpegVideoStreamDecoder::open_from(
VideoStreamDecoderOpen::new().with_options(all),
parameters.clone(),
timebase,
DecoderLimits::default(),
)
.expect("open on Auto");
assert!(
!auto.is_software(),
"Auto probes hardware under skip_frame all, as under every level"
);
for path in [
DecodePath::Auto,
DecodePath::AnyHardware,
DecodePath::Hardware(Backend::VideoToolbox),
DecodePath::Software,
] {
let pictures = session_pictures(
VideoStreamDecoderOpen::new()
.with_path(path)
.with_options(all),
timebase,
¶meters,
&every,
|_| (),
);
assert!(pictures.is_empty(), "{path:?}: {} pictures", pictures.len());
}
}
#[cfg(target_vendor = "apple")]
fn nal_units(packet: &[u8]) -> Vec<&[u8]> {
let mut units = Vec::new();
let mut rest = packet;
while !rest.is_empty() {
let (length, tail) = rest.split_first_chunk::<4>().expect("a four-byte length");
let length = u32::from_be_bytes(*length) as usize;
assert!(
length > 0 && length <= tail.len(),
"a length of {length} in a packet of {}",
packet.len()
);
let (unit, tail) = tail.split_at(length);
units.push(unit);
rest = tail;
}
units
}
#[cfg(target_vendor = "apple")]
fn rbsp(payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(payload.len());
let mut zeros = 0;
for &byte in payload {
if zeros >= 2 && byte == 3 {
zeros = 0;
continue;
}
zeros = if byte == 0 { zeros + 1 } else { 0 };
out.push(byte);
}
out
}
#[cfg(target_vendor = "apple")]
fn ue(bits: &[u8], at: &mut usize) -> u32 {
let bit = |at: usize| (bits[at / 8] >> (7 - at % 8)) & 1;
let mut zeros = 0;
while bit(*at) == 0 {
zeros += 1;
*at += 1;
}
*at += 1;
let mut value = 0_u32;
for _ in 0..zeros {
value = (value << 1) | u32::from(bit(*at));
*at += 1;
}
(1 << zeros) - 1 + value
}
#[cfg(target_vendor = "apple")]
fn slice_type(unit: &[u8]) -> u32 {
let bits = rbsp(&unit[1..]);
let mut at = 0;
let _first_mb_in_slice = ue(&bits, &mut at);
ue(&bits, &mut at)
}
#[cfg(target_vendor = "apple")]
fn intra_slice(unit: &[u8]) -> bool {
matches!(slice_type(unit) % 5, 2 | 4)
}
#[cfg(target_vendor = "apple")]
fn non_reference_alone(packet: &[u8]) -> bool {
nal_units(packet)
.into_iter()
.all(|unit| !matches!(unit[0] & 0x1F, 1..=5) || unit[0] & 0x60 == 0)
}
#[cfg(target_vendor = "apple")]
fn b_pictures_alone(packet: &[u8]) -> bool {
nal_units(packet)
.into_iter()
.all(|unit| !matches!(unit[0] & 0x1F, 1 | 5) || slice_type(unit) % 5 == 1)
}
#[cfg(target_vendor = "apple")]
fn recovery_point(unit: &[u8]) -> bool {
let bytes = rbsp(&unit[1..]);
let coded = |at: &mut usize| {
let mut value = 0_usize;
while bytes[*at] == 0xFF {
value += 255;
*at += 1;
}
value += usize::from(bytes[*at]);
*at += 1;
value
};
let mut at = 0;
while at < bytes.len() && bytes[at] != 0x80 {
let payload_type = coded(&mut at);
let size = coded(&mut at);
if payload_type == 6 {
return true;
}
at += size;
}
false
}
#[cfg(target_vendor = "apple")]
fn no_key_picture(packet: &[u8]) -> bool {
nal_units(packet)
.into_iter()
.all(|unit| match unit[0] & 0x1F {
5 => false,
6 => !recovery_point(unit),
_ => true,
})
}
#[cfg(target_vendor = "apple")]
fn no_intra_picture(packet: &[u8]) -> bool {
nal_units(packet)
.into_iter()
.all(|unit| !matches!(unit[0] & 0x1F, 1 | 5) || !intra_slice(unit))
}
#[cfg(target_vendor = "apple")]
#[derive(Clone, Copy, Debug)]
enum Feed {
WithinTheProbe,
PastTheProbe,
}
#[cfg(target_vendor = "apple")]
fn a_stream_with_no_picture_to_give_ends_cleanly(
skip: Discard,
keep: fn(&[u8]) -> bool,
feed: Feed,
) {
use mediadecode_ffmpeg::{VideoDecoder, VideoDecoderOpen};
let Some(path) = fixture("01_pandas_muted.mp4") else {
return;
};
let (timebase, parameters, packets) = video_packets(&path);
let mut kept: Vec<VideoPacket> = packets
.into_iter()
.filter(|packet| keep(packet.data().as_ref()))
.collect();
assert!(!kept.is_empty(), "{skip}: no packet kept");
match feed {
Feed::WithinTheProbe => kept.truncate(120),
Feed::PastTheProbe => {
let fragment = kept.clone();
while kept.len() <= 256 {
kept.extend(fragment.iter().cloned());
}
}
}
let options = DecoderOptions::new().with_skip_frame(skip);
let request = VideoDecoderOpen::new()
.with_timebase(Some(timebase))
.with_options(options);
let probing = VideoDecoder::open_from(request, parameters.clone()).expect("open on the probe");
assert_eq!(
delivered(probing, &kept),
Vec::<i64>::new(),
"{skip}, {feed:?}: the probe"
);
let every: Vec<&VideoPacket> = kept.iter().collect();
let pictures = session_pictures(
VideoStreamDecoderOpen::new()
.with_path(DecodePath::AnyHardware)
.with_options(options),
timebase,
¶meters,
&every,
|_| (),
);
assert!(pictures.is_empty(), "{skip}, {feed:?}: AnyHardware");
let mut auto = FfmpegVideoStreamDecoder::open_from(
VideoStreamDecoderOpen::new().with_options(options),
parameters.clone(),
timebase,
DecoderLimits::default(),
)
.expect("open on Auto");
assert!(
!auto.is_software(),
"{skip}, {feed:?}: Auto probes hardware under every level"
);
let mut frame = empty_owned_video_frame();
let mut shown = 0;
let mut take = |auto: &mut FfmpegVideoStreamDecoder, frame: &mut VideoFrame| {
while let Received::Produced = auto.receive_frame(frame).expect("decode cleanly") {
shown += 1;
}
};
for packet in &kept {
loop {
match auto.send_packet(packet).expect("decode cleanly") {
Sent::Accepted => break,
Sent::MustDrain => take(&mut auto, &mut frame),
}
}
take(&mut auto, &mut frame);
}
while let Sent::MustDrain = auto.send_eof().expect("decode cleanly") {
take(&mut auto, &mut frame);
}
take(&mut auto, &mut frame);
assert_eq!(shown, 0, "{skip}, {feed:?}: Auto");
assert!(
!auto.is_software(),
"{skip}, {feed:?}: Auto ends on the hardware decoder that completed"
);
}
#[cfg(target_vendor = "apple")]
#[test]
fn a_stream_with_no_key_picture_ends_cleanly_under_nonkey() {
a_stream_with_no_picture_to_give_ends_cleanly(
Discard::NonKey,
no_key_picture,
Feed::WithinTheProbe,
);
}
#[cfg(target_vendor = "apple")]
#[test]
fn a_stream_with_no_intra_picture_ends_cleanly_under_nonintra() {
a_stream_with_no_picture_to_give_ends_cleanly(
Discard::NonIntra,
no_intra_picture,
Feed::WithinTheProbe,
);
}
#[cfg(target_vendor = "apple")]
#[test]
fn a_probe_that_fills_with_no_picture_commits_under_nonkey_and_nonintra() {
a_stream_with_no_picture_to_give_ends_cleanly(
Discard::NonKey,
no_key_picture,
Feed::PastTheProbe,
);
a_stream_with_no_picture_to_give_ends_cleanly(
Discard::NonIntra,
no_intra_picture,
Feed::PastTheProbe,
);
}
#[cfg(target_vendor = "apple")]
#[test]
fn a_stream_of_non_reference_pictures_alone_ends_cleanly_under_nonref() {
for feed in [Feed::WithinTheProbe, Feed::PastTheProbe] {
a_stream_with_no_picture_to_give_ends_cleanly(Discard::NonReference, non_reference_alone, feed);
}
}
#[cfg(target_vendor = "apple")]
#[test]
fn a_stream_of_b_pictures_alone_ends_cleanly_under_bidir() {
for feed in [Feed::WithinTheProbe, Feed::PastTheProbe] {
a_stream_with_no_picture_to_give_ends_cleanly(Discard::Bidirectional, b_pictures_alone, feed);
}
}
#[cfg(target_vendor = "apple")]
#[test]
fn every_picture_skipped_ends_cleanly_under_all_within_and_past_the_probe_size() {
for feed in [Feed::WithinTheProbe, Feed::PastTheProbe] {
a_stream_with_no_picture_to_give_ends_cleanly(Discard::All, |_| true, feed);
}
}
#[cfg(target_vendor = "apple")]
fn pre_rolled(packets: &[VideoPacket], discarded: usize) -> Vec<VideoPacket> {
packets
.iter()
.enumerate()
.map(|(index, packet)| {
let flags = packet.flags();
let packet = packet.clone();
if index < discarded {
packet.with_flags(flags | PacketFlags::DISCARD)
} else {
packet
}
})
.collect()
}
#[cfg(target_vendor = "apple")]
fn auto_pictures(
parameters: &ffmpeg_next::codec::Parameters,
timebase: Timebase,
options: DecoderOptions,
packets: &[VideoPacket],
) -> (Vec<i64>, bool) {
let mut auto = FfmpegVideoStreamDecoder::open_from(
VideoStreamDecoderOpen::new().with_options(options),
parameters.clone(),
timebase,
DecoderLimits::default(),
)
.expect("open on Auto");
assert!(!auto.is_software(), "Auto probes hardware");
let mut frame = empty_owned_video_frame();
let mut pictures = Vec::new();
let mut take = |auto: &mut FfmpegVideoStreamDecoder, frame: &mut VideoFrame| {
while let Received::Produced = auto.receive_frame(frame).expect("decode cleanly") {
pictures.push(frame.pts().expect("a picture's pts").pts());
}
};
for packet in packets {
loop {
match auto.send_packet(packet).expect("decode cleanly") {
Sent::Accepted => break,
Sent::MustDrain => take(&mut auto, &mut frame),
}
}
take(&mut auto, &mut frame);
}
while let Sent::MustDrain = auto.send_eof().expect("decode cleanly") {
take(&mut auto, &mut frame);
}
take(&mut auto, &mut frame);
pictures.sort_unstable();
(pictures, auto.is_software())
}
#[cfg(target_vendor = "apple")]
#[test]
fn a_fragment_of_pre_roll_alone_ends_cleanly_under_default_and_nothing() {
use mediadecode_ffmpeg::{VideoDecoder, VideoDecoderOpen};
let Some(path) = fixture("01_pandas_muted.mp4") else {
return;
};
let (timebase, parameters, packets) = video_packets(&path);
assert!(packets.len() > 256);
for skip in [Discard::Default, Discard::Nothing] {
let options = DecoderOptions::new().with_skip_frame(skip);
for count in [120, packets.len()] {
let fragment = pre_rolled(&packets[..count], count);
let request = VideoDecoderOpen::new()
.with_timebase(Some(timebase))
.with_options(options);
let probing =
VideoDecoder::open_from(request, parameters.clone()).expect("open on the probe");
assert_eq!(
delivered(probing, &fragment),
Vec::<i64>::new(),
"{skip}, {count}: the probe"
);
let every: Vec<&VideoPacket> = fragment.iter().collect();
let pictures = session_pictures(
VideoStreamDecoderOpen::new()
.with_path(DecodePath::AnyHardware)
.with_options(options),
timebase,
¶meters,
&every,
|_| (),
);
assert!(pictures.is_empty(), "{skip}, {count}: AnyHardware");
let (pictures, software) = auto_pictures(¶meters, timebase, options, &fragment);
assert!(pictures.is_empty(), "{skip}, {count}: Auto");
assert!(!software, "{skip}, {count}: Auto stays on hardware");
}
}
}
#[cfg(target_vendor = "apple")]
#[test]
fn pre_roll_past_the_probe_then_pictures_keeps_the_hardware_decoder() {
use mediadecode_ffmpeg::{VideoDecoder, VideoDecoderOpen};
let Some(path) = fixture("01_pandas_muted.mp4") else {
return;
};
let (timebase, parameters, packets) = video_packets(&path);
let discarded = 270;
assert!(discarded > 256 && discarded < packets.len());
let stream = pre_rolled(&packets, discarded);
let mut shown: Vec<i64> = packets[discarded..]
.iter()
.map(|packet| packet.pts().expect("a packet's pts").pts())
.collect();
shown.sort_unstable();
let request = VideoDecoderOpen::new().with_timebase(Some(timebase));
let probing = VideoDecoder::open_from(request, parameters.clone()).expect("open on the probe");
assert_eq!(delivered(probing, &stream), shown, "the probe");
let every: Vec<&VideoPacket> = stream.iter().collect();
let pictures = session_pictures(
VideoStreamDecoderOpen::new().with_path(DecodePath::AnyHardware),
timebase,
¶meters,
&every,
|_| (),
);
assert_eq!(
pictures.into_keys().collect::<Vec<_>>(),
shown,
"AnyHardware"
);
let (pictures, software) = auto_pictures(¶meters, timebase, DecoderOptions::new(), &stream);
assert_eq!(pictures, shown, "Auto");
assert!(!software, "Auto keeps the hardware decoder");
}