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, DemuxerName, DemuxerOpen, FfmpegOwnedDemuxer as FfmpegDemuxer,
FfmpegOwnedVideoStreamDecoder as FfmpegVideoStreamDecoder, OwnedVideoFrame as VideoFrame,
OwnedVideoPacket as VideoPacket, 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>(
path: DecodePath,
timebase: Timebase,
parameters: &ffmpeg_next::codec::Parameters,
packets: &[&VideoPacket],
mut read: impl FnMut(&VideoFrame) -> T,
) -> BTreeMap<i64, T> {
let mut decoder =
FfmpegVideoStreamDecoder::open_as(parameters.clone(), timebase, DecoderLimits::default(), path)
.expect("open the session");
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],
) -> BTreeMap<i64, u64> {
session_pictures(
DecodePath::Software,
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);
assert_eq!(whole.len(), 24, "every picture of the full decode");
let references = decode(timebase, ¶meters, &admitted);
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"
);
}
}