use crate::parse::validate_tree;
use crate::*;
use std::io::{Seek, SeekFrom, Write};
pub struct Mp4Writer<W: Write + Seek> {
writer: W,
config: WriterConfig,
tracks: Vec<WriterTrack>,
first_sample_written: bool,
current_track: Option<TrackId>,
ftyp_offset: u64,
free_offset: u64,
mdat_offset: u64,
data_offset: u64,
}
struct WriterTrack {
id: TrackId,
config: TrackConfig,
samples: Vec<WriterSample>,
chunks: Vec<WriterChunk>,
force_co64: bool,
}
#[derive(Clone, Copy)]
struct WriterSample {
size: u32,
duration: u32,
cts_offset: i32,
is_sync: bool,
}
#[derive(Clone, Copy)]
struct WriterChunk {
offset: u64,
sample_count: u32,
}
impl<W: Write + Seek> Mp4Writer<W> {
pub fn new(mut writer: W, config: WriterConfig) -> Result<Self, Error> {
if config.movie_timescale == 0 {
return Err(Error::InvalidTimescale { track: None });
}
let ftyp_offset = writer.stream_position()?;
let ftyp = make_ftyp(&config, false)?;
writer.write_all(&ftyp)?;
let free_offset = writer.stream_position()?;
writer.write_all(&8u32.to_be_bytes())?;
writer.write_all(b"free")?;
let mdat_offset = writer.stream_position()?;
writer.write_all(&0u32.to_be_bytes())?;
writer.write_all(b"mdat")?;
let data_offset = writer.stream_position()?;
Ok(Self {
writer,
config,
tracks: Vec::new(),
first_sample_written: false,
current_track: None,
ftyp_offset,
free_offset,
mdat_offset,
data_offset,
})
}
pub fn add_track(&mut self, track: TrackConfig) -> Result<TrackId, Error> {
if self.first_sample_written {
return Err(Error::WriterState("tracks must be added before samples"));
}
validate_track_config(&track)?;
let next = self
.tracks
.len()
.checked_add(1)
.ok_or(Error::WriterState("track count overflow"))?;
let id = TrackId(
u32::try_from(next).map_err(|_| Error::WriterState("track count exceeds u32"))?,
);
self.tracks.push(WriterTrack {
id,
config: track,
samples: Vec::new(),
chunks: Vec::new(),
force_co64: self.config.chunk_offsets == ChunkOffsetChoice::Co64,
});
Ok(id)
}
pub fn write_sample(&mut self, id: TrackId, sample: &SampleInput<'_>) -> Result<(), Error> {
let position = self
.tracks
.iter()
.position(|track| track.id == id)
.ok_or(Error::NoSuchTrack(id))?;
let size = u32::try_from(sample.data.len()).map_err(|_| Error::LimitExceeded {
limit: "sample_size",
value: u64::try_from(sample.data.len()).unwrap_or(u64::MAX),
max: u64::from(u32::MAX),
})?;
let offset = self.writer.stream_position()?;
self.writer.write_all(sample.data)?;
let track = self
.tracks
.get_mut(position)
.ok_or(Error::NoSuchTrack(id))?;
if self.current_track != Some(id) {
track.chunks.push(WriterChunk {
offset,
sample_count: 0,
});
self.current_track = Some(id);
}
let chunk = track
.chunks
.last_mut()
.ok_or(Error::WriterState("sample chunk is missing"))?;
chunk.sample_count = chunk
.sample_count
.checked_add(1)
.ok_or(Error::WriterState("chunk sample count overflow"))?;
track.samples.push(WriterSample {
size,
duration: sample.duration,
cts_offset: sample.cts_offset,
is_sync: sample.is_sync,
});
self.first_sample_written = true;
Ok(())
}
pub fn finish(mut self) -> Result<W, Error> {
if self.tracks.is_empty() {
return Err(Error::WriterState("at least one track is required"));
}
let media_end = self.writer.stream_position()?;
let payload_size = media_end
.checked_sub(self.data_offset)
.ok_or(Error::WriterState("media-data size underflow"))?;
if payload_size <= u64::from(u32::MAX).saturating_sub(8) {
let size = u32::try_from(payload_size.saturating_add(8))
.map_err(|_| Error::WriterState("media-data size exceeds u32"))?;
self.writer.seek(SeekFrom::Start(self.mdat_offset))?;
self.writer.write_all(&size.to_be_bytes())?;
self.writer.write_all(b"mdat")?;
} else {
let size = payload_size
.checked_add(16)
.ok_or(Error::WriterState("media-data size overflow"))?;
self.writer.seek(SeekFrom::Start(self.free_offset))?;
self.writer.write_all(&1u32.to_be_bytes())?;
self.writer.write_all(b"mdat")?;
self.writer.write_all(&size.to_be_bytes())?;
}
let has_avc = self
.tracks
.iter()
.any(|track| matches!(track.config.codec, Codec::H264 { .. }));
self.writer.seek(SeekFrom::Start(self.ftyp_offset))?;
self.writer.write_all(&make_ftyp(&self.config, has_avc)?)?;
self.writer.seek(SeekFrom::Start(media_end))?;
let moov = make_moov(&self.config, &self.tracks)?;
validate_tree(
moov.get(8..).ok_or(Error::WriterState(
"encoded movie is smaller than its header",
))?,
FourCc(*b"moov"),
0,
Limits::default(),
)?;
self.writer.write_all(&moov)?;
self.writer.flush()?;
Ok(self.writer)
}
}
fn validate_track_config(track: &TrackConfig) -> Result<(), Error> {
if track.timescale == 0 {
return Err(Error::InvalidTimescale { track: None });
}
let bytes = track.language.as_bytes();
if bytes.len() != 3 || !bytes.iter().all(|byte| byte.is_ascii_lowercase()) {
return Err(Error::WriterState(
"language must be three lowercase ASCII letters",
));
}
if !track.width.is_finite()
|| !track.height.is_finite()
|| track.width < 0.0
|| track.height < 0.0
{
return Err(Error::WriterState(
"track dimensions must be finite and nonnegative",
));
}
if track.width > 65_535.0 || track.height > 65_535.0 {
return Err(Error::WriterState("track dimensions exceed 16.16 range"));
}
Ok(())
}
fn make_box(kind: FourCc, body: Vec<u8>) -> Result<Vec<u8>, Error> {
let size = body
.len()
.checked_add(8)
.ok_or(Error::WriterState("box size overflow"))?;
let size = u32::try_from(size).map_err(|_| Error::WriterState("box exceeds u32 size"))?;
let mut result = Vec::with_capacity(usize::try_from(size).unwrap_or(0));
result.extend_from_slice(&size.to_be_bytes());
result.extend_from_slice(&kind.0);
result.extend_from_slice(&body);
Ok(result)
}
fn full_box(version: u8, flags: u32, mut body: Vec<u8>) -> Vec<u8> {
let mut result = Vec::with_capacity(body.len().saturating_add(4));
result.push(version);
let bytes = flags.to_be_bytes();
result.extend_from_slice(bytes.get(1..).unwrap_or_default());
result.append(&mut body);
result
}
fn make_ftyp(config: &WriterConfig, has_avc: bool) -> Result<Vec<u8>, Error> {
let mut brands = config.compatible_brands.clone();
for required in [FourCc(*b"isom"), FourCc(*b"iso2"), FourCc(*b"mp41")] {
if !brands.contains(&required) {
brands.push(required);
}
}
brands.push(if has_avc {
FourCc(*b"avc1")
} else {
config.major_brand
});
let mut body = Vec::with_capacity(8usize.saturating_add(brands.len().saturating_mul(4)));
body.extend_from_slice(&config.major_brand.0);
body.extend_from_slice(&512u32.to_be_bytes());
for brand in brands {
body.extend_from_slice(&brand.0);
}
make_box(FourCc(*b"ftyp"), body)
}
fn make_moov(config: &WriterConfig, tracks: &[WriterTrack]) -> Result<Vec<u8>, Error> {
let movie_duration = tracks.iter().try_fold(0u64, |maximum, track| {
Ok::<u64, Error>(maximum.max(track_movie_duration(config.movie_timescale, track)?))
})?;
let mut body = make_mvhd(config.movie_timescale, movie_duration, tracks.len())?;
for track in tracks {
body.extend_from_slice(&make_trak(config.movie_timescale, track)?);
}
make_box(FourCc(*b"moov"), body)
}
fn track_media_duration(track: &WriterTrack) -> Result<u64, Error> {
track.samples.iter().try_fold(0u64, |sum, sample| {
sum.checked_add(u64::from(sample.duration))
.ok_or(Error::WriterState("track duration overflow"))
})
}
fn track_movie_duration(movie_timescale: u32, track: &WriterTrack) -> Result<u64, Error> {
if !track.config.edit_list.is_empty() {
return track.config.edit_list.iter().try_fold(0u64, |sum, edit| {
sum.checked_add(edit.segment_duration)
.ok_or(Error::WriterState("edit duration overflow"))
});
}
let media = u128::from(track_media_duration(track)?);
let scaled = media
.checked_mul(u128::from(movie_timescale))
.ok_or(Error::WriterState("movie duration overflow"))?
.checked_div(u128::from(track.config.timescale))
.ok_or(Error::InvalidTimescale {
track: Some(track.id),
})?;
u64::try_from(scaled).map_err(|_| Error::WriterState("movie duration exceeds u64"))
}
fn make_mvhd(timescale: u32, duration: u64, track_count: usize) -> Result<Vec<u8>, Error> {
let version = if duration > u64::from(u32::MAX) { 1 } else { 0 };
let mut body = Vec::new();
if version == 1 {
body.extend_from_slice(&0u64.to_be_bytes());
body.extend_from_slice(&0u64.to_be_bytes());
body.extend_from_slice(×cale.to_be_bytes());
body.extend_from_slice(&duration.to_be_bytes());
} else {
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(×cale.to_be_bytes());
body.extend_from_slice(&u32::try_from(duration).unwrap_or(u32::MAX).to_be_bytes());
}
body.extend_from_slice(&0x0001_0000u32.to_be_bytes());
body.extend_from_slice(&0x0100u16.to_be_bytes());
body.extend_from_slice(&[0; 10]);
append_matrix(&mut body);
body.extend_from_slice(&[0; 24]);
let next = u32::try_from(track_count)
.unwrap_or(u32::MAX)
.saturating_add(1);
body.extend_from_slice(&next.to_be_bytes());
make_box(FourCc(*b"mvhd"), full_box(version, 0, body))
}
fn append_matrix(body: &mut Vec<u8>) {
for value in [0x0001_0000u32, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000] {
body.extend_from_slice(&value.to_be_bytes());
}
}
fn make_trak(movie_timescale: u32, track: &WriterTrack) -> Result<Vec<u8>, Error> {
let duration = track_movie_duration(movie_timescale, track)?;
let mut body = make_tkhd(duration, track)?;
if !track.config.edit_list.is_empty() {
body.extend_from_slice(&make_edts(&track.config.edit_list)?);
}
body.extend_from_slice(&make_mdia(track)?);
make_box(FourCc(*b"trak"), body)
}
fn fixed_16_16(value: f64) -> Result<u32, Error> {
let scaled = value * 65_536.0;
if !(0.0..=f64::from(u32::MAX)).contains(&scaled) {
return Err(Error::WriterState("fixed-point value is out of range"));
}
Ok(scaled.round() as u32)
}
fn make_tkhd(duration: u64, track: &WriterTrack) -> Result<Vec<u8>, Error> {
let version = if duration > u64::from(u32::MAX) { 1 } else { 0 };
let mut body = Vec::new();
if version == 1 {
body.extend_from_slice(&[0; 16]);
body.extend_from_slice(&track.id.0.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&duration.to_be_bytes());
} else {
body.extend_from_slice(&[0; 8]);
body.extend_from_slice(&track.id.0.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&u32::try_from(duration).unwrap_or(u32::MAX).to_be_bytes());
}
body.extend_from_slice(&[0; 8]);
body.extend_from_slice(&0i16.to_be_bytes());
body.extend_from_slice(&0i16.to_be_bytes());
let volume = if track.config.kind == TrackKind::Audio {
0x0100u16
} else {
0
};
body.extend_from_slice(&volume.to_be_bytes());
body.extend_from_slice(&0u16.to_be_bytes());
append_matrix(&mut body);
body.extend_from_slice(&fixed_16_16(track.config.width)?.to_be_bytes());
body.extend_from_slice(&fixed_16_16(track.config.height)?.to_be_bytes());
make_box(FourCc(*b"tkhd"), full_box(version, 3, body))
}
fn make_edts(edits: &[Edit]) -> Result<Vec<u8>, Error> {
let version = if edits.iter().any(|edit| {
edit.segment_duration > u64::from(u32::MAX)
|| edit
.media_time
.is_some_and(|value| value > i64::from(i32::MAX) as u64)
}) {
1
} else {
0
};
let mut body = Vec::new();
body.extend_from_slice(
&u32::try_from(edits.len())
.map_err(|_| Error::WriterState("edit count exceeds u32"))?
.to_be_bytes(),
);
for edit in edits {
if version == 1 {
body.extend_from_slice(&edit.segment_duration.to_be_bytes());
let media_time = edit
.media_time
.map(|value| i64::try_from(value).unwrap_or(i64::MAX))
.unwrap_or(-1);
body.extend_from_slice(&media_time.to_be_bytes());
} else {
body.extend_from_slice(
&u32::try_from(edit.segment_duration)
.unwrap_or(u32::MAX)
.to_be_bytes(),
);
let media_time = edit
.media_time
.map(|value| i32::try_from(value).unwrap_or(i32::MAX))
.unwrap_or(-1);
body.extend_from_slice(&media_time.to_be_bytes());
}
let integer = edit.media_rate.trunc();
let fraction = ((edit.media_rate - integer) * 65_536.0).round();
body.extend_from_slice(&(integer as i16).to_be_bytes());
body.extend_from_slice(&(fraction as u16).to_be_bytes());
}
let elst = make_box(FourCc(*b"elst"), full_box(version, 0, body))?;
make_box(FourCc(*b"edts"), elst)
}
fn make_mdia(track: &WriterTrack) -> Result<Vec<u8>, Error> {
let mut body = make_mdhd(track)?;
body.extend_from_slice(&make_hdlr(track)?);
body.extend_from_slice(&make_minf(track)?);
make_box(FourCc(*b"mdia"), body)
}
fn pack_language(language: &str) -> Result<u16, Error> {
let bytes = language.as_bytes();
let first = u16::from(
*bytes
.first()
.ok_or(Error::WriterState("language is empty"))?,
)
.saturating_sub(0x60);
let second = u16::from(
*bytes
.get(1)
.ok_or(Error::WriterState("language is too short"))?,
)
.saturating_sub(0x60);
let third = u16::from(
*bytes
.get(2)
.ok_or(Error::WriterState("language is too short"))?,
)
.saturating_sub(0x60);
Ok((first << 10) | (second << 5) | third)
}
fn make_mdhd(track: &WriterTrack) -> Result<Vec<u8>, Error> {
let duration = track_media_duration(track)?;
let version = if duration > u64::from(u32::MAX) { 1 } else { 0 };
let mut body = Vec::new();
if version == 1 {
body.extend_from_slice(&[0; 16]);
body.extend_from_slice(&track.config.timescale.to_be_bytes());
body.extend_from_slice(&duration.to_be_bytes());
} else {
body.extend_from_slice(&[0; 8]);
body.extend_from_slice(&track.config.timescale.to_be_bytes());
body.extend_from_slice(&u32::try_from(duration).unwrap_or(u32::MAX).to_be_bytes());
}
body.extend_from_slice(&pack_language(&track.config.language)?.to_be_bytes());
body.extend_from_slice(&0u16.to_be_bytes());
make_box(FourCc(*b"mdhd"), full_box(version, 0, body))
}
fn handler_type(kind: TrackKind) -> FourCc {
match kind {
TrackKind::Video => FourCc(*b"vide"),
TrackKind::Audio => FourCc(*b"soun"),
TrackKind::Subtitle => FourCc(*b"subt"),
TrackKind::Other(value) => value,
}
}
fn make_hdlr(track: &WriterTrack) -> Result<Vec<u8>, Error> {
let mut body = Vec::new();
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&handler_type(track.config.kind).0);
body.extend_from_slice(&[0; 12]);
body.extend_from_slice(track.config.handler_name.as_bytes());
body.push(0);
make_box(FourCc(*b"hdlr"), full_box(0, 0, body))
}
fn make_minf(track: &WriterTrack) -> Result<Vec<u8>, Error> {
let mut body = match track.config.kind {
TrackKind::Video => {
let mut fields = Vec::new();
fields.extend_from_slice(&0u16.to_be_bytes());
fields.extend_from_slice(&[0; 6]);
make_box(FourCc(*b"vmhd"), full_box(0, 1, fields))?
}
TrackKind::Audio => {
let mut fields = Vec::new();
fields.extend_from_slice(&0i16.to_be_bytes());
fields.extend_from_slice(&0u16.to_be_bytes());
make_box(FourCc(*b"smhd"), full_box(0, 0, fields))?
}
TrackKind::Subtitle | TrackKind::Other(_) => {
make_box(FourCc(*b"nmhd"), full_box(0, 0, Vec::new()))?
}
};
body.extend_from_slice(&make_dinf()?);
body.extend_from_slice(&make_stbl(track)?);
make_box(FourCc(*b"minf"), body)
}
fn make_dinf() -> Result<Vec<u8>, Error> {
let url = make_box(FourCc(*b"url "), full_box(0, 1, Vec::new()))?;
let mut dref_body = Vec::new();
dref_body.extend_from_slice(&1u32.to_be_bytes());
dref_body.extend_from_slice(&url);
let dref = make_box(FourCc(*b"dref"), full_box(0, 0, dref_body))?;
make_box(FourCc(*b"dinf"), dref)
}
fn make_stbl(track: &WriterTrack) -> Result<Vec<u8>, Error> {
let mut body = make_stsd(track)?;
body.extend_from_slice(&make_stts(track)?);
if track.samples.iter().any(|sample| sample.cts_offset != 0) {
body.extend_from_slice(&make_ctts(track)?);
}
if track.samples.iter().any(|sample| !sample.is_sync) {
body.extend_from_slice(&make_stss(track)?);
}
body.extend_from_slice(&make_stsc(track)?);
body.extend_from_slice(&make_stsz(track)?);
body.extend_from_slice(&make_chunk_offsets(track)?);
make_box(FourCc(*b"stbl"), body)
}
fn make_stsd(track: &WriterTrack) -> Result<Vec<u8>, Error> {
let entry = make_sample_entry(track)?;
let mut body = Vec::new();
body.extend_from_slice(&1u32.to_be_bytes());
body.extend_from_slice(&entry);
make_box(FourCc(*b"stsd"), full_box(0, 0, body))
}
fn visual_fields(width: f64, height: f64) -> Result<Vec<u8>, Error> {
let mut body = Vec::with_capacity(78);
body.extend_from_slice(&[0; 6]);
body.extend_from_slice(&1u16.to_be_bytes());
body.extend_from_slice(&[0; 16]);
body.extend_from_slice(
&u16::try_from(width.round() as u64)
.map_err(|_| Error::WriterState("video width exceeds u16"))?
.to_be_bytes(),
);
body.extend_from_slice(
&u16::try_from(height.round() as u64)
.map_err(|_| Error::WriterState("video height exceeds u16"))?
.to_be_bytes(),
);
body.extend_from_slice(&0x0048_0000u32.to_be_bytes());
body.extend_from_slice(&0x0048_0000u32.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&1u16.to_be_bytes());
body.extend_from_slice(&[0; 32]);
body.extend_from_slice(&0x0018u16.to_be_bytes());
body.extend_from_slice(&u16::MAX.to_be_bytes());
Ok(body)
}
fn audio_fields(channels: u16, sample_rate: u32) -> Vec<u8> {
let mut body = Vec::with_capacity(28);
body.extend_from_slice(&[0; 6]);
body.extend_from_slice(&1u16.to_be_bytes());
body.extend_from_slice(&[0; 8]);
body.extend_from_slice(&channels.to_be_bytes());
body.extend_from_slice(&16u16.to_be_bytes());
body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&0u16.to_be_bytes());
let fixed_rate = if sample_rate > 65_535 {
0
} else {
sample_rate.saturating_mul(65_536)
};
body.extend_from_slice(&fixed_rate.to_be_bytes());
body
}
fn append_sidecars(body: &mut Vec<u8>, sidecars: &[SidecarBox]) -> Result<(), Error> {
for sidecar in sidecars {
body.extend_from_slice(&make_box(sidecar.kind, sidecar.payload.clone())?);
}
Ok(())
}
fn make_sample_entry(track: &WriterTrack) -> Result<Vec<u8>, Error> {
match &track.config.codec {
Codec::H264 { entry, config } => {
let mut body = visual_fields(track.config.width, track.config.height)?;
body.extend_from_slice(&make_box(FourCc(*b"avcC"), config.raw.clone())?);
append_sidecars(&mut body, &config.sidecars)?;
make_box(*entry, body)
}
Codec::H265 { entry, config } => {
let mut body = visual_fields(track.config.width, track.config.height)?;
body.extend_from_slice(&make_box(FourCc(*b"hvcC"), config.raw.clone())?);
append_sidecars(&mut body, &config.sidecars)?;
make_box(*entry, body)
}
Codec::Aac { config } => {
let mut body = audio_fields(config.channels, config.sample_rate);
body.extend_from_slice(&make_box(FourCc(*b"esds"), config.raw_esds.clone())?);
append_sidecars(&mut body, &config.sidecars)?;
make_box(FourCc(*b"mp4a"), body)
}
Codec::Opus { config } => {
let mut body = audio_fields(u16::from(config.channels), track.config.timescale);
body.extend_from_slice(&make_box(FourCc(*b"dOps"), config.raw.clone())?);
append_sidecars(&mut body, &config.sidecars)?;
make_box(FourCc(*b"Opus"), body)
}
Codec::Other { entry, raw } => make_box(*entry, raw.clone()),
}
}
fn rle_u32<I>(values: I) -> Vec<(u32, u32)>
where
I: IntoIterator<Item = u32>,
{
let mut result: Vec<(u32, u32)> = Vec::new();
for value in values {
if let Some(last) = result.last_mut() {
if last.1 == value {
last.0 = last.0.saturating_add(1);
continue;
}
}
result.push((1, value));
}
result
}
fn make_stts(track: &WriterTrack) -> Result<Vec<u8>, Error> {
let entries = rle_u32(track.samples.iter().map(|sample| sample.duration));
let mut body = Vec::new();
body.extend_from_slice(
&u32::try_from(entries.len())
.map_err(|_| Error::WriterState("time entry count exceeds u32"))?
.to_be_bytes(),
);
for (count, delta) in entries {
body.extend_from_slice(&count.to_be_bytes());
body.extend_from_slice(&delta.to_be_bytes());
}
make_box(FourCc(*b"stts"), full_box(0, 0, body))
}
fn make_ctts(track: &WriterTrack) -> Result<Vec<u8>, Error> {
let version = if track.samples.iter().any(|sample| sample.cts_offset < 0) {
1
} else {
0
};
let mut entries: Vec<(u32, i32)> = Vec::new();
for sample in &track.samples {
if let Some(last) = entries.last_mut() {
if last.1 == sample.cts_offset {
last.0 = last.0.saturating_add(1);
continue;
}
}
entries.push((1, sample.cts_offset));
}
let mut body = Vec::new();
body.extend_from_slice(
&u32::try_from(entries.len())
.map_err(|_| Error::WriterState("composition entry count exceeds u32"))?
.to_be_bytes(),
);
for (count, offset) in entries {
body.extend_from_slice(&count.to_be_bytes());
if version == 1 {
body.extend_from_slice(&offset.to_be_bytes());
} else {
body.extend_from_slice(
&u32::try_from(offset)
.map_err(|_| Error::WriterState("negative version-zero composition offset"))?
.to_be_bytes(),
);
}
}
make_box(FourCc(*b"ctts"), full_box(version, 0, body))
}
fn make_stss(track: &WriterTrack) -> Result<Vec<u8>, Error> {
let count = track.samples.iter().filter(|sample| sample.is_sync).count();
let mut body = Vec::new();
body.extend_from_slice(
&u32::try_from(count)
.map_err(|_| Error::WriterState("sync sample count exceeds u32"))?
.to_be_bytes(),
);
for (index, sample) in track.samples.iter().enumerate() {
if sample.is_sync {
let number = u32::try_from(index)
.map_err(|_| Error::WriterState("sample index exceeds u32"))?
.saturating_add(1);
body.extend_from_slice(&number.to_be_bytes());
}
}
make_box(FourCc(*b"stss"), full_box(0, 0, body))
}
fn make_stsc(track: &WriterTrack) -> Result<Vec<u8>, Error> {
let mut entries: Vec<(u32, u32)> = Vec::new();
for (index, chunk) in track.chunks.iter().enumerate() {
if entries
.last()
.is_some_and(|entry| entry.1 == chunk.sample_count)
{
continue;
}
entries.push((
u32::try_from(index)
.map_err(|_| Error::WriterState("chunk index exceeds u32"))?
.saturating_add(1),
chunk.sample_count,
));
}
let mut body = Vec::new();
body.extend_from_slice(
&u32::try_from(entries.len())
.map_err(|_| Error::WriterState("chunk map count exceeds u32"))?
.to_be_bytes(),
);
for (first_chunk, samples_per_chunk) in entries {
body.extend_from_slice(&first_chunk.to_be_bytes());
body.extend_from_slice(&samples_per_chunk.to_be_bytes());
body.extend_from_slice(&1u32.to_be_bytes());
}
make_box(FourCc(*b"stsc"), full_box(0, 0, body))
}
fn make_stsz(track: &WriterTrack) -> Result<Vec<u8>, Error> {
let fixed = track
.samples
.first()
.map(|first| first.size)
.filter(|size| track.samples.iter().all(|sample| sample.size == *size))
.unwrap_or(0);
let mut body = Vec::new();
body.extend_from_slice(&fixed.to_be_bytes());
body.extend_from_slice(
&u32::try_from(track.samples.len())
.map_err(|_| Error::WriterState("sample count exceeds u32"))?
.to_be_bytes(),
);
if fixed == 0 {
for sample in &track.samples {
body.extend_from_slice(&sample.size.to_be_bytes());
}
}
make_box(FourCc(*b"stsz"), full_box(0, 0, body))
}
fn make_chunk_offsets(track: &WriterTrack) -> Result<Vec<u8>, Error> {
let use_co64 = track.force_co64
|| track
.chunks
.iter()
.any(|chunk| chunk.offset > u64::from(u32::MAX));
let mut body = Vec::new();
body.extend_from_slice(
&u32::try_from(track.chunks.len())
.map_err(|_| Error::WriterState("chunk count exceeds u32"))?
.to_be_bytes(),
);
if use_co64 {
for chunk in &track.chunks {
body.extend_from_slice(&chunk.offset.to_be_bytes());
}
make_box(FourCc(*b"co64"), full_box(0, 0, body))
} else {
for chunk in &track.chunks {
body.extend_from_slice(
&u32::try_from(chunk.offset)
.unwrap_or(u32::MAX)
.to_be_bytes(),
);
}
make_box(FourCc(*b"stco"), full_box(0, 0, body))
}
}