mod support;
use std::{cell::RefCell, rc::Rc};
use mediadecode::{
Timestamp,
demuxer::{
AudioTrackPacket, DataTrackPacket, DemuxAttachmentPacket, DemuxedPacket, Demuxer,
SubtitleTrackPacket, TrackIndex, TrackKind, VideoTrackPacket,
},
};
use mediadecode_ffmpeg::{Ffmpeg, FfmpegBuffer, FfmpegDemuxer};
use mediaencode::{
MuxError, MuxRefusal, Muxer, check_decode_order, mediaspine::track::TrackDescription,
};
use support::Corpus;
fn remux<Dm, M>(
demuxer: &mut Dm,
open: impl FnOnce(Vec<TrackDescription<Dm::Adapter, Dm::Buffer, ()>>) -> M,
) where
Dm: Demuxer,
Dm::Error: core::fmt::Debug,
M: Muxer<Adapter = Dm::Adapter, Buffer = Dm::Buffer>,
M::Refusal: core::fmt::Debug,
M::Error: core::fmt::Debug,
{
let mut files: Vec<Option<DemuxAttachmentPacket<Dm::Adapter, Dm::Buffer>>> =
demuxer.tracks().iter().map(|_| None).collect();
let mut first = None;
while let Some(packet) = demuxer.next_packet().expect("a packet or the end") {
match packet {
DemuxedPacket::Attachment(attachment) => {
let (track, payload) = attachment.into_parts();
files[track.get()] = Some(payload);
}
timed => {
first = Some(timed);
break;
}
}
}
let table = demuxer
.tracks()
.iter()
.zip(files)
.map(|(row, file)| TrackDescription::from_track_info(&**row, file, ()))
.collect::<Result<Vec<_>, _>>()
.expect("every row describes a track");
let mut muxer = open(table);
if let Some(packet) = first {
muxer
.write_packet(packet)
.map_err(|error| error.map_packet(drop))
.expect("the first timed packet");
}
while let Some(packet) = demuxer.next_packet().expect("a packet or the end") {
muxer
.write_packet(packet)
.map_err(|error| error.map_packet(drop))
.expect("a timed packet");
}
muxer.finish().expect("the container is finished");
}
fn owned_text(text: &impl AsRef<str>) -> String {
text.as_ref().to_owned()
}
#[derive(Debug, PartialEq)]
struct FileSeen {
track: usize,
filename: Option<String>,
mime_type: Option<String>,
payload: Vec<u8>,
}
#[derive(Debug, Default)]
struct Seen {
kinds: Vec<TrackKind>,
codecs: Vec<mediadecode_ffmpeg::CodecId>,
languages: Vec<Option<String>>,
files: Vec<FileSeen>,
written: Vec<usize>,
finished: bool,
}
struct Ledger<B> {
kinds: Vec<TrackKind>,
last: Vec<Option<Timestamp>>,
seen: Rc<RefCell<Seen>>,
_buffer: core::marker::PhantomData<B>,
}
impl<B: AsRef<[u8]>> Ledger<B> {
fn open(table: Vec<TrackDescription<Ffmpeg, B, ()>>, seen: Rc<RefCell<Seen>>) -> Self {
let kinds: Vec<TrackKind> = table.iter().map(TrackDescription::kind).collect();
{
let mut record = seen.borrow_mut();
record.kinds = kinds.clone();
record.codecs = table.iter().map(TrackDescription::codec).collect();
record.languages = table
.iter()
.map(|description| description.language().map(owned_text))
.collect();
record.files = table
.iter()
.enumerate()
.filter_map(|(track, description)| {
description.file().map(|file| FileSeen {
track,
filename: file.filename().map(owned_text),
mime_type: file.mime_type().map(owned_text),
payload: file.payload().data().as_ref().to_vec(),
})
})
.collect();
record.written = vec![0; kinds.len()];
}
let last = vec![None; kinds.len()];
Self {
kinds,
last,
seen,
_buffer: core::marker::PhantomData,
}
}
fn admit(
&mut self,
track: TrackIndex,
offered: TrackKind,
dts: Option<Timestamp>,
) -> Option<MuxRefusal> {
let Some(&expected) = self.kinds.get(track.get()) else {
return Some(MuxRefusal::UnknownTrack { track });
};
if expected != offered {
return Some(MuxRefusal::KindMismatch {
track,
expected,
offered,
});
}
let Some(dts) = dts else {
return Some(MuxRefusal::MissingDts { track });
};
if let Err(refusal) = check_decode_order(track, self.last[track.get()], dts) {
return Some(refusal);
}
self.last[track.get()] = Some(dts);
self.seen.borrow_mut().written[track.get()] += 1;
None
}
}
impl<B: AsRef<[u8]>> Muxer for Ledger<B> {
type Adapter = Ffmpeg;
type Buffer = B;
type Refusal = core::convert::Infallible;
type Error = core::convert::Infallible;
fn write_video(
&mut self,
packet: VideoTrackPacket<Ffmpeg, B>,
) -> Result<(), MuxError<Self::Error, Self::Refusal, VideoTrackPacket<Ffmpeg, B>>> {
let dts = packet.packet().dts();
match self.admit(packet.track(), TrackKind::Video, dts) {
Some(refusal) => Err(MuxError::Refused { refusal, packet }),
None => Ok(()),
}
}
fn write_audio(
&mut self,
packet: AudioTrackPacket<Ffmpeg, B>,
) -> Result<(), MuxError<Self::Error, Self::Refusal, AudioTrackPacket<Ffmpeg, B>>> {
let dts = packet.packet().dts();
match self.admit(packet.track(), TrackKind::Audio, dts) {
Some(refusal) => Err(MuxError::Refused { refusal, packet }),
None => Ok(()),
}
}
fn write_subtitle(
&mut self,
packet: SubtitleTrackPacket<Ffmpeg, B>,
) -> Result<(), MuxError<Self::Error, Self::Refusal, SubtitleTrackPacket<Ffmpeg, B>>> {
let dts = packet.packet().pts();
match self.admit(packet.track(), TrackKind::Subtitle, dts) {
Some(refusal) => Err(MuxError::Refused { refusal, packet }),
None => Ok(()),
}
}
fn write_data(
&mut self,
packet: DataTrackPacket<Ffmpeg, B>,
) -> Result<(), MuxError<Self::Error, Self::Refusal, DataTrackPacket<Ffmpeg, B>>> {
let dts = packet.packet().pts();
match self.admit(packet.track(), TrackKind::Data, dts) {
Some(refusal) => Err(MuxError::Refused { refusal, packet }),
None => Ok(()),
}
}
fn finish(self) -> Result<(), Self::Error> {
self.seen.borrow_mut().finished = true;
Ok(())
}
}
#[test]
fn a_generic_remux_builds_its_table_from_a_live_demuxer() {
let Some(corpus) = Corpus::new() else { return };
let path = corpus.multi_track_mkv();
let seen = Rc::new(RefCell::new(Seen::default()));
let mut demuxer = FfmpegDemuxer::open(&path).expect("open mkv");
let rows_before: Vec<(TrackKind, mediadecode_ffmpeg::CodecId)> = demuxer
.tracks()
.iter()
.map(|row| (row.kind(), row.params().codec()))
.collect();
let languages_before: Vec<Option<String>> = demuxer
.tracks()
.iter()
.map(|row| row.language().map(owned_text))
.collect();
let ledger_seen = Rc::clone(&seen);
remux(&mut demuxer, move |table| {
Ledger::<FfmpegBuffer>::open(table, ledger_seen)
});
let seen = seen.borrow();
assert!(seen.finished, "the container was finished");
assert_eq!(
seen.kinds,
vec![
TrackKind::Video,
TrackKind::Audio,
TrackKind::Subtitle,
TrackKind::Attachment,
],
"the muxer was opened with all four tracks",
);
let described: Vec<(TrackKind, mediadecode_ffmpeg::CodecId)> = seen
.kinds
.iter()
.copied()
.zip(seen.codecs.iter().copied())
.collect();
assert_eq!(described, rows_before, "each description is its row's");
assert_eq!(seen.languages, languages_before, "each row's language");
assert_eq!(
seen.files,
vec![FileSeen {
track: 3,
filename: Some("font.ttf".to_owned()),
mime_type: Some("application/x-truetype-font".to_owned()),
payload: support::FONT_PAYLOAD.to_vec(),
}],
"the attachment track's one file: the delivered bytes, under the row's identity",
);
let mut recount = FfmpegDemuxer::open(&path).expect("reopen mkv");
let mut expected = vec![0usize; seen.kinds.len()];
while let Some(packet) = recount.next_packet().expect("a packet or the end") {
if !matches!(packet, DemuxedPacket::Attachment(_)) {
expected[packet.track().get()] += 1;
}
}
assert_eq!(seen.written, expected, "every timed packet was written");
assert!(
seen.written[..3].iter().all(|&count| count > 0),
"each timed track carried packets"
);
let rows_after: Vec<(TrackKind, mediadecode_ffmpeg::CodecId)> = demuxer
.tracks()
.iter()
.map(|row| (row.kind(), row.params().codec()))
.collect();
assert_eq!(rows_after, rows_before, "the demuxer keeps every row");
assert_eq!(
demuxer.tracks()[3].filename().map(owned_text).as_deref(),
Some("font.ttf"),
"and the attachment row its identity",
);
}
#[test]
fn a_generic_remux_writes_cues_that_share_an_instant() {
let Some(corpus) = Corpus::new() else { return };
let path = corpus.simultaneous_cues_mkv();
let mut source = FfmpegDemuxer::open(&path).expect("open mkv");
let mut cue_times = Vec::new();
while let Some(packet) = source.next_packet().expect("a packet or the end") {
if let DemuxedPacket::Subtitle(cue) = &packet {
cue_times.push(cue.packet().pts().expect("a cue has a time"));
}
}
assert_eq!(cue_times.len(), 2, "the fixture carries two cues");
assert_eq!(cue_times[0], cue_times[1], "at one instant");
let seen = Rc::new(RefCell::new(Seen::default()));
let mut demuxer = FfmpegDemuxer::open(&path).expect("open mkv");
let ledger_seen = Rc::clone(&seen);
remux(&mut demuxer, move |table| {
Ledger::<FfmpegBuffer>::open(table, ledger_seen)
});
let seen = seen.borrow();
assert!(seen.finished, "the container was finished");
assert_eq!(seen.kinds, vec![TrackKind::Video, TrackKind::Subtitle]);
assert_eq!(seen.written[1], 2, "both cues were written");
}