use std::io::{Read, Seek, Write};
use crate::edit::{EditEntry, EditList};
use crate::error::{Error, Result};
use crate::metadata::{Chapter, MetadataItem};
use crate::mp4::mux::bytes::invalid;
use crate::mp4::mux::chapters::chapter_track;
use crate::mp4::mux::entry::build_entry;
use crate::mp4::mux::interleave::{earliest_open, next_chunk};
use crate::mp4::mux::layout::{patch_mdat, settle_moov, shift_up, write_at, write_head};
use crate::mp4::mux::metadata::{Level, check_chapters, check_items};
use crate::mp4::mux::moov::{Movie, build_moov, track_duration};
use crate::mp4::mux::options::MuxOptions;
use crate::mp4::mux::spec::{PacketFormat, PacketIn, TrackId, TrackSpec};
use crate::mp4::mux::track::TrackState;
pub const MAX_TRACKS: usize = 1024;
#[derive(Debug)]
pub struct Muxer<W> {
writer: W,
options: MuxOptions,
tracks: Vec<TrackState>,
metadata: Vec<MetadataItem>,
chapters: Vec<Chapter>,
reserved: u64,
pos: u64,
buffered: usize,
started: bool,
broken: bool,
}
impl<W> Muxer<W> {
pub fn buffered_bytes(&self) -> usize {
self.buffered
}
}
impl<W: Write + Seek> Muxer<W> {
pub fn new(mut writer: W, options: MuxOptions) -> Result<Self> {
options.check()?;
let (reserved, pos) = write_head(&mut writer, options.brand)?;
Ok(Self {
writer,
options,
tracks: Vec::new(),
metadata: Vec::new(),
chapters: Vec::new(),
reserved,
pos,
buffered: 0,
started: false,
broken: false,
})
}
fn check_usable(&self) -> Result<()> {
if self.broken {
return Err(invalid(
"an earlier write to the target failed, so the output is incomplete. Start \
again on a new target"
.to_owned(),
));
}
Ok(())
}
pub fn add_track(&mut self, mut spec: TrackSpec) -> Result<TrackId> {
self.check_usable()?;
if self.started {
return Err(invalid(
"`add_track` came after the first packet. Add every track first".to_owned(),
));
}
if self.tracks.len() >= MAX_TRACKS {
return Err(invalid(format!(
"the movie holds {MAX_TRACKS} tracks, the most this muxer accepts. Mux fewer tracks"
)));
}
if spec.timescale == 0 {
return Err(invalid(
"`TrackSpec::timescale` is 0. Use a positive tick rate".to_owned(),
));
}
check_items(&spec.metadata, Level::Track)?;
spec.edit_list = checked_edit_list(&spec.edit_list, self.options.movie_timescale)?;
let (fourcc, entry) = build_entry(&spec.sample_entry, spec.pixel_aspect, spec.colour)?;
if spec.packet_format == PacketFormat::PlainText && &fourcc.0 != b"tx3g" {
return Err(invalid(format!(
"`PacketFormat::PlainText` writes `tx3g` samples, but the sample entry is \
`{fourcc}`. Use `SampleEntrySpec::Tx3g`"
)));
}
let id = self.tracks.len() as u32 + 1;
self.tracks.push(TrackState::new(
id,
spec,
entry,
self.options.chunk_duration_ms,
));
Ok(TrackId(id))
}
pub fn set_metadata(&mut self, items: Vec<MetadataItem>) -> Result<()> {
check_items(&items, Level::Movie)?;
self.metadata = items;
Ok(())
}
pub fn set_chapters(&mut self, chapters: Vec<Chapter>) -> Result<()> {
check_chapters(&chapters)?;
self.chapters = chapters;
Ok(())
}
pub fn write_packet(&mut self, track: TrackId, packet: &PacketIn<'_>) -> Result<()> {
self.check_usable()?;
let slot = (track.0 as usize)
.checked_sub(1)
.filter(|&slot| slot < self.tracks.len())
.ok_or_else(|| Error::TrackNotFound {
track_id: track.0,
available: self.tracks.iter().map(|t| t.id).collect(),
})?;
let signed = self.options.brand.signed_offsets();
let Some(state) = self.tracks.get_mut(slot) else {
return Ok(());
};
self.buffered += state.push(packet, signed)?;
self.started = true;
self.drain(false)
}
pub fn set_edit_list(&mut self, track: TrackId, list: &EditList) -> Result<()> {
self.check_usable()?;
let list = checked_edit_list(list, self.options.movie_timescale)?;
let slot = (track.0 as usize)
.checked_sub(1)
.filter(|&slot| slot < self.tracks.len())
.ok_or_else(|| Error::TrackNotFound {
track_id: track.0,
available: self.tracks.iter().map(|t| t.id).collect(),
})?;
if let Some(state) = self.tracks.get_mut(slot) {
state.spec.edit_list = list;
}
Ok(())
}
fn drain(&mut self, force: bool) -> Result<()> {
loop {
let over = self.buffered > self.options.max_buffer;
if let Some(slot) = next_chunk(&self.tracks, force || over) {
self.write_chunk(slot)?;
continue;
}
if over && let Some(slot) = earliest_open(&self.tracks) {
if let Some(track) = self.tracks.get_mut(slot) {
track.close_open();
}
continue;
}
return Ok(());
}
}
fn write_chunk(&mut self, slot: usize) -> Result<()> {
let Some(track) = self.tracks.get_mut(slot) else {
return Ok(());
};
let Some(chunk) = track.closed.pop_front() else {
return Ok(());
};
track.table.push_chunk(self.pos, chunk.samples);
if let Err(err) = self.writer.write_all(&chunk.data) {
self.broken = true;
return Err(Error::io("the output: writing mdat", self.pos, err));
}
self.pos += chunk.data.len() as u64;
self.buffered = self.buffered.saturating_sub(chunk.data.len());
Ok(())
}
fn check_references(&self) -> Result<()> {
for track in &self.tracks {
for reference in &track.spec.references {
for &id in &reference.track_ids {
if id == 0 || id as usize > self.tracks.len() {
return Err(invalid(format!(
"track {} has a `{}` reference to track {id}, but the movie \
holds tracks 1 to {}. Reference output track ids",
track.id,
reference.kind,
self.tracks.len()
)));
}
}
}
}
Ok(())
}
fn add_chapter_track(&mut self) -> Result<Option<u32>> {
if self.chapters.is_empty() || self.tracks.is_empty() {
return Ok(None);
}
let timescale = self.options.movie_timescale;
let duration = self
.tracks
.iter()
.map(|track| track_duration(track, timescale))
.max()
.unwrap_or(0);
let id = self.tracks.len() as u32 + 1;
let track = chapter_track(
id,
&self.chapters,
timescale,
duration,
self.options.chunk_duration_ms,
)?;
self.tracks.push(track);
Ok(Some(id))
}
}
impl<W: Read + Write + Seek> Muxer<W> {
pub fn finish(mut self) -> Result<W> {
self.check_usable()?;
self.check_references()?;
for track in &mut self.tracks {
track.finish()?;
}
self.drain(true)?;
let chapter_id = self.add_chapter_track()?;
self.drain(true)?;
patch_mdat(&mut self.writer, self.reserved, self.pos)?;
let movie = Movie {
options: &self.options,
tracks: &self.tracks,
chapter_track: chapter_id,
metadata: &self.metadata,
chapters: &self.chapters,
};
if self.options.faststart {
let moov = settle_moov(|shift| build_moov(&movie, shift))?;
shift_up(&mut self.writer, self.reserved, self.pos, moov.len() as u64)?;
write_at(&mut self.writer, self.reserved, &moov)?;
} else {
let moov = build_moov(&movie, 0)?;
write_at(&mut self.writer, self.pos, &moov)?;
}
self.writer
.flush()
.map_err(|err| Error::io("the output: flushing", self.pos, err))?;
Ok(self.writer)
}
}
fn checked_edit_list(list: &EditList, movie_timescale: u32) -> Result<EditList> {
if list.entries.is_empty() {
return Ok(EditList {
movie_timescale,
entries: Vec::new(),
});
}
if list.movie_timescale == 0 {
return Err(invalid(
"`EditList::movie_timescale` is 0 on a list with edits. Set the timescale its \
segment durations count in"
.to_owned(),
));
}
let mut entries = Vec::with_capacity(list.entries.len());
for edit in &list.entries {
if edit.media_time < EditEntry::EMPTY_MEDIA_TIME {
return Err(invalid(format!(
"an edit has media_time {}. Use -1 for an empty edit, or a value of 0 or more",
edit.media_time
)));
}
let scaled = u128::from(edit.segment_duration) * u128::from(movie_timescale);
let divisor = u128::from(list.movie_timescale);
let segment_duration = u64::try_from(scaled / divisor)
.ok()
.filter(|_| scaled.is_multiple_of(divisor))
.ok_or_else(|| {
invalid(format!(
"an edit of {} ticks at {} ticks per second has no exact value at the \
movie timescale {movie_timescale}. Set `MuxOptions::movie_timescale` \
to {}",
edit.segment_duration, list.movie_timescale, list.movie_timescale
))
})?;
entries.push(EditEntry {
segment_duration,
..*edit
});
}
Ok(EditList {
movie_timescale,
entries,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(segment_duration: u64, media_time: i64) -> EditEntry {
EditEntry {
segment_duration,
media_time,
media_rate: EditEntry::NORMAL_RATE,
}
}
#[test]
fn edit_lists_convert_only_when_exact() {
let list = EditList {
movie_timescale: 1000,
entries: vec![entry(500, 0)],
};
let converted = checked_edit_list(&list, 48_000).expect("the conversion is exact");
assert_eq!(converted.entries[0].segment_duration, 24_000);
assert_eq!(converted.movie_timescale, 48_000);
let inexact = EditList {
movie_timescale: 1000,
entries: vec![entry(1, 0)],
};
assert!(checked_edit_list(&inexact, 1).is_err());
}
#[test]
fn edit_lists_refuse_a_negative_media_time() {
let list = EditList {
movie_timescale: 1000,
entries: vec![entry(500, -2)],
};
assert!(checked_edit_list(&list, 1000).is_err());
}
}