use std::fmt;
use std::io::{self, Write};
use crate::api::{AacProfile, AudioCodec, Metadata, VideoCodec};
use crate::assert_invariant;
use crate::codec::av1::{extract_av1_config, Av1Config};
use crate::codec::h264::{annexb_to_avcc, default_avc_config, extract_avc_config, AvcConfig};
use crate::codec::h265::{extract_hevc_config, hevc_annexb_to_hvcc, HevcConfig};
use crate::codec::opus::{is_valid_opus_packet, OpusConfig, OPUS_SAMPLE_RATE};
use crate::codec::vp9::{extract_vp9_config, Vp9Config};
const MOVIE_TIMESCALE: u32 = 1000;
pub const MEDIA_TIMESCALE: u32 = 90_000;
#[derive(Clone, Debug)]
pub enum VideoConfig {
Avc(AvcConfig),
Hevc(HevcConfig),
Av1(Av1Config),
Vp9(Vp9Config),
}
pub struct Mp4Writer<Writer> {
writer: Writer,
video_codec: VideoCodec,
video_samples: Vec<SampleInfo>,
video_prev_pts: Option<u64>,
video_last_delta: Option<u32>,
video_config: Option<VideoConfig>,
audio_track: Option<Mp4AudioTrack>,
audio_samples: Vec<SampleInfo>,
audio_prev_pts: Option<u64>,
audio_last_delta: Option<u32>,
finalized: bool,
bytes_written: u64,
}
pub struct Mp4VideoTrack {
pub width: u32,
pub height: u32,
}
pub struct Mp4AudioTrack {
pub sample_rate: u32,
pub channels: u16,
pub codec: AudioCodec,
}
struct SampleInfo {
pts: u64,
dts: u64, data: Vec<u8>,
is_keyframe: bool,
duration: Option<u32>,
}
struct SampleTables {
durations: Vec<u32>,
sizes: Vec<u32>,
keyframes: Vec<u32>,
chunk_offsets: Vec<u32>,
samples_per_chunk: u32,
cts_offsets: Vec<i32>, has_bframes: bool, }
impl SampleTables {
fn from_samples(
samples: &[SampleInfo],
chunk_offsets: Vec<u32>,
samples_per_chunk: u32,
fallback_duration: Option<u32>,
) -> Self {
let sample_count = samples.len() as u32;
let mut durations = Vec::with_capacity(sample_count as usize);
for (idx, sample) in samples.iter().enumerate() {
let duration = sample.duration.unwrap_or_else(|| {
if idx == samples.len() - 1 {
fallback_duration.unwrap_or(1)
} else {
1
}
});
durations.push(duration);
}
let sizes = samples
.iter()
.map(|sample| sample.data.len() as u32)
.collect();
let keyframes = samples
.iter()
.enumerate()
.filter_map(|(idx, sample)| {
if sample.is_keyframe {
Some(idx as u32 + 1)
} else {
None
}
})
.collect();
let mut has_bframes = false;
let cts_offsets: Vec<i32> = samples
.iter()
.map(|sample| {
let offset = (sample.pts as i64 - sample.dts as i64) as i32;
if offset != 0 {
has_bframes = true;
}
offset
})
.collect();
let _ = sample_count;
Self {
durations,
sizes,
keyframes,
chunk_offsets,
samples_per_chunk,
cts_offsets,
has_bframes,
}
}
fn total_duration(&self) -> u64 {
self.durations.iter().map(|&d| d as u64).sum()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum ErrorSeverity {
Error,
Warning,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct AdtsValidationError {
pub kind: AdtsErrorKind,
pub severity: ErrorSeverity,
pub byte_offset: usize,
pub expected: Option<String>,
pub found: Option<String>,
pub hex_dump: Option<String>,
pub suggestion: Option<String>,
pub code_example: Option<String>,
pub technical_details: Option<String>,
pub related_errors: Vec<AdtsValidationError>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub enum AdtsErrorKind {
FrameTooShort,
MissingSyncword,
InvalidFrameLength,
InvalidHeaderLength,
InvalidMpegVersion,
InvalidLayer,
InvalidSampleRateIndex,
InvalidChannelConfig,
CrcMismatch,
}
impl fmt::Display for AdtsValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let severity_icon = match self.severity {
ErrorSeverity::Error => "🚨",
ErrorSeverity::Warning => "⚠️",
};
write!(f, "{} ", severity_icon)?;
match &self.kind {
AdtsErrorKind::FrameTooShort => {
write!(
f,
"ADTS frame too short: need at least 7 bytes for header, got {}",
self.byte_offset
)?;
}
AdtsErrorKind::MissingSyncword => {
write!(
f,
"ADTS syncword missing at byte {}: expected 0xFFF in first 12 bits",
self.byte_offset
)?;
if let (Some(_expected), Some(found)) = (&self.expected, &self.found) {
write!(f, " (expected {}, found {})", _expected, found)?;
}
}
AdtsErrorKind::InvalidFrameLength => {
write!(
f,
"ADTS frame length invalid at byte {}: ",
self.byte_offset
)?;
if let (Some(_expected), Some(found)) = (&self.expected, &self.found) {
write!(f, "expected {}, found {}", _expected, found)?;
}
write!(
f,
" (frame length must be >= header length and <= total frame size)"
)?;
}
AdtsErrorKind::InvalidHeaderLength => {
write!(
f,
"ADTS header length mismatch at byte {}: ",
self.byte_offset
)?;
if let (Some(expected), Some(found)) = (&self.expected, &self.found) {
write!(f, "expected header length {}, found {}", expected, found)?;
}
write!(f, " (check protection_absent flag)")?;
}
AdtsErrorKind::InvalidMpegVersion => {
write!(
f,
"ADTS MPEG version invalid at byte {}: ",
self.byte_offset
)?;
if let (Some(_expected), Some(found)) = (&self.expected, &self.found) {
write!(f, "expected {}, found {}", _expected, found)?;
}
write!(f, " (only MPEG-4 AAC is supported)")?;
}
AdtsErrorKind::InvalidLayer => {
write!(f, "ADTS layer field invalid at byte {}: ", self.byte_offset)?;
if let (Some(_expected), Some(found)) = (&self.expected, &self.found) {
write!(f, "expected {}, found {}", _expected, found)?;
}
write!(f, " (must be 0 for AAC)")?;
}
AdtsErrorKind::InvalidSampleRateIndex => {
write!(
f,
"ADTS sample rate index invalid at byte {}: ",
self.byte_offset
)?;
if let (Some(_expected), Some(found)) = (&self.expected, &self.found) {
write!(f, "expected 0-12, found {}", found)?;
}
write!(f, " (valid range is 0-12 corresponding to 96000-7350 Hz)")?;
}
AdtsErrorKind::InvalidChannelConfig => {
write!(
f,
"ADTS channel configuration invalid at byte {}: ",
self.byte_offset
)?;
if let (Some(_expected), Some(found)) = (&self.expected, &self.found) {
write!(f, "expected 1-7, found {}", found)?;
}
write!(f, " (valid range is 1-7 for mono/stereo configurations)")?;
}
AdtsErrorKind::CrcMismatch => {
write!(f, "ADTS CRC mismatch at byte {}: ", self.byte_offset)?;
write!(f, "frame data doesn't match CRC checksum")?;
}
}
if let Some(hex) = &self.hex_dump {
write!(f, "\n Hex dump: {}", hex)?;
}
if let Some(suggestion) = &self.suggestion {
write!(f, "\n Suggestion: {}", suggestion)?;
}
if let Some(code) = &self.code_example {
write!(f, "\n Code example: {}", code)?;
}
if f.alternate() {
if let Some(tech) = &self.technical_details {
write!(f, "\n🔍 Technical details: {}", tech)?;
}
}
if !self.related_errors.is_empty() {
write!(f, "\n\n📋 Related errors in this frame:")?;
for (i, related) in self.related_errors.iter().enumerate() {
write!(f, "\n {}. {}", i + 1, related)?;
}
}
Ok(())
}
}
impl AdtsValidationError {
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn to_json_compact(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn is_critical(&self) -> bool {
matches!(self.severity, ErrorSeverity::Error)
}
pub fn all_errors(&self) -> Vec<&AdtsValidationError> {
let mut result = vec![self];
for related in &self.related_errors {
result.extend(related.all_errors());
}
result
}
}
#[derive(Debug)]
pub enum Mp4WriterError {
NonIncreasingTimestamp,
FirstFrameMustBeKeyframe,
FirstFrameMissingSpsPps,
FirstFrameMissingSequenceHeader,
FirstFrameMissingVp9Config,
#[allow(dead_code)]
InvalidAdts,
InvalidAdtsDetailed(Box<AdtsValidationError>),
InvalidOpusPacket,
AudioNotEnabled,
DurationOverflow,
AlreadyFinalized,
}
impl fmt::Display for Mp4WriterError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Mp4WriterError::NonIncreasingTimestamp => write!(f, "timestamps must grow"),
Mp4WriterError::FirstFrameMustBeKeyframe => {
write!(f, "first frame must be a keyframe")
}
Mp4WriterError::FirstFrameMissingSpsPps => {
write!(f, "first frame must contain SPS/PPS")
}
Mp4WriterError::FirstFrameMissingSequenceHeader => {
write!(f, "first AV1 frame must contain Sequence Header OBU")
}
Mp4WriterError::FirstFrameMissingVp9Config => {
write!(f, "first VP9 frame must contain valid frame header")
}
Mp4WriterError::InvalidAdts => write!(f, "invalid ADTS frame"),
Mp4WriterError::InvalidAdtsDetailed(err) => write!(f, "{}", err),
Mp4WriterError::InvalidOpusPacket => write!(f, "invalid Opus packet"),
Mp4WriterError::AudioNotEnabled => write!(f, "audio track not enabled"),
Mp4WriterError::DurationOverflow => write!(f, "sample duration overflow"),
Mp4WriterError::AlreadyFinalized => write!(f, "writer already finalised"),
}
}
}
impl std::error::Error for Mp4WriterError {}
impl<Writer: Write> Mp4Writer<Writer> {
pub fn new(writer: Writer, video_codec: VideoCodec) -> Self {
Self {
writer,
video_codec,
video_samples: Vec::new(),
video_prev_pts: None,
video_last_delta: None,
video_config: None,
audio_track: None,
audio_samples: Vec::new(),
audio_prev_pts: None,
audio_last_delta: None,
finalized: false,
bytes_written: 0,
}
}
pub(crate) fn video_sample_count(&self) -> u64 {
self.video_samples.len() as u64
}
pub(crate) fn audio_sample_count(&self) -> u64 {
self.audio_samples.len() as u64
}
pub(crate) fn bytes_written(&self) -> u64 {
self.bytes_written
}
pub(crate) fn max_end_pts(&self) -> Option<u64> {
fn track_end(samples: &[SampleInfo], last_delta: Option<u32>) -> Option<u64> {
let last = samples.last()?;
Some(last.pts + u64::from(last_delta.unwrap_or(0)))
}
let video_end = track_end(&self.video_samples, self.video_last_delta);
let audio_end = track_end(&self.audio_samples, self.audio_last_delta);
match (video_end, audio_end) {
(Some(v), Some(a)) => Some(v.max(a)),
(Some(v), None) => Some(v),
(None, Some(a)) => Some(a),
(None, None) => None,
}
}
fn write_counted(writer: &mut Writer, bytes_written: &mut u64, buf: &[u8]) -> io::Result<()> {
*bytes_written = bytes_written.saturating_add(buf.len() as u64);
writer.write_all(buf)
}
pub fn enable_audio(&mut self, track: Mp4AudioTrack) {
self.audio_track = Some(track);
}
pub fn write_video_sample(
&mut self,
pts: u64,
data: &[u8],
is_keyframe: bool,
) -> Result<(), Mp4WriterError> {
self.write_video_sample_with_dts(pts, pts, data, is_keyframe)
}
pub fn write_video_sample_with_dts(
&mut self,
pts: u64,
dts: u64,
data: &[u8],
is_keyframe: bool,
) -> Result<(), Mp4WriterError> {
if self.finalized {
return Err(Mp4WriterError::AlreadyFinalized);
}
if let Some(prev) = self.video_prev_pts {
if dts <= prev {
return Err(Mp4WriterError::NonIncreasingTimestamp);
}
let delta = dts - prev;
if delta > u64::from(u32::MAX) {
return Err(Mp4WriterError::DurationOverflow);
}
let delta = delta as u32;
if let Some(last) = self.video_samples.last_mut() {
last.duration = Some(delta);
}
self.video_last_delta = Some(delta);
} else {
if !is_keyframe {
return Err(Mp4WriterError::FirstFrameMustBeKeyframe);
}
let config = match self.video_codec {
VideoCodec::H264 => extract_avc_config(data).map(VideoConfig::Avc),
VideoCodec::H265 => extract_hevc_config(data).map(VideoConfig::Hevc),
VideoCodec::Av1 => extract_av1_config(data).map(VideoConfig::Av1),
VideoCodec::Vp9 => extract_vp9_config(data).map(VideoConfig::Vp9),
};
if config.is_none() {
return Err(match self.video_codec {
VideoCodec::Av1 => Mp4WriterError::FirstFrameMissingSequenceHeader,
VideoCodec::Vp9 => Mp4WriterError::FirstFrameMissingVp9Config,
_ => Mp4WriterError::FirstFrameMissingSpsPps,
});
}
self.video_config = config;
}
let converted = match self.video_codec {
VideoCodec::H264 => annexb_to_avcc(data),
VideoCodec::H265 => hevc_annexb_to_hvcc(data),
VideoCodec::Av1 => data.to_vec(), VideoCodec::Vp9 => data.to_vec(), };
if converted.len() > u32::MAX as usize {
return Err(Mp4WriterError::DurationOverflow);
}
self.video_samples.push(SampleInfo {
pts,
dts,
data: converted,
is_keyframe,
duration: None,
});
self.video_prev_pts = Some(dts); Ok(())
}
pub fn write_audio_sample(&mut self, pts: u64, data: &[u8]) -> Result<(), Mp4WriterError> {
if self.finalized {
return Err(Mp4WriterError::AlreadyFinalized);
}
let audio_track = self
.audio_track
.as_ref()
.ok_or(Mp4WriterError::AudioNotEnabled)?;
if let Some(prev) = self.audio_prev_pts {
if pts < prev {
return Err(Mp4WriterError::NonIncreasingTimestamp);
}
let delta = pts - prev;
if delta > u64::from(u32::MAX) {
return Err(Mp4WriterError::DurationOverflow);
}
let delta = delta as u32;
if let Some(last) = self.audio_samples.last_mut() {
last.duration = Some(delta);
}
self.audio_last_delta = Some(delta);
}
let sample_data = match audio_track.codec {
AudioCodec::Aac(profile) => {
assert_invariant!(
matches!(
profile,
AacProfile::Lc
| AacProfile::Main
| AacProfile::Ssr
| AacProfile::Ltp
| AacProfile::He
| AacProfile::Hev2
),
"INV-020: AAC profile must be one of the supported variants",
"aac audio processing"
);
let raw = adts_to_raw(data)
.map_err(|e| Mp4WriterError::InvalidAdtsDetailed(Box::new(e)))?;
raw.to_vec()
}
AudioCodec::Opus => {
if !is_valid_opus_packet(data) {
return Err(Mp4WriterError::InvalidOpusPacket);
}
data.to_vec()
}
AudioCodec::None => {
return Err(Mp4WriterError::AudioNotEnabled);
}
};
if sample_data.len() > u32::MAX as usize {
return Err(Mp4WriterError::DurationOverflow);
}
self.audio_samples.push(SampleInfo {
pts,
dts: pts, data: sample_data,
is_keyframe: false,
duration: None,
});
self.audio_prev_pts = Some(pts);
Ok(())
}
pub fn finalize(
&mut self,
video: &Mp4VideoTrack,
metadata: Option<&Metadata>,
fast_start: bool,
) -> io::Result<()> {
if self.finalized {
return Err(io::Error::other("mp4 writer already finalised"));
}
self.finalized = true;
let video_config = self
.video_config
.clone()
.or_else(|| {
if self.video_samples.is_empty() {
match self.video_codec {
VideoCodec::H264 => Some(VideoConfig::Avc(default_avc_config())),
VideoCodec::H265 => None, VideoCodec::Av1 => None, VideoCodec::Vp9 => None, }
} else {
None
}
})
.unwrap_or_else(|| VideoConfig::Avc(default_avc_config()));
if fast_start {
self.finalize_fast_start(video, metadata, &video_config)
} else {
self.finalize_standard(video, metadata, &video_config)
}
}
fn finalize_standard(
&mut self,
video: &Mp4VideoTrack,
metadata: Option<&Metadata>,
video_config: &VideoConfig,
) -> io::Result<()> {
let ftyp_box = build_ftyp_box();
let ftyp_len = ftyp_box.len() as u32;
Self::write_counted(&mut self.writer, &mut self.bytes_written, &ftyp_box)?;
let audio_present = self.audio_track.is_some();
if !audio_present {
let chunk_offset = if !self.video_samples.is_empty() {
let mut payload_size: u32 = 0;
for sample in &self.video_samples {
payload_size += sample.data.len() as u32;
}
let mdat_size = 8 + payload_size;
Self::write_counted(
&mut self.writer,
&mut self.bytes_written,
&mdat_size.to_be_bytes(),
)?;
Self::write_counted(&mut self.writer, &mut self.bytes_written, b"mdat")?;
for sample in &self.video_samples {
Self::write_counted(&mut self.writer, &mut self.bytes_written, &sample.data)?;
}
Some(ftyp_len + 8)
} else {
None
};
let (chunk_offsets, samples_per_chunk) = match chunk_offset {
Some(offset) => (vec![offset], self.video_samples.len() as u32),
None => (Vec::new(), 0),
};
let tables = SampleTables::from_samples(
&self.video_samples,
chunk_offsets,
samples_per_chunk,
self.video_last_delta,
);
let moov_box = build_moov_box(video, &tables, None, video_config, metadata);
return Self::write_counted(&mut self.writer, &mut self.bytes_written, &moov_box);
}
let mut total_payload_size: u32 = 0;
for sample in &self.video_samples {
total_payload_size += sample.data.len() as u32;
}
for sample in &self.audio_samples {
total_payload_size += sample.data.len() as u32;
}
let mdat_size = 8 + total_payload_size;
Self::write_counted(
&mut self.writer,
&mut self.bytes_written,
&mdat_size.to_be_bytes(),
)?;
Self::write_counted(&mut self.writer, &mut self.bytes_written, b"mdat")?;
let schedule = self.compute_interleave_schedule();
let mut video_chunk_offsets = Vec::with_capacity(self.video_samples.len());
let mut audio_chunk_offsets = Vec::with_capacity(self.audio_samples.len());
let mut cursor = ftyp_len + 8;
for (_, kind, idx) in schedule {
match kind {
TrackKind::Video => {
video_chunk_offsets.push(cursor);
let sample = &self.video_samples[idx];
let sample_len = sample.data.len() as u32;
Self::write_counted(&mut self.writer, &mut self.bytes_written, &sample.data)?;
cursor += sample_len;
}
TrackKind::Audio => {
audio_chunk_offsets.push(cursor);
let sample = &self.audio_samples[idx];
let sample_len = sample.data.len() as u32;
Self::write_counted(&mut self.writer, &mut self.bytes_written, &sample.data)?;
cursor += sample_len;
}
}
}
let video_tables = SampleTables::from_samples(
&self.video_samples,
video_chunk_offsets,
1,
self.video_last_delta,
);
let audio_tables = SampleTables::from_samples(
&self.audio_samples,
audio_chunk_offsets,
1,
self.audio_last_delta,
);
let audio_track = self
.audio_track
.as_ref()
.expect("audio_present implies track");
let moov_box = build_moov_box(
video,
&video_tables,
Some((audio_track, &audio_tables)),
video_config,
metadata,
);
Self::write_counted(&mut self.writer, &mut self.bytes_written, &moov_box)
}
fn finalize_fast_start(
&mut self,
video: &Mp4VideoTrack,
metadata: Option<&Metadata>,
video_config: &VideoConfig,
) -> io::Result<()> {
let ftyp_box = build_ftyp_box();
let ftyp_len = ftyp_box.len() as u32;
let mut mdat_payload_size: u32 = 0;
for sample in &self.video_samples {
mdat_payload_size += sample.data.len() as u32;
}
for sample in &self.audio_samples {
mdat_payload_size += sample.data.len() as u32;
}
let mdat_header_size = 8u32;
let mdat_total_size = mdat_header_size + mdat_payload_size;
let audio_present = self.audio_track.is_some();
let (placeholder_video_tables, placeholder_audio_tables) = if audio_present {
let schedule = self.compute_interleave_schedule();
let mut video_offsets = Vec::with_capacity(self.video_samples.len());
let mut audio_offsets = Vec::with_capacity(self.audio_samples.len());
let mut cursor = 0u32;
for (_, kind, _) in &schedule {
match kind {
TrackKind::Video => {
video_offsets.push(cursor);
cursor += 1; }
TrackKind::Audio => {
audio_offsets.push(cursor);
cursor += 1; }
}
}
let video_tables = SampleTables::from_samples(
&self.video_samples,
video_offsets,
1,
self.video_last_delta,
);
let audio_tables = SampleTables::from_samples(
&self.audio_samples,
audio_offsets,
1,
self.audio_last_delta,
);
(video_tables, Some(audio_tables))
} else {
let chunk_offsets = if self.video_samples.is_empty() {
Vec::new()
} else {
vec![0u32] };
let samples_per_chunk = if self.video_samples.is_empty() {
0
} else {
self.video_samples.len() as u32
};
let video_tables = SampleTables::from_samples(
&self.video_samples,
chunk_offsets,
samples_per_chunk,
self.video_last_delta,
);
(video_tables, None)
};
let placeholder_moov = if let Some(ref audio_tables) = placeholder_audio_tables {
let audio_track = self.audio_track.as_ref().unwrap();
build_moov_box(
video,
&placeholder_video_tables,
Some((audio_track, audio_tables)),
video_config,
metadata,
)
} else {
build_moov_box(
video,
&placeholder_video_tables,
None,
video_config,
metadata,
)
};
let moov_len = placeholder_moov.len() as u32;
let mdat_data_start = ftyp_len + moov_len + mdat_header_size;
let (final_video_tables, final_audio_tables) = if audio_present {
let schedule = self.compute_interleave_schedule();
let mut video_offsets = Vec::with_capacity(self.video_samples.len());
let mut audio_offsets = Vec::with_capacity(self.audio_samples.len());
let mut cursor = mdat_data_start;
for (_, kind, idx) in &schedule {
match kind {
TrackKind::Video => {
video_offsets.push(cursor);
cursor += self.video_samples[*idx].data.len() as u32;
}
TrackKind::Audio => {
audio_offsets.push(cursor);
cursor += self.audio_samples[*idx].data.len() as u32;
}
}
}
let video_tables = SampleTables::from_samples(
&self.video_samples,
video_offsets,
1,
self.video_last_delta,
);
let audio_tables = SampleTables::from_samples(
&self.audio_samples,
audio_offsets,
1,
self.audio_last_delta,
);
(video_tables, Some(audio_tables))
} else {
let chunk_offsets = if self.video_samples.is_empty() {
Vec::new()
} else {
vec![mdat_data_start]
};
let samples_per_chunk = if self.video_samples.is_empty() {
0
} else {
self.video_samples.len() as u32
};
let video_tables = SampleTables::from_samples(
&self.video_samples,
chunk_offsets,
samples_per_chunk,
self.video_last_delta,
);
(video_tables, None)
};
let final_moov = if let Some(ref audio_tables) = final_audio_tables {
let audio_track = self.audio_track.as_ref().unwrap();
build_moov_box(
video,
&final_video_tables,
Some((audio_track, audio_tables)),
video_config,
metadata,
)
} else {
build_moov_box(video, &final_video_tables, None, video_config, metadata)
};
Self::write_counted(&mut self.writer, &mut self.bytes_written, &ftyp_box)?;
Self::write_counted(&mut self.writer, &mut self.bytes_written, &final_moov)?;
Self::write_counted(
&mut self.writer,
&mut self.bytes_written,
&mdat_total_size.to_be_bytes(),
)?;
Self::write_counted(&mut self.writer, &mut self.bytes_written, b"mdat")?;
if audio_present {
let schedule = self.compute_interleave_schedule();
for (_, kind, idx) in schedule {
match kind {
TrackKind::Video => {
Self::write_counted(
&mut self.writer,
&mut self.bytes_written,
&self.video_samples[idx].data,
)?;
}
TrackKind::Audio => {
Self::write_counted(
&mut self.writer,
&mut self.bytes_written,
&self.audio_samples[idx].data,
)?;
}
}
}
} else {
for sample in &self.video_samples {
Self::write_counted(&mut self.writer, &mut self.bytes_written, &sample.data)?;
}
}
Ok(())
}
fn compute_interleave_schedule(&self) -> Vec<(u64, TrackKind, usize)> {
let mut schedule: Vec<(u64, TrackKind, usize)> = Vec::new();
for (idx, sample) in self.video_samples.iter().enumerate() {
schedule.push((sample.pts, TrackKind::Video, idx));
}
for (idx, sample) in self.audio_samples.iter().enumerate() {
schedule.push((sample.pts, TrackKind::Audio, idx));
}
schedule.sort_by_key(|(pts, kind, idx)| {
let kind_order = match kind {
TrackKind::Video => 0u8,
TrackKind::Audio => 1u8,
};
(*pts, kind_order, *idx)
});
schedule
}
}
#[derive(Clone, Copy)]
enum TrackKind {
Video,
Audio,
}
#[allow(clippy::result_large_err)]
fn adts_to_raw(frame: &[u8]) -> Result<&[u8], AdtsValidationError> {
let create_hex_dump = |offset: usize, len: usize| -> String {
let start = offset.saturating_sub(8).min(frame.len());
let end = (offset + len + 8).min(frame.len());
let slice = &frame[start..end];
let mut hex = String::new();
let mut ascii = String::new();
for (i, &byte) in slice.iter().enumerate() {
let global_offset = start + i;
if global_offset == offset {
hex.push_str(&format!("\x1b[91m{:02x}*\x1b[0m ", byte));
} else if global_offset >= offset && global_offset < offset + len {
hex.push_str(&format!("\x1b[93m{:02x}\x1b[0m ", byte)); } else {
hex.push_str(&format!("{:02x} ", byte));
}
let ascii_char = if byte.is_ascii_graphic() {
byte as char
} else {
'.'
};
if global_offset == offset {
ascii.push_str(&format!("\x1b[91m{}\x1b[0m", ascii_char));
} else if global_offset >= offset && global_offset < offset + len {
ascii.push_str(&format!("\x1b[93m{}\x1b[0m", ascii_char));
} else {
ascii.push(ascii_char);
}
if (i + 1) % 16 == 0 {
hex.push_str(&format!(" |{}|\n", ascii));
ascii.clear();
}
}
if !ascii.is_empty() {
while hex.chars().filter(|&c| c != '\x1b').count() % (16 * 3) != 0 {
hex.push(' ');
}
hex.push_str(&format!(" |{}|", ascii));
}
format!("Hex dump around byte {}:\n{}", offset, hex)
};
if frame.len() < 7 {
return Err(AdtsValidationError {
kind: AdtsErrorKind::FrameTooShort,
severity: ErrorSeverity::Error,
byte_offset: frame.len(),
expected: Some("≥7 bytes for ADTS header".to_string()),
found: Some(format!("{} bytes", frame.len())),
hex_dump: Some(create_hex_dump(0, frame.len())),
suggestion: Some("Ensure you're passing complete ADTS frames. Check if the audio data is truncated or corrupted during transmission.".to_string()),
code_example: Some("Ensure your audio frame buffer contains the complete ADTS frame before calling write_audio().".to_string()),
technical_details: Some("ADTS header requires minimum 7 bytes: syncword (2 bytes), MPEG info (1 byte), frame length (3 bytes partial), buffer fullness (2 bytes partial).".to_string()),
related_errors: Vec::new(),
});
}
let syncword = ((frame[0] as u16) << 4) | ((frame[1] as u16) >> 4);
if syncword != 0xFFF {
return Err(AdtsValidationError {
kind: AdtsErrorKind::MissingSyncword,
severity: ErrorSeverity::Error,
byte_offset: 0,
expected: Some("0xFFF (12-bit syncword)".to_string()),
found: Some(format!("0x{:03X}", syncword)),
hex_dump: Some(create_hex_dump(0, 2)),
suggestion: Some("This doesn't appear to be an ADTS frame. Check if you're passing raw AAC data instead of ADTS-wrapped frames, or if the data is corrupted.".to_string()),
code_example: Some("Check frame starts with ADTS syncword: if (frame[0] & 0xFF) == 0xFF && (frame[1] & 0xF0) == 0xF0 { /* valid ADTS */ }".to_string()),
technical_details: Some("ADTS syncword is 0xFFF (all 1s in first 12 bits). If this is raw AAC, use AudioCodec::Aac without ADTS framing.".to_string()),
related_errors: Vec::new(),
});
}
let mpeg_version = (frame[1] >> 3) & 0x01;
if mpeg_version != 0 {
return Err(AdtsValidationError {
kind: AdtsErrorKind::InvalidMpegVersion,
severity: ErrorSeverity::Error,
byte_offset: 1,
expected: Some("0 (MPEG-4)".to_string()),
found: Some(format!("{} (MPEG-2)", mpeg_version)),
hex_dump: Some(create_hex_dump(1, 1)),
suggestion: Some(
"Muxide only supports MPEG-4 AAC. Convert your audio to MPEG-4 AAC format."
.to_string(),
),
code_example: Some(
"Use ffmpeg: ffmpeg -i input.mp3 -c:a aac -profile:a aac_low output.m4a"
.to_string(),
),
technical_details: Some(
"MPEG version bit: 0=MPEG-4, 1=MPEG-2. Muxide requires MPEG-4 AAC.".to_string(),
),
related_errors: Vec::new(),
});
}
let layer = (frame[1] >> 1) & 0x03;
if layer != 0 {
return Err(AdtsValidationError {
kind: AdtsErrorKind::InvalidLayer,
severity: ErrorSeverity::Error,
byte_offset: 1,
expected: Some("0 (AAC)".to_string()),
found: Some(format!("{} (Layer {})", layer, layer)),
hex_dump: Some(create_hex_dump(1, 1)),
suggestion: Some(
"This appears to be MP3 or other MPEG audio format. Convert to AAC format."
.to_string(),
),
code_example: Some(
"Convert MP3 to AAC: ffmpeg -i input.mp3 -c:a aac -b:a 128k output.m4a".to_string(),
),
technical_details: Some(
"Layer field: 00=AAC, 01=Layer3, 10=Layer2, 11=Layer1. AAC requires 00."
.to_string(),
),
related_errors: Vec::new(),
});
}
let protection_absent = (frame[1] & 0x01) != 0;
let header_len = if protection_absent { 7 } else { 9 };
if frame.len() < header_len {
return Err(AdtsValidationError {
kind: AdtsErrorKind::InvalidHeaderLength,
severity: ErrorSeverity::Error,
byte_offset: 1,
expected: Some(format!("≥{} bytes (protection_absent={})", header_len, protection_absent)),
found: Some(format!("{} bytes", frame.len())),
hex_dump: Some(create_hex_dump(0, frame.len())),
suggestion: Some(format!("Frame is too short for {} header. Check if CRC protection is present and adjust header length calculation.", if protection_absent { "unprotected" } else { "protected" })),
code_example: None,
technical_details: Some(format!("Header length: 7 bytes (no CRC) or 9 bytes (with CRC). protection_absent bit: {}", protection_absent)),
related_errors: Vec::new(),
});
}
let profile = (frame[2] >> 6) & 0x03;
let _profile_name = match profile {
0 => "Main",
1 => "LC (Low Complexity)",
2 => "SSR (Scalable Sample Rate)",
3 => "LTP (Long Term Prediction)",
_ => "Unknown",
};
let sample_rate_idx = (frame[2] >> 2) & 0x0F;
if sample_rate_idx > 12 {
return Err(AdtsValidationError {
kind: AdtsErrorKind::InvalidSampleRateIndex,
severity: ErrorSeverity::Error,
byte_offset: 2,
expected: Some("0-12 (96000-7350 Hz)".to_string()),
found: Some(format!("{} (invalid)", sample_rate_idx)),
hex_dump: Some(create_hex_dump(2, 1)),
suggestion: Some("Invalid sample rate index. Valid values: 0=96000, 1=88200, 2=64000, 3=48000, 4=44100, 5=32000, 6=24000, 7=22050, 8=16000, 9=12000, 10=11025, 11=8000, 12=7350 Hz.".to_string()),
code_example: Some("Common AAC sample rates: 44100 Hz (index 4), 48000 Hz (index 3), 22050 Hz (index 7)".to_string()),
technical_details: Some("Sample rate index is 4 bits (0-12). Values 13-15 are reserved.".to_string()),
related_errors: Vec::new(),
});
}
let channel_config = ((frame[2] & 0x01) << 2) | ((frame[3] >> 6) & 0x03);
if channel_config == 0 || channel_config > 7 {
return Err(AdtsValidationError {
kind: AdtsErrorKind::InvalidChannelConfig,
severity: ErrorSeverity::Error,
byte_offset: 2,
expected: Some("1-7 (mono to 7.1 surround)".to_string()),
found: Some(format!("{} (invalid)", channel_config)),
hex_dump: Some(create_hex_dump(2, 2)),
suggestion: Some("Invalid channel configuration. For stereo use 2, for mono use 1. Values 0 and 8+ are reserved.".to_string()),
code_example: Some("AAC channel configs: 1=mono, 2=stereo. Use AudioCodec::Aac(AacProfile::Lc) for 2-channel stereo.".to_string()),
technical_details: Some("Channel config: 1=mono, 2=stereo, 3=3.0, 4=4.0, 5=5.0, 6=5.1, 7=7.1. 0=implicit, 8+=reserved.".to_string()),
related_errors: Vec::new(),
});
}
let aac_frame_length: usize = (((frame[3] & 0x03) as usize) << 11)
| ((frame[4] as usize) << 3)
| (((frame[5] & 0xE0) as usize) >> 5);
if aac_frame_length < header_len {
return Err(AdtsValidationError {
kind: AdtsErrorKind::InvalidFrameLength,
severity: ErrorSeverity::Error,
byte_offset: 3,
expected: Some(format!("≥{} (header length)", header_len)),
found: Some(format!("{} (too small)", aac_frame_length)),
hex_dump: Some(create_hex_dump(3, 3)),
suggestion: Some("Frame length is smaller than header. This indicates corrupted frame length field. Check bytes 3-5.".to_string()),
code_example: None,
technical_details: Some(format!("Frame length (13 bits): includes header + payload. Must be ≥{} for {} header.", header_len, if protection_absent { "unprotected" } else { "protected" })),
related_errors: Vec::new(),
});
}
if aac_frame_length > frame.len() {
return Err(AdtsValidationError {
kind: AdtsErrorKind::InvalidFrameLength,
severity: ErrorSeverity::Error,
byte_offset: 3,
expected: Some(format!("≤{} (available data)", frame.len())),
found: Some(format!("{} (too large)", aac_frame_length)),
hex_dump: Some(create_hex_dump(3, 3)),
suggestion: Some("Frame length exceeds available data. Frame may be truncated or frame length field corrupted.".to_string()),
code_example: None,
technical_details: Some(format!("Frame length {} > buffer size {}. Check if frame is complete.", aac_frame_length, frame.len())),
related_errors: Vec::new(),
});
}
if !protection_absent && frame.len() >= header_len + 2 {
let crc_start = header_len - 2;
if frame.len() < crc_start + 2 {
return Err(AdtsValidationError {
kind: AdtsErrorKind::CrcMismatch,
severity: ErrorSeverity::Error,
byte_offset: crc_start,
expected: Some("2 CRC bytes".to_string()),
found: Some(format!(
"{} bytes available",
frame.len().saturating_sub(crc_start)
)),
hex_dump: Some(create_hex_dump(
crc_start,
frame.len().saturating_sub(crc_start),
)),
suggestion: Some(
"CRC protection is enabled but CRC bytes are missing or truncated.".to_string(),
),
code_example: None,
technical_details: Some(
"CRC is 16 bits stored after header when protection_absent=0.".to_string(),
),
related_errors: Vec::new(),
});
}
}
Ok(&frame[header_len..aac_frame_length])
}
fn build_moov_box(
video: &Mp4VideoTrack,
video_tables: &SampleTables,
audio: Option<(&Mp4AudioTrack, &SampleTables)>,
video_config: &VideoConfig,
metadata: Option<&Metadata>,
) -> Vec<u8> {
let video_duration_media = video_tables.total_duration();
let video_duration_ms =
(video_duration_media * MOVIE_TIMESCALE as u64 / MEDIA_TIMESCALE as u64) as u32;
let mvhd_payload = build_mvhd_payload(video_duration_ms);
let mvhd_box = build_box(b"mvhd", &mvhd_payload);
let trak_box = build_trak_box(video, video_tables, video_config, metadata);
let mut payload = Vec::new();
payload.extend_from_slice(&mvhd_box);
payload.extend_from_slice(&trak_box);
if let Some((audio_track, audio_tables)) = audio {
let audio_trak = build_audio_trak_box(audio_track, audio_tables, metadata);
payload.extend_from_slice(&audio_trak);
}
if let Some(meta) = metadata {
let udta_box = build_udta_box(meta);
if !udta_box.is_empty() {
payload.extend_from_slice(&udta_box);
}
}
build_box(b"moov", &payload)
}
fn build_audio_trak_box(
audio: &Mp4AudioTrack,
tables: &SampleTables,
metadata: Option<&Metadata>,
) -> Vec<u8> {
let tkhd_box = build_audio_tkhd_box();
let mdia_box = build_audio_mdia_box(audio, tables, metadata);
let mut payload = Vec::new();
payload.extend_from_slice(&tkhd_box);
payload.extend_from_slice(&mdia_box);
build_box(b"trak", &payload)
}
fn build_audio_tkhd_box() -> Vec<u8> {
build_tkhd_box_with_id(2, 0x0100, 0, 0)
}
fn build_audio_mdia_box(
audio: &Mp4AudioTrack,
tables: &SampleTables,
metadata: Option<&Metadata>,
) -> Vec<u8> {
let duration = tables.total_duration();
let language = metadata.and_then(|m| m.language.as_deref());
let mdhd_box = build_mdhd_box_with_timescale_and_duration(MEDIA_TIMESCALE, duration, language);
let hdlr_box = build_sound_hdlr_box();
let minf_box = build_audio_minf_box(audio, tables);
let mut payload = Vec::new();
payload.extend_from_slice(&mdhd_box);
payload.extend_from_slice(&hdlr_box);
payload.extend_from_slice(&minf_box);
build_box(b"mdia", &payload)
}
fn build_audio_minf_box(audio: &Mp4AudioTrack, tables: &SampleTables) -> Vec<u8> {
let smhd_box = build_smhd_box();
let dinf_box = build_dinf_box();
let stbl_box = build_audio_stbl_box(audio, tables);
let mut payload = Vec::new();
payload.extend_from_slice(&smhd_box);
payload.extend_from_slice(&dinf_box);
payload.extend_from_slice(&stbl_box);
build_box(b"minf", &payload)
}
fn build_audio_stbl_box(audio: &Mp4AudioTrack, tables: &SampleTables) -> Vec<u8> {
let stsd_box = build_audio_stsd_box(audio);
let stts_box = build_stts_box(&tables.durations);
let stsc_box = build_stsc_box(tables.samples_per_chunk, tables.chunk_offsets.len() as u32);
let stsz_box = build_stsz_box(&tables.sizes);
let stco_box = build_stco_box(&tables.chunk_offsets);
let mut payload = Vec::new();
payload.extend_from_slice(&stsd_box);
payload.extend_from_slice(&stts_box);
payload.extend_from_slice(&stsc_box);
payload.extend_from_slice(&stsz_box);
payload.extend_from_slice(&stco_box);
build_box(b"stbl", &payload)
}
fn build_audio_stsd_box(audio: &Mp4AudioTrack) -> Vec<u8> {
let sample_entry_box = match audio.codec {
AudioCodec::Aac(_) => build_mp4a_box(audio),
AudioCodec::Opus => build_opus_box(audio),
AudioCodec::None => build_mp4a_box(audio), };
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&1u32.to_be_bytes());
payload.extend_from_slice(&sample_entry_box);
build_box(b"stsd", &payload)
}
fn build_mp4a_box(audio: &Mp4AudioTrack) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&[0u8; 6]);
payload.extend_from_slice(&1u16.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&audio.channels.to_be_bytes());
payload.extend_from_slice(&16u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
let rate_fixed = audio.sample_rate << 16;
payload.extend_from_slice(&rate_fixed.to_be_bytes());
let esds = build_esds_box(audio);
payload.extend_from_slice(&esds);
build_box(b"mp4a", &payload)
}
fn build_esds_box(audio: &Mp4AudioTrack) -> Vec<u8> {
let asc = build_audio_specific_config(audio.sample_rate, audio.channels);
let mut dec_specific = Vec::new();
dec_specific.push(0x05);
dec_specific.push(asc.len() as u8);
dec_specific.extend_from_slice(&asc);
let mut dec_config_payload = Vec::new();
dec_config_payload.push(0x40);
dec_config_payload.push(0x15);
dec_config_payload.extend_from_slice(&[0x00, 0x00, 0x00]);
dec_config_payload.extend_from_slice(&0u32.to_be_bytes());
dec_config_payload.extend_from_slice(&0u32.to_be_bytes());
dec_config_payload.extend_from_slice(&dec_specific);
let mut dec_config = Vec::new();
dec_config.push(0x04);
dec_config.push(dec_config_payload.len() as u8);
dec_config.extend_from_slice(&dec_config_payload);
let sl_config = [0x06u8, 0x01u8, 0x02u8];
let mut es_payload = Vec::new();
es_payload.extend_from_slice(&1u16.to_be_bytes());
es_payload.push(0);
es_payload.extend_from_slice(&dec_config);
es_payload.extend_from_slice(&sl_config);
let mut es_desc = Vec::new();
es_desc.push(0x03);
es_desc.push(es_payload.len() as u8);
es_desc.extend_from_slice(&es_payload);
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&es_desc);
build_box(b"esds", &payload)
}
fn build_audio_specific_config(sample_rate: u32, channels: u16) -> [u8; 2] {
let sfi = match sample_rate {
96000 => 0,
88200 => 1,
64000 => 2,
48000 => 3,
44100 => 4,
32000 => 5,
24000 => 6,
22050 => 7,
16000 => 8,
12000 => 9,
11025 => 10,
8000 => 11,
7350 => 12,
_ => 4,
};
let aot = 2u8;
let chan = (channels.min(15) as u8) & 0x0f;
let byte0 = (aot << 3) | (sfi >> 1);
let byte1 = ((sfi & 1) << 7) | (chan << 3);
[byte0, byte1]
}
fn build_opus_box(audio: &Mp4AudioTrack) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&[0u8; 6]);
payload.extend_from_slice(&1u16.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&audio.channels.to_be_bytes());
payload.extend_from_slice(&16u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
let rate_fixed = OPUS_SAMPLE_RATE << 16;
payload.extend_from_slice(&rate_fixed.to_be_bytes());
let dops = build_dops_box(audio);
payload.extend_from_slice(&dops);
build_box(b"Opus", &payload)
}
fn build_dops_box(audio: &Mp4AudioTrack) -> Vec<u8> {
let config = OpusConfig::default().with_channels(audio.channels as u8);
let mut payload = Vec::new();
payload.push(config.version);
payload.push(config.output_channel_count);
payload.extend_from_slice(&config.pre_skip.to_be_bytes());
payload.extend_from_slice(&config.input_sample_rate.to_be_bytes());
payload.extend_from_slice(&config.output_gain.to_be_bytes());
payload.push(config.channel_mapping_family);
if config.channel_mapping_family != 0 {
payload.push(config.stream_count.unwrap_or(1));
payload.push(config.coupled_count.unwrap_or(0));
if let Some(mapping) = &config.channel_mapping {
payload.extend_from_slice(mapping);
} else {
for i in 0..config.output_channel_count {
payload.push(i);
}
}
}
build_box(b"dOps", &payload)
}
fn build_trak_box(
video: &Mp4VideoTrack,
tables: &SampleTables,
video_config: &VideoConfig,
metadata: Option<&Metadata>,
) -> Vec<u8> {
let tkhd_box = build_tkhd_box(video);
let mdia_box = build_mdia_box(video, tables, video_config, metadata);
let mut payload = Vec::new();
payload.extend_from_slice(&tkhd_box);
payload.extend_from_slice(&mdia_box);
build_box(b"trak", &payload)
}
fn build_mdia_box(
video: &Mp4VideoTrack,
tables: &SampleTables,
video_config: &VideoConfig,
metadata: Option<&Metadata>,
) -> Vec<u8> {
let duration = tables.total_duration();
let language = metadata.and_then(|m| m.language.as_deref());
let mdhd_box = build_mdhd_box_with_timescale_and_duration(MEDIA_TIMESCALE, duration, language);
let hdlr_box = build_hdlr_box();
let minf_box = build_minf_box(video, tables, video_config);
let mut payload = Vec::new();
payload.extend_from_slice(&mdhd_box);
payload.extend_from_slice(&hdlr_box);
payload.extend_from_slice(&minf_box);
build_box(b"mdia", &payload)
}
fn build_minf_box(
video: &Mp4VideoTrack,
tables: &SampleTables,
video_config: &VideoConfig,
) -> Vec<u8> {
let vmhd_box = build_vmhd_box();
let dinf_box = build_dinf_box();
let stbl_box = build_stbl_box(video, tables, video_config);
let mut payload = Vec::new();
payload.extend_from_slice(&vmhd_box);
payload.extend_from_slice(&dinf_box);
payload.extend_from_slice(&stbl_box);
build_box(b"minf", &payload)
}
fn build_stbl_box(
video: &Mp4VideoTrack,
tables: &SampleTables,
video_config: &VideoConfig,
) -> Vec<u8> {
let stsd_box = build_stsd_box(video, video_config);
let stts_box = build_stts_box(&tables.durations);
let stsc_box = build_stsc_box(tables.samples_per_chunk, tables.chunk_offsets.len() as u32);
let stsz_box = build_stsz_box(&tables.sizes);
let stco_box = build_stco_box(&tables.chunk_offsets);
let mut payload = Vec::new();
payload.extend_from_slice(&stsd_box);
payload.extend_from_slice(&stts_box);
if tables.has_bframes {
let ctts_box = build_ctts_box(&tables.cts_offsets);
payload.extend_from_slice(&ctts_box);
}
payload.extend_from_slice(&stsc_box);
payload.extend_from_slice(&stsz_box);
payload.extend_from_slice(&stco_box);
if !tables.keyframes.is_empty() {
let stss_box = build_stss_box(&tables.keyframes);
payload.extend_from_slice(&stss_box);
}
build_box(b"stbl", &payload)
}
fn build_stsd_box(video: &Mp4VideoTrack, video_config: &VideoConfig) -> Vec<u8> {
let sample_entry = match video_config {
VideoConfig::Avc(avc_config) => build_avc1_box(video, avc_config),
VideoConfig::Hevc(hevc_config) => build_hvc1_box(video, hevc_config),
VideoConfig::Av1(av1_config) => build_av01_box(video, av1_config),
VideoConfig::Vp9(vp9_config) => build_vp09_box(video, vp9_config),
};
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&1u32.to_be_bytes());
payload.extend_from_slice(&sample_entry);
build_box(b"stsd", &payload)
}
fn build_stts_box(durations: &[u32]) -> Vec<u8> {
let mut entries: Vec<(u32, u32)> = Vec::new();
for &duration in durations {
if let Some(last) = entries.last_mut() {
if last.1 == duration {
last.0 += 1;
continue;
}
}
entries.push((1u32, duration));
}
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&(entries.len() as u32).to_be_bytes());
for (count, delta) in entries {
payload.extend_from_slice(&count.to_be_bytes());
payload.extend_from_slice(&delta.to_be_bytes());
}
build_box(b"stts", &payload)
}
fn build_stsc_box(samples_per_chunk: u32, chunk_count: u32) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
if chunk_count == 0 || samples_per_chunk == 0 {
payload.extend_from_slice(&0u32.to_be_bytes());
return build_box(b"stsc", &payload);
}
payload.extend_from_slice(&1u32.to_be_bytes());
payload.extend_from_slice(&1u32.to_be_bytes());
payload.extend_from_slice(&samples_per_chunk.to_be_bytes());
payload.extend_from_slice(&1u32.to_be_bytes());
build_box(b"stsc", &payload)
}
fn build_stsz_box(sizes: &[u32]) -> Vec<u8> {
for (i, &size) in sizes.iter().enumerate() {
assert_invariant!(
size > 0,
"No empty samples in stsz",
&format!("build_stsz_box[{}]", i)
);
}
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&(sizes.len() as u32).to_be_bytes());
for size in sizes {
payload.extend_from_slice(&size.to_be_bytes());
}
build_box(b"stsz", &payload)
}
fn build_stco_box(chunk_offsets: &[u32]) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&(chunk_offsets.len() as u32).to_be_bytes());
for offset in chunk_offsets {
payload.extend_from_slice(&offset.to_be_bytes());
}
build_box(b"stco", &payload)
}
fn build_stss_box(keyframes: &[u32]) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&(keyframes.len() as u32).to_be_bytes());
for index in keyframes {
payload.extend_from_slice(&index.to_be_bytes());
}
build_box(b"stss", &payload)
}
fn build_ctts_box(cts_offsets: &[i32]) -> Vec<u8> {
let mut entries: Vec<(u32, i32)> = Vec::new();
for &offset in cts_offsets {
if let Some(last) = entries.last_mut() {
if last.1 == offset {
last.0 += 1;
continue;
}
}
entries.push((1, offset));
}
let mut payload = Vec::new();
payload.extend_from_slice(&0x0100_0000_u32.to_be_bytes());
payload.extend_from_slice(&(entries.len() as u32).to_be_bytes());
for (count, offset) in entries {
payload.extend_from_slice(&count.to_be_bytes());
payload.extend_from_slice(&offset.to_be_bytes());
}
build_box(b"ctts", &payload)
}
fn build_avc1_box(video: &Mp4VideoTrack, avc_config: &AvcConfig) -> Vec<u8> {
assert_invariant!(
video.width <= u16::MAX as u32,
"Width must fit in 16-bit",
"build_avc1_box"
);
assert_invariant!(
video.height <= u16::MAX as u32,
"Height must fit in 16-bit",
"build_avc1_box"
);
let mut payload = Vec::new();
payload.extend_from_slice(&[0u8; 6]);
payload.extend_from_slice(&1u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&(video.width as u16).to_be_bytes());
payload.extend_from_slice(&(video.height as u16).to_be_bytes());
payload.extend_from_slice(&0x0048_0000_u32.to_be_bytes());
payload.extend_from_slice(&0x0048_0000_u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&1u16.to_be_bytes());
payload.extend_from_slice(&[0u8; 32]);
payload.extend_from_slice(&0x0018u16.to_be_bytes());
payload.extend_from_slice(&0xffffu16.to_be_bytes());
let avc_c_box = build_avcc_box(avc_config);
payload.extend_from_slice(&avc_c_box);
build_box(b"avc1", &payload)
}
fn build_avcc_box(avc_config: &AvcConfig) -> Vec<u8> {
let mut payload = Vec::new();
let (profile_indication, profile_compat, level_indication) = if avc_config.sps.len() >= 4 {
(avc_config.sps[1], avc_config.sps[2], avc_config.sps[3])
} else {
(0x42, 0x00, 0x1e)
};
payload.push(1);
payload.push(profile_indication);
payload.push(profile_compat);
payload.push(level_indication);
payload.push(0xff);
payload.push(0xe1);
payload.extend_from_slice(&(avc_config.sps.len() as u16).to_be_bytes());
payload.extend_from_slice(&avc_config.sps);
payload.push(1);
payload.extend_from_slice(&(avc_config.pps.len() as u16).to_be_bytes());
payload.extend_from_slice(&avc_config.pps);
build_box(b"avcC", &payload)
}
fn build_hvc1_box(video: &Mp4VideoTrack, hevc_config: &HevcConfig) -> Vec<u8> {
assert_invariant!(
video.width <= u16::MAX as u32,
"Width must fit in 16-bit",
"build_hvc1_box"
);
assert_invariant!(
video.height <= u16::MAX as u32,
"Height must fit in 16-bit",
"build_hvc1_box"
);
let mut payload = Vec::new();
payload.extend_from_slice(&[0u8; 6]);
payload.extend_from_slice(&1u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&(video.width as u16).to_be_bytes());
payload.extend_from_slice(&(video.height as u16).to_be_bytes());
payload.extend_from_slice(&0x0048_0000_u32.to_be_bytes());
payload.extend_from_slice(&0x0048_0000_u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&1u16.to_be_bytes());
payload.extend_from_slice(&[0u8; 32]);
payload.extend_from_slice(&0x0018u16.to_be_bytes());
payload.extend_from_slice(&0xffffu16.to_be_bytes());
let hvcc_box = build_hvcc_box(hevc_config);
payload.extend_from_slice(&hvcc_box);
build_box(b"hvc1", &payload)
}
fn build_hvcc_box(hevc_config: &HevcConfig) -> Vec<u8> {
let mut payload = Vec::new();
let general_profile_space = hevc_config.general_profile_space();
let general_tier_flag = hevc_config.general_tier_flag();
let general_profile_idc = hevc_config.general_profile_idc();
let general_level_idc = hevc_config.general_level_idc();
payload.push(1);
let byte1 = (general_profile_space << 6)
| (if general_tier_flag { 0x20 } else { 0 })
| (general_profile_idc & 0x1f);
payload.push(byte1);
payload.extend_from_slice(&[0x60, 0x00, 0x00, 0x00]);
payload.extend_from_slice(&[0x90, 0x00, 0x00, 0x00, 0x00, 0x00]);
payload.push(general_level_idc);
payload.extend_from_slice(&[0xf0, 0x00]);
payload.push(0xfc);
payload.push(0xfd);
payload.push(0xf8);
payload.push(0xf8);
payload.extend_from_slice(&0u16.to_be_bytes());
payload.push(0x03);
payload.push(3);
payload.push(0x80 | 32); payload.extend_from_slice(&1u16.to_be_bytes()); payload.extend_from_slice(&(hevc_config.vps.len() as u16).to_be_bytes());
payload.extend_from_slice(&hevc_config.vps);
payload.push(0x80 | 33); payload.extend_from_slice(&1u16.to_be_bytes()); payload.extend_from_slice(&(hevc_config.sps.len() as u16).to_be_bytes());
payload.extend_from_slice(&hevc_config.sps);
payload.push(0x80 | 34); payload.extend_from_slice(&1u16.to_be_bytes()); payload.extend_from_slice(&(hevc_config.pps.len() as u16).to_be_bytes());
payload.extend_from_slice(&hevc_config.pps);
build_box(b"hvcC", &payload)
}
fn build_av01_box(video: &Mp4VideoTrack, av1_config: &Av1Config) -> Vec<u8> {
assert_invariant!(
video.width <= u16::MAX as u32,
"Width must fit in 16-bit",
"build_av01_box"
);
assert_invariant!(
video.height <= u16::MAX as u32,
"Height must fit in 16-bit",
"build_av01_box"
);
let mut payload = Vec::new();
payload.extend_from_slice(&[0u8; 6]);
payload.extend_from_slice(&1u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&(video.width as u16).to_be_bytes());
payload.extend_from_slice(&(video.height as u16).to_be_bytes());
payload.extend_from_slice(&0x0048_0000_u32.to_be_bytes());
payload.extend_from_slice(&0x0048_0000_u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&1u16.to_be_bytes());
payload.extend_from_slice(&[0u8; 32]);
payload.extend_from_slice(&0x0018u16.to_be_bytes());
payload.extend_from_slice(&0xffffu16.to_be_bytes());
let av1c_box = build_av1c_box(av1_config);
payload.extend_from_slice(&av1c_box);
build_box(b"av01", &payload)
}
fn build_av1c_box(av1_config: &Av1Config) -> Vec<u8> {
let mut payload = Vec::new();
payload.push(0x81);
let byte1 = ((av1_config.seq_profile & 0x07) << 5) | (av1_config.seq_level_idx & 0x1f);
payload.push(byte1);
let byte2 = ((av1_config.seq_tier & 0x01) << 7)
| (if av1_config.high_bitdepth { 0x40 } else { 0 })
| (if av1_config.twelve_bit { 0x20 } else { 0 })
| (if av1_config.monochrome { 0x10 } else { 0 })
| (if av1_config.chroma_subsampling_x {
0x08
} else {
0
})
| (if av1_config.chroma_subsampling_y {
0x04
} else {
0
})
| (av1_config.chroma_sample_position & 0x03);
payload.push(byte2);
payload.push(0x00);
payload.extend_from_slice(&av1_config.sequence_header);
build_box(b"av1C", &payload)
}
fn build_vp09_box(video: &Mp4VideoTrack, vp9_config: &Vp9Config) -> Vec<u8> {
assert_invariant!(
video.width <= u16::MAX as u32,
"Width must fit in 16-bit",
"build_vp09_box"
);
assert_invariant!(
video.height <= u16::MAX as u32,
"Height must fit in 16-bit",
"build_vp09_box"
);
let mut payload = Vec::new();
payload.extend_from_slice(&[0u8; 6]);
payload.extend_from_slice(&1u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&(video.width as u16).to_be_bytes());
payload.extend_from_slice(&(video.height as u16).to_be_bytes());
payload.extend_from_slice(&0x0048_0000_u32.to_be_bytes());
payload.extend_from_slice(&0x0048_0000_u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&1u16.to_be_bytes());
payload.extend_from_slice(&[0u8; 32]);
payload.extend_from_slice(&0x0018u16.to_be_bytes());
payload.extend_from_slice(&0xffffu16.to_be_bytes());
let vpcc_box = build_vpcc_box(vp9_config);
payload.extend_from_slice(&vpcc_box);
build_box(b"vp09", &payload)
}
fn build_vpcc_box(vp9_config: &Vp9Config) -> Vec<u8> {
let payload = vec![
1, vp9_config.profile, 0, vp9_config.bit_depth, vp9_config.color_space, vp9_config.transfer_function, vp9_config.matrix_coefficients, 0, ];
build_box(b"vpcC", &payload)
}
fn build_vmhd_box() -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
build_box(b"vmhd", &payload)
}
fn build_dinf_box() -> Vec<u8> {
let dref_box = build_dref_box();
build_box(b"dinf", &dref_box)
}
fn build_dref_box() -> Vec<u8> {
let url_box = build_url_box();
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&1u32.to_be_bytes());
payload.extend_from_slice(&url_box);
build_box(b"dref", &payload)
}
fn build_url_box() -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&1u32.to_be_bytes());
build_box(b"url ", &payload)
}
#[allow(dead_code)]
fn build_mdhd_box() -> Vec<u8> {
build_mdhd_box_with_timescale_and_duration(MEDIA_TIMESCALE, 0, None)
}
#[allow(dead_code)]
fn build_mdhd_box_with_timescale(timescale: u32, duration: u64) -> Vec<u8> {
build_mdhd_box_with_timescale_and_duration(timescale, duration, None)
}
fn encode_language_code(language: &str) -> [u8; 2] {
let chars: Vec<char> = language.chars().take(3).collect();
let c1 = chars.first().copied().unwrap_or('u') as u16;
let c2 = chars.get(1).copied().unwrap_or('n') as u16;
let c3 = chars.get(2).copied().unwrap_or('d') as u16;
let packed = ((c1.saturating_sub(0x60) & 0x1F) << 10)
| ((c2.saturating_sub(0x60) & 0x1F) << 5)
| (c3.saturating_sub(0x60) & 0x1F);
packed.to_be_bytes()
}
fn build_mdhd_box_with_timescale_and_duration(
timescale: u32,
duration: u64,
language: Option<&str>,
) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(×cale.to_be_bytes());
payload.extend_from_slice(&(duration as u32).to_be_bytes()); payload.extend_from_slice(&encode_language_code(language.unwrap_or("und"))); payload.extend_from_slice(&0u16.to_be_bytes()); build_box(b"mdhd", &payload)
}
fn build_hdlr_box() -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(b"vide");
payload.extend_from_slice(&[0u8; 12]);
payload.extend_from_slice(b"VideoHandler");
payload.push(0);
build_box(b"hdlr", &payload)
}
fn build_sound_hdlr_box() -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(b"soun");
payload.extend_from_slice(&[0u8; 12]);
payload.extend_from_slice(b"SoundHandler");
payload.push(0);
build_box(b"hdlr", &payload)
}
fn build_smhd_box() -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
build_box(b"smhd", &payload)
}
fn build_tkhd_box(video: &Mp4VideoTrack) -> Vec<u8> {
build_tkhd_box_with_id(1, 0, video.width, video.height)
}
fn build_tkhd_box_with_id(track_id: u32, volume: u16, width: u32, height: u32) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&track_id.to_be_bytes());
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&0u64.to_be_bytes());
payload.extend_from_slice(&0u64.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
payload.extend_from_slice(&volume.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes());
let matrix = [
0x0001_0000_u32,
0,
0,
0,
0x0001_0000_u32,
0,
0,
0,
0x4000_0000_u32,
];
for value in matrix {
payload.extend_from_slice(&value.to_be_bytes());
}
let width_fixed = width << 16;
let height_fixed = height << 16;
payload.extend_from_slice(&width_fixed.to_be_bytes());
payload.extend_from_slice(&height_fixed.to_be_bytes());
build_box(b"tkhd", &payload)
}
fn build_ftyp_box() -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(b"isom");
payload.extend_from_slice(&0x200_u32.to_be_bytes());
payload.extend_from_slice(b"isommp41");
build_box(b"ftyp", &payload)
}
fn build_mvhd_payload(duration_ms: u32) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&MOVIE_TIMESCALE.to_be_bytes()); payload.extend_from_slice(&duration_ms.to_be_bytes()); payload.extend_from_slice(&0x0001_0000_u32.to_be_bytes()); payload.extend_from_slice(&0x0100u16.to_be_bytes()); payload.extend_from_slice(&0u16.to_be_bytes()); payload.extend_from_slice(&0u64.to_be_bytes()); let matrix = [
0x0001_0000_u32,
0,
0,
0,
0x0001_0000_u32,
0,
0,
0,
0x4000_0000_u32,
];
for value in matrix {
payload.extend_from_slice(&value.to_be_bytes());
}
for _ in 0..6 {
payload.extend_from_slice(&0u32.to_be_bytes()); }
payload.extend_from_slice(&2u32.to_be_bytes()); payload
}
fn build_box(typ: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let length = (8 + payload.len()) as u32;
let mut buffer = Vec::with_capacity(payload.len() + 8);
buffer.extend_from_slice(&length.to_be_bytes());
buffer.extend_from_slice(typ);
buffer.extend_from_slice(payload);
assert_invariant!(
buffer.len() == 8 + payload.len(),
"Box size must equal header + payload",
"build_box"
);
buffer
}
fn build_udta_box(metadata: &Metadata) -> Vec<u8> {
let mut ilst_payload = Vec::new();
if let Some(title) = &metadata.title {
ilst_payload.extend_from_slice(&build_ilst_string_item(b"\xa9nam", title));
}
if let Some(creation_time) = metadata.creation_time {
let date_str = format_unix_timestamp(creation_time);
ilst_payload.extend_from_slice(&build_ilst_string_item(b"\xa9day", &date_str));
}
if ilst_payload.is_empty() {
return Vec::new(); }
let ilst_box = build_box(b"ilst", &ilst_payload);
let hdlr_box = build_meta_hdlr_box();
let mut meta_payload = vec![0u8; 4]; meta_payload.extend_from_slice(&hdlr_box);
meta_payload.extend_from_slice(&ilst_box);
let meta_box = build_box(b"meta", &meta_payload);
build_box(b"udta", &meta_box)
}
fn build_ilst_string_item(atom_type: &[u8; 4], value: &str) -> Vec<u8> {
let mut data_payload = Vec::new();
data_payload.extend_from_slice(&[0, 0, 0, 1]); data_payload.extend_from_slice(&[0, 0, 0, 0]); data_payload.extend_from_slice(value.as_bytes());
let data_box = build_box(b"data", &data_payload);
build_box(atom_type, &data_box)
}
fn build_meta_hdlr_box() -> Vec<u8> {
let mut payload = vec![0u8; 4]; payload.extend_from_slice(&[0, 0, 0, 0]); payload.extend_from_slice(b"mdir"); payload.extend_from_slice(b"appl"); payload.extend_from_slice(&[0, 0, 0, 0]); payload.extend_from_slice(&[0, 0, 0, 0]); payload.push(0); build_box(b"hdlr", &payload)
}
fn format_unix_timestamp(unix_secs: u64) -> String {
const SECS_PER_MIN: u64 = 60;
const SECS_PER_HOUR: u64 = 3600;
const SECS_PER_DAY: u64 = 86400;
let days_since_epoch = unix_secs / SECS_PER_DAY;
let remaining_secs = unix_secs % SECS_PER_DAY;
let hours = remaining_secs / SECS_PER_HOUR;
let minutes = (remaining_secs % SECS_PER_HOUR) / SECS_PER_MIN;
let seconds = remaining_secs % SECS_PER_MIN;
let (year, month, day) = days_to_ymd(days_since_epoch);
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
year, month, day, hours, minutes, seconds
)
}
fn days_to_ymd(days: u64) -> (u32, u32, u32) {
let mut remaining_days = days as i64;
let mut year = 1970u32;
loop {
let days_in_year = if is_leap_year(year) { 366 } else { 365 };
if remaining_days < days_in_year {
break;
}
remaining_days -= days_in_year;
year += 1;
}
let days_in_months: [i64; 12] = if is_leap_year(year) {
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut month = 1u32;
for &days_in_month in &days_in_months {
if remaining_days < days_in_month {
break;
}
remaining_days -= days_in_month;
month += 1;
}
let day = (remaining_days + 1) as u32;
(year, month, day)
}
fn is_leap_year(year: u32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn h264_keyframe() -> Vec<u8> {
vec![
0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1e, 0xda, 0x02, 0x80, 0x2d, 0x8b, 0x11,
0x00, 0x00, 0x00, 0x01, 0x68, 0xce, 0x38, 0x80, 0x00, 0x00, 0x00, 0x01, 0x65, 0xaa,
0xbb, 0xcc, 0xdd,
]
}
#[test]
fn mp4_writer_error_display_covers_all_variants() {
let variants = [
Mp4WriterError::NonIncreasingTimestamp,
Mp4WriterError::FirstFrameMustBeKeyframe,
Mp4WriterError::FirstFrameMissingSpsPps,
Mp4WriterError::FirstFrameMissingSequenceHeader,
Mp4WriterError::InvalidAdts,
Mp4WriterError::InvalidAdtsDetailed(Box::new(AdtsValidationError {
kind: AdtsErrorKind::FrameTooShort,
severity: ErrorSeverity::Error,
byte_offset: 5,
expected: Some("≥7 bytes for ADTS header".to_string()),
found: Some("5 bytes".to_string()),
hex_dump: Some("00 01 02 03 04* 05 06 07 08 09 (showing bytes 0-9)".to_string()),
suggestion: Some("Ensure you're passing complete ADTS frames. Check if the audio data is truncated or corrupted during transmission.".to_string()),
code_example: None,
technical_details: Some("ADTS header requires minimum 7 bytes: syncword (2 bytes), MPEG info (1 byte), frame length (3 bytes partial), buffer fullness (2 bytes partial).".to_string()),
related_errors: Vec::new(),
})),
Mp4WriterError::InvalidOpusPacket,
Mp4WriterError::AudioNotEnabled,
Mp4WriterError::DurationOverflow,
Mp4WriterError::AlreadyFinalized,
];
for v in variants {
let s = format!("{v}");
assert!(!s.is_empty());
}
}
#[test]
fn write_video_with_dts_enforces_first_keyframe_and_codec_config() {
let sink = Cursor::new(Vec::<u8>::new());
let mut writer = Mp4Writer::new(sink, VideoCodec::H264);
let not_keyframe = vec![0x00, 0x00, 0x00, 0x01, 0x41, 0x9a, 0x24, 0x6c];
assert!(matches!(
writer.write_video_sample_with_dts(0, 0, ¬_keyframe, false),
Err(Mp4WriterError::FirstFrameMustBeKeyframe)
));
let sink = Cursor::new(Vec::<u8>::new());
let mut hevc = Mp4Writer::new(sink, VideoCodec::H265);
assert!(matches!(
hevc.write_video_sample_with_dts(0, 0, &h264_keyframe(), true),
Err(Mp4WriterError::FirstFrameMissingSpsPps)
));
let sink = Cursor::new(Vec::<u8>::new());
let mut av1 = Mp4Writer::new(sink, VideoCodec::Av1);
assert!(matches!(
av1.write_video_sample_with_dts(0, 0, &h264_keyframe(), true),
Err(Mp4WriterError::FirstFrameMissingSequenceHeader)
));
}
#[test]
fn write_video_with_dts_enforces_monotonic_dts_and_duration_bounds() {
let sink = Cursor::new(Vec::<u8>::new());
let mut writer = Mp4Writer::new(sink, VideoCodec::H264);
writer
.write_video_sample_with_dts(0, 0, &h264_keyframe(), true)
.unwrap();
assert!(matches!(
writer.write_video_sample_with_dts(3000, 0, &h264_keyframe(), false),
Err(Mp4WriterError::NonIncreasingTimestamp)
));
let sink = Cursor::new(Vec::<u8>::new());
let mut writer = Mp4Writer::new(sink, VideoCodec::H264);
writer
.write_video_sample_with_dts(0, 0, &h264_keyframe(), true)
.unwrap();
let big_delta = u64::from(u32::MAX) + 1;
assert!(matches!(
writer.write_video_sample_with_dts(big_delta, big_delta, &h264_keyframe(), false),
Err(Mp4WriterError::DurationOverflow)
));
let sink = Cursor::new(Vec::<u8>::new());
let mut writer = Mp4Writer::new(sink, VideoCodec::H264);
writer
.write_video_sample_with_dts(0, 0, &h264_keyframe(), true)
.unwrap();
writer
.write_video_sample_with_dts(3000, 3000, &h264_keyframe(), false)
.unwrap();
assert_eq!(writer.video_samples[0].duration, Some(3000));
}
#[test]
fn write_audio_sample_covers_disabled_and_invalid_inputs() {
let sink = Cursor::new(Vec::<u8>::new());
let mut writer = Mp4Writer::new(sink, VideoCodec::H264);
assert!(matches!(
writer.write_audio_sample(0, &[0u8; 3]),
Err(Mp4WriterError::AudioNotEnabled)
));
let sink = Cursor::new(Vec::<u8>::new());
let mut writer = Mp4Writer::new(sink, VideoCodec::H264);
writer.enable_audio(Mp4AudioTrack {
sample_rate: 48000,
channels: 2,
codec: AudioCodec::Aac(AacProfile::Lc),
});
assert!(matches!(
writer.write_audio_sample(0, &[0x00, 0x01, 0x02]),
Err(Mp4WriterError::InvalidAdtsDetailed(_))
));
let sink = Cursor::new(Vec::<u8>::new());
let mut writer = Mp4Writer::new(sink, VideoCodec::H264);
writer.enable_audio(Mp4AudioTrack {
sample_rate: 48000,
channels: 2,
codec: AudioCodec::Opus,
});
assert!(matches!(
writer.write_audio_sample(0, &[]),
Err(Mp4WriterError::InvalidOpusPacket)
));
}
#[test]
fn finalize_covers_empty_video_default_config_and_double_finalize() {
let sink = Cursor::new(Vec::<u8>::new());
let mut writer = Mp4Writer::new(sink, VideoCodec::H264);
let video = Mp4VideoTrack {
width: 640,
height: 480,
};
writer.finalize(&video, None, false).unwrap();
assert!(writer.finalize(&video, None, false).is_err());
}
#[test]
fn write_rejects_after_finalize() {
let sink = Cursor::new(Vec::<u8>::new());
let mut writer = Mp4Writer::new(sink, VideoCodec::H264);
let video = Mp4VideoTrack {
width: 640,
height: 480,
};
writer
.write_video_sample_with_dts(0, 0, &h264_keyframe(), true)
.unwrap();
writer.finalize(&video, None, true).unwrap();
assert!(matches!(
writer.write_video_sample_with_dts(3000, 3000, &h264_keyframe(), false),
Err(Mp4WriterError::AlreadyFinalized)
));
}
#[test]
fn aac_profile_validation_accepts_all_supported_profiles() {
use crate::api::AacProfile;
let supported_profiles = [
AacProfile::Lc,
AacProfile::Main,
AacProfile::Ssr,
AacProfile::Ltp,
AacProfile::He,
AacProfile::Hev2,
];
for profile in supported_profiles {
let sink = Cursor::new(Vec::<u8>::new());
let mut writer = Mp4Writer::new(sink, VideoCodec::H264);
writer.enable_audio(Mp4AudioTrack {
sample_rate: 48000,
channels: 2,
codec: AudioCodec::Aac(profile),
});
let adts_frame = vec![
0xFF, 0xF1, 0x4C, 0x80, 0x1F, 0xFC, 0x00, 0x00, 0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23, 0x80,
];
let result = writer.write_audio_sample(0, &adts_frame);
assert!(!matches!(result, Err(Mp4WriterError::InvalidAdtsDetailed(_)) if false));
}
}
#[test]
fn adts_to_raw_validates_frame_structure() {
let short_frame = vec![0xFF, 0xF1, 0x4C];
let result = adts_to_raw(&short_frame);
assert!(matches!(
result,
Err(AdtsValidationError {
kind: AdtsErrorKind::FrameTooShort,
..
})
));
let bad_sync = vec![
0xFE, 0xF1, 0x4C, 0x80, 0x1F, 0xFC, 0x00, 0x00, 0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23,
0x80,
];
let result = adts_to_raw(&bad_sync);
assert!(matches!(
result,
Err(AdtsValidationError {
kind: AdtsErrorKind::MissingSyncword,
..
})
));
let mpeg2_frame = vec![
0xFF, 0xF9, 0x4C, 0x80, 0x1F, 0xFC, 0x00, 0x00, 0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23,
0x80,
];
let result = adts_to_raw(&mpeg2_frame);
assert!(matches!(
result,
Err(AdtsValidationError {
kind: AdtsErrorKind::InvalidMpegVersion,
..
})
));
let non_aac_layer = vec![
0xFF, 0xF5, 0x4C, 0x80, 0x1F, 0xFC, 0x00, 0x00, 0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23,
0x80,
];
let result = adts_to_raw(&non_aac_layer);
assert!(matches!(
result,
Err(AdtsValidationError {
kind: AdtsErrorKind::InvalidLayer,
..
})
));
}
#[test]
fn build_audio_specific_config_standard_rates() {
assert_eq!(build_audio_specific_config(44100, 2), [0x12, 0x10]); assert_eq!(build_audio_specific_config(48000, 2), [0x11, 0x90]); assert_eq!(build_audio_specific_config(22050, 1), [0x13, 0x88]); assert_eq!(build_audio_specific_config(8000, 1), [0x15, 0x88]); }
#[test]
fn build_audio_specific_config_edge_cases() {
assert_eq!(build_audio_specific_config(12345, 2), [0x12, 0x10]);
assert_eq!(build_audio_specific_config(44100, 16), [0x12, 0x78]); assert_eq!(build_audio_specific_config(44100, 0), [0x12, 0x00]); }
#[test]
fn build_stts_box_empty_durations() {
let durations = Vec::new();
let box_data = build_stts_box(&durations);
assert_eq!(box_data.len(), 16);
assert_eq!(&box_data[0..4], &[0, 0, 0, 16]); assert_eq!(&box_data[4..8], b"stts"); assert_eq!(box_data[8..12], [0, 0, 0, 0]); assert_eq!(box_data[12..16], [0, 0, 0, 0]); }
#[test]
fn build_stts_box_single_duration() {
let durations = vec![3000];
let box_data = build_stts_box(&durations);
assert_eq!(box_data.len(), 24);
assert_eq!(&box_data[0..4], &[0, 0, 0, 24]); assert_eq!(&box_data[4..8], b"stts"); assert_eq!(box_data[8..12], [0, 0, 0, 0]); assert_eq!(box_data[12..16], [0, 0, 0, 1]); assert_eq!(box_data[16..20], [0, 0, 0, 1]); assert_eq!(box_data[20..24], [0, 0, 0x0B, 0xB8]); }
#[test]
fn build_stsc_box_single_chunk() {
let box_data = build_stsc_box(1, 1);
assert_eq!(box_data.len(), 28);
assert_eq!(&box_data[0..4], &[0, 0, 0, 28]); assert_eq!(&box_data[4..8], b"stsc"); assert_eq!(box_data[8..12], [0, 0, 0, 0]); assert_eq!(box_data[12..16], [0, 0, 0, 1]); assert_eq!(box_data[16..20], [0, 0, 0, 1]); assert_eq!(box_data[20..24], [0, 0, 0, 1]); assert_eq!(box_data[24..28], [0, 0, 0, 1]); }
#[test]
fn build_stsz_box_empty_sizes() {
let sizes = Vec::new();
let box_data = build_stsz_box(&sizes);
assert_eq!(box_data.len(), 20);
assert_eq!(&box_data[0..4], &[0, 0, 0, 20]); assert_eq!(&box_data[4..8], b"stsz"); assert_eq!(box_data[8..12], [0, 0, 0, 0]); assert_eq!(box_data[12..16], [0, 0, 0, 0]); assert_eq!(box_data[16..20], [0, 0, 0, 0]); }
#[test]
fn build_stsz_box_uniform_sizes() {
let sizes = vec![1024; 3];
let box_data = build_stsz_box(&sizes);
assert_eq!(box_data.len(), 32);
assert_eq!(&box_data[0..4], &[0, 0, 0, 32]); assert_eq!(&box_data[4..8], b"stsz"); assert_eq!(box_data[8..12], [0, 0, 0, 0]); assert_eq!(box_data[12..16], [0, 0, 0, 0]); assert_eq!(box_data[16..20], [0, 0, 0, 3]); assert_eq!(box_data[20..24], [0, 0, 0x04, 0x00]); assert_eq!(box_data[24..28], [0, 0, 0x04, 0x00]); assert_eq!(box_data[28..32], [0, 0, 0x04, 0x00]); }
#[test]
fn build_stsz_box_variable_sizes() {
let sizes = vec![100, 200, 300];
let box_data = build_stsz_box(&sizes);
assert_eq!(box_data.len(), 32);
assert_eq!(&box_data[0..4], &[0, 0, 0, 32]); assert_eq!(&box_data[4..8], b"stsz"); assert_eq!(box_data[8..12], [0, 0, 0, 0]); assert_eq!(box_data[12..16], [0, 0, 0, 0]); assert_eq!(box_data[16..20], [0, 0, 0, 3]); assert_eq!(box_data[20..24], [0, 0, 0, 100]); assert_eq!(box_data[24..28], [0, 0, 0, 200]); assert_eq!(box_data[28..32], [0, 0, 1, 44]); }
#[test]
fn build_stco_box_single_offset() {
let offsets = vec![1000];
let box_data = build_stco_box(&offsets);
assert_eq!(box_data.len(), 20);
assert_eq!(&box_data[0..4], &[0, 0, 0, 20]); assert_eq!(&box_data[4..8], b"stco"); assert_eq!(box_data[8..12], [0, 0, 0, 0]); assert_eq!(box_data[12..16], [0, 0, 0, 1]); assert_eq!(box_data[16..20], [0, 0, 0x03, 0xe8]); }
#[test]
fn build_stss_box_single_keyframe() {
let keyframes = vec![1];
let box_data = build_stss_box(&keyframes);
assert_eq!(box_data.len(), 20);
assert_eq!(&box_data[0..4], &[0, 0, 0, 20]); assert_eq!(&box_data[4..8], b"stss"); assert_eq!(box_data[8..12], [0, 0, 0, 0]); assert_eq!(box_data[12..16], [0, 0, 0, 1]); assert_eq!(box_data[16..20], [0, 0, 0, 1]); }
#[test]
fn build_ctts_box_single_offset() {
let cts_offsets = vec![3000];
let box_data = build_ctts_box(&cts_offsets);
assert_eq!(box_data.len(), 24);
assert_eq!(&box_data[0..4], &[0, 0, 0, 24]); assert_eq!(&box_data[4..8], b"ctts"); assert_eq!(box_data[8..12], [1, 0, 0, 0]); assert_eq!(box_data[12..16], [0, 0, 0, 1]); assert_eq!(box_data[16..20], [0, 0, 0, 1]); assert_eq!(box_data[20..24], [0, 0, 0x0b, 0xb8]); }
}