use std::{
path::Path,
sync::{Arc, Mutex},
};
use crate::pp_log::{PpLog, pp_error};
use ffmpeg_next as ffmpeg;
use thiserror::Error as ThisError;
use crate::{
buffer::MediaBuffer,
control::ControlMsg,
element::{Element, ElementType, Sink, element_pp_log},
error::Result,
};
#[derive(Debug, ThisError)]
pub enum Mp4MuxerError {
#[error("Mp4Muxer stream sinks only accept Packet or Eos buffers, got {0}")]
UnsupportedBuffer(&'static str),
#[error("ffmpeg error: {0}")]
Ffmpeg(#[from] ffmpeg::Error),
}
struct PendingStream {
name: Arc<str>,
input_time_base: ffmpeg::Rational,
}
pub struct Mp4Muxer {
output: ffmpeg::format::context::Output,
streams: Vec<PendingStream>,
}
impl Mp4Muxer {
pub fn create(path: impl AsRef<Path>) -> Result<Self> {
let output = ffmpeg::format::output(&path).map_err(Mp4MuxerError::from)?;
Ok(Self {
output,
streams: Vec::new(),
})
}
pub fn add_stream(
&mut self,
name: impl Into<String>,
parameters: ffmpeg::codec::Parameters,
time_base: ffmpeg::Rational,
) -> Result<()> {
let mut stream = self
.output
.add_stream(parameters.id())
.map_err(Mp4MuxerError::from)?;
stream.set_time_base(time_base);
stream.set_parameters(parameters);
self.streams.push(PendingStream {
name: name.into().into(),
input_time_base: time_base,
});
Ok(())
}
pub fn open(mut self) -> Result<Vec<Box<dyn Sink>>> {
self.output.write_header().map_err(Mp4MuxerError::from)?;
let total = self.streams.len();
let shared = Arc::new(Mp4MuxerShared {
state: Mutex::new(MuxerState {
output: self.output,
done: 0,
finished: false,
}),
total,
});
Ok(self
.streams
.into_iter()
.enumerate()
.map(|(index, stream)| -> Box<dyn Sink> {
Box::new(Mp4MuxerStreamSink {
pp_log: element_pp_log(ElementType::Mp4Muxer, &stream.name, None),
name: stream.name,
shared: shared.clone(),
stream_index: index,
input_time_base: stream.input_time_base,
done: false,
})
})
.collect())
}
}
struct MuxerState {
output: ffmpeg::format::context::Output,
done: usize,
finished: bool,
}
struct Mp4MuxerShared {
state: Mutex<MuxerState>,
total: usize,
}
impl Mp4MuxerShared {
fn write_packet(
&self,
stream_index: usize,
input_time_base: ffmpeg::Rational,
packet: &ffmpeg::Packet,
) -> Result<()> {
let mut state = self.state.lock().unwrap();
if state.finished {
return Ok(());
}
let mut packet = packet.clone();
let output_time_base = state
.output
.stream(stream_index)
.expect("stream was added in Mp4Muxer::add_stream")
.time_base();
packet.rescale_ts(input_time_base, output_time_base);
packet.set_stream(stream_index);
packet.set_position(-1);
packet
.write_interleaved(&mut state.output)
.map_err(Mp4MuxerError::from)?;
Ok(())
}
fn finish_track(&self) -> Result<()> {
let mut state = self.state.lock().unwrap();
state.done += 1;
if state.finished || state.done < self.total {
return Ok(());
}
state.finished = true;
state.output.write_trailer().map_err(Mp4MuxerError::from)?;
Ok(())
}
}
pub struct Mp4MuxerStreamSink {
pp_log: PpLog,
name: Arc<str>,
shared: Arc<Mp4MuxerShared>,
stream_index: usize,
input_time_base: ffmpeg::Rational,
done: bool,
}
impl Mp4MuxerStreamSink {
fn finish(&mut self) -> Result<()> {
if self.done {
return Ok(());
}
self.done = true;
self.shared
.finish_track()
.inspect_err(|error| pp_error!(self, "write_trailer failed: {error}"))
}
}
impl Element for Mp4MuxerStreamSink {
fn name(&self) -> Arc<str> {
self.name.clone()
}
fn element_type(&self) -> ElementType {
ElementType::Mp4Muxer
}
fn pp_log(&self) -> &PpLog {
&self.pp_log
}
fn pp_log_mut(&mut self) -> &mut PpLog {
&mut self.pp_log
}
}
impl Sink for Mp4MuxerStreamSink {
fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
match buf {
MediaBuffer::Packet(packet) => self
.shared
.write_packet(self.stream_index, self.input_time_base, &packet)
.inspect_err(|error| pp_error!(self, "write_interleaved failed: {error}")),
MediaBuffer::Eos => self.finish(),
other => Err(Mp4MuxerError::UnsupportedBuffer(other.kind()).into()),
}
}
fn control(&mut self, msg: ControlMsg) -> Result<()> {
if msg == ControlMsg::Stop {
self.finish()?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::element::Source;
use crate::elements::{AudioCodec, SwAudioEncoder, SwAudioEncoderOptions};
fn open_aac_encoder(sample_rate: u32, channels: u16) -> SwAudioEncoder {
SwAudioEncoder::new(
"encoder",
SwAudioEncoderOptions {
codec: AudioCodec::Aac,
sample_rate,
channels,
time_base: ffmpeg::Rational::new(1, sample_rate as i32),
bit_rate: 64_000,
},
)
.expect("aac encoder must be available")
}
fn silent_frame(
sample_rate: u32,
channels: u16,
samples: usize,
pts: i64,
) -> ffmpeg::frame::Audio {
let mut frame = ffmpeg::frame::Audio::new(
ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed),
samples,
ffmpeg::ChannelLayout::default(channels as i32),
);
frame.set_rate(sample_rate);
frame.set_pts(Some(pts));
frame.data_mut(0).fill(0);
frame
}
#[test]
fn single_track_still_produces_a_playable_file() {
let mut encoder = open_aac_encoder(48000, 1);
let dir = std::env::temp_dir();
let path = dir.join(format!("mp4_muxer_single_test_{}.mp4", std::process::id()));
let mut muxer = Mp4Muxer::create(&path).expect("mp4 muxer must open");
muxer
.add_stream(
"audio",
encoder.parameters(),
ffmpeg::Rational::new(1, 48000),
)
.expect("add_stream must succeed");
let mut sinks = muxer.open().expect("open must write the header");
assert_eq!(sinks.len(), 1);
encoder.src_pads()[0].link(sinks.pop().unwrap());
for tick in 0..20i64 {
encoder
.consume(MediaBuffer::Audio(Arc::new(silent_frame(
48000,
1,
960,
tick * 960,
))))
.expect("consume must succeed");
}
encoder
.consume(MediaBuffer::Eos)
.expect("eos must flush cleanly");
drop(encoder);
let input = ffmpeg::format::input(&path).expect("muxed file must be readable back");
assert_eq!(input.streams().count(), 1);
std::fs::remove_file(&path).ok();
}
#[test]
fn dropping_every_sink_without_eos_or_stop_still_releases_the_file() {
let encoder = open_aac_encoder(48000, 1);
let dir = std::env::temp_dir();
let path = dir.join(format!("mp4_muxer_drop_test_{}.mp4", std::process::id()));
let mut muxer = Mp4Muxer::create(&path).expect("mp4 muxer must open");
muxer
.add_stream(
"audio",
encoder.parameters(),
ffmpeg::Rational::new(1, 48000),
)
.expect("add_stream must succeed");
let sinks = muxer.open().expect("open must write the header");
drop(sinks);
drop(encoder);
std::fs::remove_file(&path)
.expect("file handle must be released once every sink is dropped");
}
#[test]
fn muxes_two_independent_tracks_without_finalizing_early() {
let mut encoder_a = open_aac_encoder(48000, 2);
let mut encoder_b = open_aac_encoder(44100, 1);
let dir = std::env::temp_dir();
let path = dir.join(format!("mp4_muxer_multi_test_{}.mp4", std::process::id()));
let mut muxer = Mp4Muxer::create(&path).expect("mp4 muxer must open");
muxer
.add_stream("a", encoder_a.parameters(), ffmpeg::Rational::new(1, 48000))
.expect("add_stream a");
muxer
.add_stream("b", encoder_b.parameters(), ffmpeg::Rational::new(1, 44100))
.expect("add_stream b");
let mut sinks = muxer.open().expect("open must write the header");
assert_eq!(sinks.len(), 2);
let sink_b = sinks.pop().unwrap();
let sink_a = sinks.pop().unwrap();
encoder_a.src_pads()[0].link(sink_a);
encoder_b.src_pads()[0].link(sink_b);
for tick in 0..10i64 {
encoder_a
.consume(MediaBuffer::Audio(Arc::new(silent_frame(
48000,
2,
960,
tick * 960,
))))
.expect("consume must succeed");
}
encoder_a
.consume(MediaBuffer::Eos)
.expect("eos must flush cleanly");
for tick in 0..10i64 {
encoder_b
.consume(MediaBuffer::Audio(Arc::new(silent_frame(
44100,
1,
882,
tick * 882,
))))
.expect("consume must succeed");
}
encoder_b
.consume(MediaBuffer::Eos)
.expect("eos must flush cleanly");
drop(encoder_a);
drop(encoder_b);
let mut input = ffmpeg::format::input(&path).expect("muxed file must be readable back");
assert_eq!(input.streams().count(), 2, "expected two tracks");
let mut counts = [0usize; 2];
let mut packet = ffmpeg::Packet::empty();
while packet.read(&mut input).is_ok() {
counts[packet.stream()] += 1;
packet = ffmpeg::Packet::empty();
}
assert!(counts[0] > 0, "track a has no packets: {counts:?}");
assert!(
counts[1] > 0,
"track b has no packets: {counts:?} — trailer was written before track b finished"
);
std::fs::remove_file(&path).ok();
}
}