use std::io::{Cursor, Seek, SeekFrom, Write};
use oxideav_core::{Error, MediaType, Packet, Result, StreamInfo};
use oxideav_core::{Muxer, WriteSeek};
use crate::options::{BrandPreset, Mp4MuxerOptions};
use crate::sample_entries::{sample_entry_for, SampleEntry};
struct TrackState {
stream: StreamInfo,
media_time_scale: u32,
sample_entry: SampleEntry,
sample_sizes: Vec<u32>,
stts: Vec<(u32, u32)>,
keyframes: Vec<u32>,
chunk_offsets: Vec<u64>,
stsc: Vec<(u32, u32, u32)>,
current_chunk_samples: u32,
current_chunk_start_offset: u64,
samples_per_chunk_target: u32,
chunk_count: u32,
last_samples_per_chunk: u32,
next_sample_index: u32,
cumulative_duration: u64,
prev_pts_in_ts: Option<i64>,
first_pts_in_ts: Option<i64>,
}
impl TrackState {
fn new(stream: StreamInfo, sample_entry: SampleEntry) -> Self {
let media_time_scale = match stream.params.media_type {
MediaType::Audio => stream.params.sample_rate.unwrap_or(1000),
_ => 1000,
};
Self {
stream,
media_time_scale,
sample_entry,
sample_sizes: Vec::new(),
stts: Vec::new(),
keyframes: Vec::new(),
chunk_offsets: Vec::new(),
stsc: Vec::new(),
current_chunk_samples: 0,
current_chunk_start_offset: 0,
samples_per_chunk_target: 1,
chunk_count: 0,
last_samples_per_chunk: 0,
next_sample_index: 0,
cumulative_duration: 0,
prev_pts_in_ts: None,
first_pts_in_ts: None,
}
}
fn close_current_chunk(&mut self) {
if self.current_chunk_samples == 0 {
return;
}
let spc = self.current_chunk_samples;
self.chunk_count += 1;
if spc != self.last_samples_per_chunk {
self.stsc.push((self.chunk_count, spc, 1));
self.last_samples_per_chunk = spc;
}
self.current_chunk_samples = 0;
}
}
pub fn open(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
open_with_options(output, streams, Mp4MuxerOptions::default())
}
pub fn open_mov(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
let opts = Mp4MuxerOptions {
brand: BrandPreset::Mov,
..Mp4MuxerOptions::default()
};
open_with_options(output, streams, opts)
}
pub fn open_ismv(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
let opts = Mp4MuxerOptions {
brand: BrandPreset::Ismv,
..Mp4MuxerOptions::default()
};
open_with_options(output, streams, opts)
}
pub fn open_with_options(
output: Box<dyn WriteSeek>,
streams: &[StreamInfo],
options: Mp4MuxerOptions,
) -> Result<Box<dyn Muxer>> {
if streams.is_empty() {
return Err(Error::invalid("mp4 muxer: need at least one stream"));
}
let mut tracks = Vec::with_capacity(streams.len());
for s in streams {
let entry = sample_entry_for(&s.params)?;
tracks.push(TrackState::new(s.clone(), entry));
}
Ok(Box::new(Mp4Muxer {
output,
tracks,
options,
ftyp_bytes: Vec::new(),
mdat_size_offset: 0,
mdat_start_offset: 0,
mdat_bytes: 0,
mdat_buffer: None,
header_written: false,
trailer_written: false,
}))
}
struct Mp4Muxer {
output: Box<dyn WriteSeek>,
tracks: Vec<TrackState>,
options: Mp4MuxerOptions,
ftyp_bytes: Vec<u8>,
mdat_size_offset: u64,
mdat_start_offset: u64,
mdat_bytes: u64,
mdat_buffer: Option<Cursor<Vec<u8>>>,
header_written: bool,
trailer_written: bool,
}
impl Muxer for Mp4Muxer {
fn format_name(&self) -> &str {
match self.options.brand {
BrandPreset::Mov => "mov",
BrandPreset::Ismv => "ismv",
_ => "mp4",
}
}
fn write_header(&mut self) -> Result<()> {
if self.header_written {
return Err(Error::other("mp4 muxer: write_header called twice"));
}
let major = self.options.brand.major_brand();
let compat = self.options.brand.compatible_brands();
let mut ftyp_body = Vec::with_capacity(8 + 4 * compat.len());
ftyp_body.extend_from_slice(&major);
let minor_version: u32 = match self.options.brand {
BrandPreset::Mp4 => 0x0000_0200,
_ => 0,
};
ftyp_body.extend_from_slice(&minor_version.to_be_bytes());
for b in &compat {
ftyp_body.extend_from_slice(b);
}
let ftyp = wrap_box(b"ftyp", &ftyp_body);
self.ftyp_bytes = ftyp.clone();
if self.options.faststart {
self.output.write_all(&ftyp)?;
self.mdat_start_offset = 0;
self.mdat_size_offset = 0;
self.mdat_buffer = Some(Cursor::new(Vec::new()));
} else {
self.output.write_all(&ftyp)?;
let pos = self.output.stream_position()?;
self.mdat_size_offset = pos;
self.output.write_all(&[0, 0, 0, 0])?;
self.output.write_all(b"mdat")?;
self.mdat_start_offset = self.output.stream_position()?;
}
for t in &mut self.tracks {
t.samples_per_chunk_target = default_samples_per_chunk(&t.stream);
t.current_chunk_start_offset = self.mdat_start_offset;
}
self.header_written = true;
Ok(())
}
fn write_packet(&mut self, packet: &Packet) -> Result<()> {
if !self.header_written {
return Err(Error::other("mp4 muxer: write_header not called"));
}
let idx = packet.stream_index as usize;
if idx >= self.tracks.len() {
return Err(Error::invalid(format!(
"mp4 muxer: unknown stream index {idx}"
)));
}
let cur_offset = self.mdat_start_offset + self.mdat_bytes;
if let Some(buf) = self.mdat_buffer.as_mut() {
buf.get_mut().extend_from_slice(&packet.data);
} else {
self.output.write_all(&packet.data)?;
}
self.mdat_bytes += packet.data.len() as u64;
let t = &mut self.tracks[idx];
let pts_in_ts = packet
.pts
.map(|v| rescale_to_media_ts(v, packet.time_base, t.media_time_scale));
let delta = compute_delta(t, packet, pts_in_ts);
t.sample_sizes.push(packet.data.len() as u32);
match t.stts.last_mut() {
Some((count, d)) if *d == delta => *count += 1,
_ => t.stts.push((1, delta)),
}
t.cumulative_duration += delta as u64;
if packet.flags.keyframe {
t.keyframes.push(t.next_sample_index + 1);
}
if t.current_chunk_samples == 0 {
t.chunk_offsets.push(cur_offset);
t.current_chunk_start_offset = cur_offset;
}
t.current_chunk_samples += 1;
if t.current_chunk_samples >= t.samples_per_chunk_target {
t.close_current_chunk();
}
if let Some(p) = pts_in_ts {
if t.first_pts_in_ts.is_none() {
t.first_pts_in_ts = Some(p);
}
t.prev_pts_in_ts = Some(p);
} else {
let base = t.prev_pts_in_ts.unwrap_or(0);
t.prev_pts_in_ts = Some(base + delta as i64);
if t.first_pts_in_ts.is_none() {
t.first_pts_in_ts = Some(0);
}
}
t.next_sample_index += 1;
Ok(())
}
fn write_trailer(&mut self) -> Result<()> {
if self.trailer_written {
return Ok(());
}
if !self.header_written {
return Err(Error::other("mp4 muxer: write_trailer before write_header"));
}
for t in &mut self.tracks {
t.close_current_chunk();
}
if self.options.faststart {
self.write_trailer_faststart()?;
} else {
self.write_trailer_direct()?;
}
self.output.flush()?;
self.trailer_written = true;
Ok(())
}
}
impl Mp4Muxer {
fn write_trailer_direct(&mut self) -> Result<()> {
let end_pos = self.output.stream_position()?;
let mdat_total = end_pos - self.mdat_size_offset;
if mdat_total <= u32::MAX as u64 {
self.output.seek(SeekFrom::Start(self.mdat_size_offset))?;
self.output.write_all(&(mdat_total as u32).to_be_bytes())?;
self.output.seek(SeekFrom::Start(end_pos))?;
} else {
return Err(Error::unsupported(
"mp4 muxer: mdat > 4 GiB requires largesize header (not yet supported)",
));
}
let moov = build_moov(&self.tracks)?;
self.output.write_all(&moov)?;
Ok(())
}
fn write_trailer_faststart(&mut self) -> Result<()> {
let ftyp_size = self.ftyp_bytes.len() as u64;
let mdat_header_size: u64 = 8;
let mdat_payload = self
.mdat_buffer
.take()
.map(|c| c.into_inner())
.unwrap_or_default();
let mdat_total = mdat_header_size + mdat_payload.len() as u64;
if mdat_total > u32::MAX as u64 {
return Err(Error::unsupported(
"mp4 muxer: faststart mdat > 4 GiB requires largesize header (not yet supported)",
));
}
let orig_offsets: Vec<Vec<u64>> = self
.tracks
.iter()
.map(|t| t.chunk_offsets.clone())
.collect();
let mut moov_size: u64 = 0;
let mut moov_bytes: Vec<u8> = Vec::new();
for attempt in 0..4 {
let base = ftyp_size + moov_size + mdat_header_size;
for (t, orig) in self.tracks.iter_mut().zip(orig_offsets.iter()) {
t.chunk_offsets = orig.iter().map(|o| *o + base).collect();
}
let candidate = build_moov(&self.tracks)?;
let candidate_size = candidate.len() as u64;
let converged = candidate_size == moov_size;
moov_size = candidate_size;
moov_bytes = candidate;
if converged {
break;
}
if attempt == 3 {
return Err(Error::other(
"mp4 muxer: faststart moov size did not converge",
));
}
}
self.output.seek(SeekFrom::Start(ftyp_size))?;
self.output.write_all(&moov_bytes)?;
self.output.write_all(&(mdat_total as u32).to_be_bytes())?;
self.output.write_all(b"mdat")?;
self.output.write_all(&mdat_payload)?;
Ok(())
}
}
fn build_moov(tracks: &[TrackState]) -> Result<Vec<u8>> {
let movie_timescale: u32 = 1000;
let mut movie_duration: u64 = 0;
for (i, t) in tracks.iter().enumerate() {
let track_duration_movie =
rescale_u64(t.cumulative_duration, t.media_time_scale, movie_timescale);
if track_duration_movie > movie_duration {
movie_duration = track_duration_movie;
}
let _ = i;
}
let next_track_id = (tracks.len() as u32) + 1;
let mut moov_body = Vec::new();
moov_body.extend_from_slice(&build_mvhd(movie_timescale, movie_duration, next_track_id));
for (i, t) in tracks.iter().enumerate() {
moov_body.extend_from_slice(&build_trak(i as u32 + 1, t, movie_timescale)?);
}
Ok(wrap_box(b"moov", &moov_body))
}
fn build_mvhd(timescale: u32, duration: u64, next_track_id: u32) -> Vec<u8> {
let use_v1 = duration > u32::MAX as u64;
let mut body = Vec::with_capacity(120);
if use_v1 {
body.push(1); body.extend_from_slice(&[0, 0, 0]); 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.push(0); body.extend_from_slice(&[0, 0, 0]); 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(&(duration as u32).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, 0]); body.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0]); let identity: [u32; 9] = [0x0001_0000, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000];
for v in identity {
body.extend_from_slice(&v.to_be_bytes());
}
body.extend_from_slice(&[0u8; 24]);
body.extend_from_slice(&next_track_id.to_be_bytes());
wrap_box(b"mvhd", &body)
}
fn build_trak(track_id: u32, t: &TrackState, movie_timescale: u32) -> Result<Vec<u8>> {
let mut body = Vec::new();
let track_duration_movie =
rescale_u64(t.cumulative_duration, t.media_time_scale, movie_timescale);
body.extend_from_slice(&build_tkhd(track_id, track_duration_movie, &t.stream));
body.extend_from_slice(&build_mdia(t)?);
Ok(wrap_box(b"trak", &body))
}
fn build_tkhd(track_id: u32, duration: u64, stream: &StreamInfo) -> Vec<u8> {
let use_v1 = duration > u32::MAX as u64;
let mut body = Vec::new();
let flags: u32 = 0x0000_0007; if use_v1 {
body.push(1);
body.extend_from_slice(&flags.to_be_bytes()[1..4]);
body.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(&track_id.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&duration.to_be_bytes());
} else {
body.push(0);
body.extend_from_slice(&flags.to_be_bytes()[1..4]);
body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&track_id.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&(duration as u32).to_be_bytes());
}
body.extend_from_slice(&[0u8; 8]); body.extend_from_slice(&0i16.to_be_bytes()); body.extend_from_slice(&0i16.to_be_bytes()); let volume: u16 = if stream.params.media_type == MediaType::Audio {
0x0100
} else {
0
};
body.extend_from_slice(&volume.to_be_bytes());
body.extend_from_slice(&[0, 0]); let identity: [u32; 9] = [0x0001_0000, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000];
for v in identity {
body.extend_from_slice(&v.to_be_bytes());
}
let (w, h) = match stream.params.media_type {
MediaType::Video => (
(stream.params.width.unwrap_or(0)) << 16,
(stream.params.height.unwrap_or(0)) << 16,
),
_ => (0, 0),
};
body.extend_from_slice(&w.to_be_bytes());
body.extend_from_slice(&h.to_be_bytes());
wrap_box(b"tkhd", &body)
}
fn build_mdia(t: &TrackState) -> Result<Vec<u8>> {
let mut body = Vec::new();
body.extend_from_slice(&build_mdhd(t));
body.extend_from_slice(&build_hdlr(&t.stream));
body.extend_from_slice(&build_minf(t)?);
Ok(wrap_box(b"mdia", &body))
}
fn build_mdhd(t: &TrackState) -> Vec<u8> {
let duration = t.cumulative_duration;
let use_v1 = duration > u32::MAX as u64;
let mut body = Vec::new();
if use_v1 {
body.push(1);
body.extend_from_slice(&[0, 0, 0]); body.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(&t.media_time_scale.to_be_bytes());
body.extend_from_slice(&duration.to_be_bytes());
} else {
body.push(0);
body.extend_from_slice(&[0, 0, 0]); body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&t.media_time_scale.to_be_bytes());
body.extend_from_slice(&(duration as u32).to_be_bytes());
}
let lang = pack_language(b"und");
body.extend_from_slice(&lang.to_be_bytes());
body.extend_from_slice(&0u16.to_be_bytes());
wrap_box(b"mdhd", &body)
}
fn pack_language(code: &[u8; 3]) -> u16 {
let a = (code[0].saturating_sub(0x60) & 0x1F) as u16;
let b = (code[1].saturating_sub(0x60) & 0x1F) as u16;
let c = (code[2].saturating_sub(0x60) & 0x1F) as u16;
(a << 10) | (b << 5) | c
}
fn build_hdlr(stream: &StreamInfo) -> Vec<u8> {
let (handler_type, name): (&[u8; 4], &str) = match stream.params.media_type {
MediaType::Audio => (b"soun", "SoundHandler"),
MediaType::Video => (b"vide", "VideoHandler"),
_ => (b"data", "DataHandler"),
};
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(handler_type);
body.extend_from_slice(&[0u8; 12]); body.extend_from_slice(name.as_bytes());
body.push(0); wrap_box(b"hdlr", &body)
}
fn build_minf(t: &TrackState) -> Result<Vec<u8>> {
let mut body = Vec::new();
match t.stream.params.media_type {
MediaType::Audio => body.extend_from_slice(&build_smhd()),
MediaType::Video => body.extend_from_slice(&build_vmhd()),
_ => body.extend_from_slice(&build_nmhd()),
}
body.extend_from_slice(&build_dinf());
body.extend_from_slice(&build_stbl(t)?);
Ok(wrap_box(b"minf", &body))
}
fn build_smhd() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&0i16.to_be_bytes()); body.extend_from_slice(&[0, 0]); wrap_box(b"smhd", &body)
}
fn build_vmhd() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 1]); body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&[0u8; 6]); wrap_box(b"vmhd", &body)
}
fn build_nmhd() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); wrap_box(b"nmhd", &body)
}
fn build_dinf() -> Vec<u8> {
let mut dref_body = Vec::new();
dref_body.extend_from_slice(&[0, 0, 0, 0]); dref_body.extend_from_slice(&1u32.to_be_bytes()); let mut url_body = Vec::new();
url_body.extend_from_slice(&[0, 0, 0, 1]);
dref_body.extend_from_slice(&wrap_box(b"url ", &url_body));
let dref = wrap_box(b"dref", &dref_body);
wrap_box(b"dinf", &dref)
}
fn build_stbl(t: &TrackState) -> Result<Vec<u8>> {
let mut body = Vec::new();
body.extend_from_slice(&build_stsd(t));
body.extend_from_slice(&build_stts(&t.stts));
if !t.keyframes.is_empty() {
body.extend_from_slice(&build_stss(&t.keyframes));
}
body.extend_from_slice(&build_stsc(&t.stsc));
body.extend_from_slice(&build_stsz(&t.sample_sizes));
body.extend_from_slice(&build_chunk_offset_box(&t.chunk_offsets));
Ok(wrap_box(b"stbl", &body))
}
fn build_stsd(t: &TrackState) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&wrap_box(&t.sample_entry.fourcc, &t.sample_entry.body));
wrap_box(b"stsd", &body)
}
fn build_stts(runs: &[(u32, u32)]) -> Vec<u8> {
let mut body = Vec::with_capacity(8 + runs.len() * 8);
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&(runs.len() as u32).to_be_bytes());
for (count, delta) in runs {
body.extend_from_slice(&count.to_be_bytes());
body.extend_from_slice(&delta.to_be_bytes());
}
wrap_box(b"stts", &body)
}
fn build_stss(keyframes: &[u32]) -> Vec<u8> {
let mut body = Vec::with_capacity(8 + keyframes.len() * 4);
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&(keyframes.len() as u32).to_be_bytes());
for k in keyframes {
body.extend_from_slice(&k.to_be_bytes());
}
wrap_box(b"stss", &body)
}
fn build_stsc(runs: &[(u32, u32, u32)]) -> Vec<u8> {
let mut body = Vec::with_capacity(8 + runs.len() * 12);
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&(runs.len() as u32).to_be_bytes());
for (fc, spc, sdi) in runs {
body.extend_from_slice(&fc.to_be_bytes());
body.extend_from_slice(&spc.to_be_bytes());
body.extend_from_slice(&sdi.to_be_bytes());
}
wrap_box(b"stsc", &body)
}
fn build_stsz(sizes: &[u32]) -> Vec<u8> {
let mut body = Vec::with_capacity(12 + sizes.len() * 4);
body.extend_from_slice(&[0, 0, 0, 0]); let uniform = if !sizes.is_empty() && sizes.iter().all(|&s| s == sizes[0]) {
sizes[0]
} else {
0
};
body.extend_from_slice(&uniform.to_be_bytes());
body.extend_from_slice(&(sizes.len() as u32).to_be_bytes());
if uniform == 0 {
for s in sizes {
body.extend_from_slice(&s.to_be_bytes());
}
}
wrap_box(b"stsz", &body)
}
fn build_chunk_offset_box(offsets: &[u64]) -> Vec<u8> {
let needs_64 = offsets.iter().any(|&o| o > u32::MAX as u64);
if needs_64 {
let mut body = Vec::with_capacity(8 + offsets.len() * 8);
body.extend_from_slice(&[0, 0, 0, 0]);
body.extend_from_slice(&(offsets.len() as u32).to_be_bytes());
for o in offsets {
body.extend_from_slice(&o.to_be_bytes());
}
wrap_box(b"co64", &body)
} else {
let mut body = Vec::with_capacity(8 + offsets.len() * 4);
body.extend_from_slice(&[0, 0, 0, 0]);
body.extend_from_slice(&(offsets.len() as u32).to_be_bytes());
for o in offsets {
body.extend_from_slice(&(*o as u32).to_be_bytes());
}
wrap_box(b"stco", &body)
}
}
fn wrap_box(kind: &[u8; 4], body: &[u8]) -> Vec<u8> {
let total = (8 + body.len()) as u32;
let mut out = Vec::with_capacity(total as usize);
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(kind);
out.extend_from_slice(body);
out
}
fn default_samples_per_chunk(stream: &StreamInfo) -> u32 {
match stream.params.media_type {
MediaType::Audio => {
if stream.params.codec_id.as_str().starts_with("pcm_") {
1
} else {
50
}
}
MediaType::Video => 1,
_ => 1,
}
}
fn compute_delta(t: &TrackState, packet: &Packet, pts_in_ts: Option<i64>) -> u32 {
if let Some(d) = packet.duration {
if d > 0 {
let v = rescale_to_media_ts(d, packet.time_base, t.media_time_scale);
if v > 0 {
return v as u32;
}
}
}
if let (Some(prev), Some(cur)) = (t.prev_pts_in_ts, pts_in_ts) {
let d = (cur - prev).max(0);
if d > 0 {
return d as u32;
}
}
1
}
fn rescale_to_media_ts(value: i64, from_tb: oxideav_core::TimeBase, to_ts: u32) -> i64 {
let from_r = from_tb.as_rational();
if from_r.den == 0 || to_ts == 0 {
return value;
}
let num = from_r.num as i128 * to_ts as i128;
let den = from_r.den as i128;
let prod = value as i128 * num;
(prod / den) as i64
}
fn rescale_u64(value: u64, from_ts: u32, to_ts: u32) -> u64 {
if from_ts == 0 {
return value;
}
let prod = value as u128 * to_ts as u128;
(prod / from_ts as u128) as u64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wrap_box_header_layout() {
let b = wrap_box(b"test", &[1, 2, 3]);
assert_eq!(&b[0..4], &[0, 0, 0, 11]);
assert_eq!(&b[4..8], b"test");
assert_eq!(&b[8..], &[1, 2, 3]);
}
#[test]
fn chunk_offset_auto_promotes_to_co64() {
let offsets = [(u32::MAX as u64) + 1];
let b = build_chunk_offset_box(&offsets);
assert_eq!(&b[4..8], b"co64");
}
#[test]
fn chunk_offset_stays_stco_when_small() {
let offsets = [42u64, 1234, 999_999];
let b = build_chunk_offset_box(&offsets);
assert_eq!(&b[4..8], b"stco");
}
#[test]
fn pack_language_und() {
assert_eq!(pack_language(b"und"), ((21u16) << 10) | ((14u16) << 5) | 4);
}
}