use crate::core::context::muxer::{Muxer, SqMuxPlan, StreamBsfChains};
use crate::core::context::obj_pool::ObjPool;
use crate::core::context::pre_mux_queue::PreMuxQueueReceiver;
use crate::core::context::{PacketBox, PacketData};
use crate::core::packet_sink::{dispose_panic_payload, JobFailureSummary};
use crate::core::scheduler::ffmpeg_scheduler::{
is_stopping, packet_is_null, set_scheduler_error, wait_until_not_paused, STATUS_ABORT,
STATUS_END,
};
use crate::core::scheduler::input_controller::{InputController, SchNode};
use crate::core::scheduler::progress::OutputTelemetry;
use crate::core::scheduler::sync_queue::SyncQueue;
use crate::error::Error::Muxing;
use crate::error::{MuxingError, MuxingOperationError, OpenOutputError, WriteHeaderError};
use crate::raw::{BitStreamFilter, FormatContext};
use crate::util::ffmpeg_utils::{av_err2str, hashmap_to_avdictionary, DictGuard};
use crate::util::thread_synchronizer::ThreadSynchronizer;
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender};
use ffmpeg_next::packet::{Mut, Ref};
use ffmpeg_next::Packet;
use ffmpeg_sys_next::AVMediaType::{AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_SUBTITLE, AVMEDIA_TYPE_VIDEO};
use ffmpeg_sys_next::{
av_compare_ts, av_get_audio_frame_duration2, av_interleaved_write_frame, av_packet_move_ref,
av_packet_rescale_ts, av_rescale_delta, av_rescale_q, av_write_trailer,
avcodec_parameters_copy, avformat_write_header, avio_open2, avio_seek, avio_size,
AVFormatContext, AVIOContext, AVPacket, AVRational, AVERROR, AVERROR_EOF, AVFMT_NOFILE,
AVFMT_NOTIMESTAMPS, AVFMT_TS_NONSTRICT, AVIO_FLAG_WRITE, AV_LOG_DEBUG, AV_LOG_WARNING,
AV_NOPTS_VALUE, AV_PKT_FLAG_KEY, AV_TIME_BASE_Q, EAGAIN, ENOMEM,
};
use log::{debug, error, info, trace, warn};
use std::collections::HashMap;
use std::collections::VecDeque;
use std::ffi::{CStr, CString};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
struct SqMux {
queue: SyncQueue<PacketBox>,
sq_idx: Vec<Option<usize>>,
ostream: Vec<usize>,
}
fn build_sq_mux(plan: SqMuxPlan, stream_count: usize) -> SqMux {
let mut queue = SyncQueue::<PacketBox>::new(plan.buf_size_us);
let mut sq_idx = vec![None; stream_count];
let mut ostream = Vec::with_capacity(plan.streams.len());
for (output_stream_index, limiting, frames_max) in plan.streams {
let idx = queue.add_stream(limiting);
if output_stream_index < stream_count {
sq_idx[output_stream_index] = Some(idx);
}
ostream.push(output_stream_index);
if let Some(max) = frames_max {
queue.sq_limit_frames(idx, max);
}
}
SqMux {
queue,
sq_idx,
ostream,
}
}
type MuxWorkerHandoff = Arc<
Mutex<(
Option<MuxTeardownGuard>,
Option<MuxSlotGuard>,
Option<SinkDisposal>,
Option<MuxDoneGuard>,
)>,
>;
unsafe fn sq_pkt_end(pkt: *const AVPacket) -> (Option<i64>, AVRational, i32) {
let pts = (*pkt).pts;
let end = if pts == AV_NOPTS_VALUE {
None
} else {
Some(pts + (*pkt).duration)
};
(end, (*pkt).time_base, 0)
}
pub(crate) fn mux_init(
mux_idx: usize,
mux: &mut Muxer,
packet_pool: ObjPool<Packet>,
input_controller: Arc<InputController>,
mux_stream_nodes: Vec<Arc<SchNode>>,
scheduler_status: Arc<AtomicUsize>,
thread_sync: ThreadSynchronizer,
scheduler_result: Arc<Mutex<Option<crate::error::Result<()>>>>,
mux_done_remaining: Arc<AtomicUsize>,
progress: Arc<OutputTelemetry>,
) -> crate::error::Result<()> {
let sq_mux_plan = mux.sq_mux_plan();
let packet_sink = BundleDisposal::new(mux.take_packet_sink());
let out_fmt_ctx = mux
.out_fmt_ctx
.take()
.expect("mux_init called without an output context");
let mux_done = MuxDoneGuard::new(mux_done_remaining, scheduler_status.clone());
let (queue_sender, pkt_receiver) = match mux.take_queue() {
Some((s, r)) => (Some(s), Some(r)),
None => (None, None),
};
let guard = MuxTeardownGuard::new(
pkt_receiver,
mux.take_src_pre_recvs(),
mux.enc_handle_receiver(),
out_fmt_ctx,
);
let slot_guard = MuxSlotGuard::armed(thread_sync.clone(), scheduler_status.clone());
mux_task_start(
mux_idx,
guard,
slot_guard,
queue_sender,
mux.start_time_us,
mux.recording_time_us,
mux.stream_count(),
mux.format_opts.clone(),
mux.strict_avoptions,
mux.bsf_chains.clone(),
packet_sink,
mux.mux_start_gate(),
mux.interrupt_state.clone(),
packet_pool,
input_controller,
mux_stream_nodes,
sq_mux_plan,
scheduler_status,
scheduler_result,
mux_done,
progress,
)
}
pub(crate) fn ready_to_init_mux(
mux_idx: usize,
mux: &mut Muxer,
packet_pool: ObjPool<Packet>,
input_controller: Arc<InputController>,
scheduler_status: Arc<AtomicUsize>,
thread_sync: ThreadSynchronizer,
scheduler_result: Arc<Mutex<Option<crate::error::Result<()>>>>,
mux_done_remaining: Arc<AtomicUsize>,
progress: Arc<OutputTelemetry>,
) -> crate::error::Result<Option<crossbeam_channel::Sender<i32>>> {
if !mux.is_ready() {
let (sender, receiver) = crossbeam_channel::bounded(1);
let mux_done = MuxDoneGuard::new(mux_done_remaining, scheduler_status.clone());
let sq_mux_plan = mux.sq_mux_plan();
let packet_sink = BundleDisposal::new(mux.take_packet_sink());
let out_fmt_ctx = mux
.out_fmt_ctx
.take()
.expect("ready_to_init_mux called without an output context");
let mux_stream_nodes = mux.mux_stream_nodes.clone();
let (queue_sender, pkt_receiver) = match mux.take_queue() {
Some((s, r)) => (Some(s), Some(r)),
None => (None, None),
};
let guard = MuxTeardownGuard::new(
pkt_receiver,
mux.take_src_pre_recvs(),
mux.enc_handle_receiver(),
out_fmt_ctx,
);
let mux_start_gate = mux.mux_start_gate();
let interrupt_state = mux.interrupt_state.clone();
let start_time_us = mux.start_time_us;
let recording_time_us = mux.recording_time_us;
let stream_count = mux.stream_count();
let nb_streams_ready = mux.nb_streams_ready.clone();
let enc_registered = mux.enc_registered.clone();
let format_opts = mux.format_opts.clone();
let strict_avoptions = mux.strict_avoptions;
let bsf_chains = mux.bsf_chains.clone();
let result = std::thread::Builder::new().name(format!("ready-to-init-muxer{mux_idx}")).spawn(move || {
let mut slot_guard = Some(MuxSlotGuard::armed(
thread_sync.clone(),
scheduler_status.clone(),
));
let mut guard = Some(guard);
let mut packet_sink = packet_sink;
let _registration_barrier = MuxRegistrationBarrier {
enc_registered: enc_registered.clone(),
};
let _panic_status = MuxPanicStatusGuard {
scheduler_status: scheduler_status.clone(),
scheduler_result: scheduler_result.clone(),
};
let wait_enc_registered = || {
while !enc_registered.load(Ordering::Acquire) {
std::thread::sleep(Duration::from_millis(1));
}
};
let mut queue_sender = queue_sender;
loop {
let result = receiver.recv_timeout(Duration::from_millis(100));
if is_stopping(wait_until_not_paused(&scheduler_status)) {
info!("Init muxer receiver end command, finishing.");
break;
}
if let Err(e) = result {
if e == RecvTimeoutError::Disconnected {
warn!(
"mux init aborted: encoder(s) exited before all {stream_count} streams became ready ({} ready)",
nb_streams_ready.load(Ordering::Acquire)
);
break;
}
continue;
}
let stream_index = result.unwrap();
debug!("output_stream: {stream_index} is readied");
let nb_streams_ready = nb_streams_ready.fetch_add(1, Ordering::Release);
if nb_streams_ready + 1 == stream_count {
wait_enc_registered();
let guard = guard
.take()
.expect("mux waiter reached all-ready without a context");
let slot_guard = slot_guard
.take()
.expect("mux waiter reached all-ready without its slot guard");
if let Err(e) = mux_task_start(
mux_idx,
guard,
slot_guard,
queue_sender.take(),
start_time_us,
recording_time_us,
stream_count,
format_opts,
strict_avoptions,
bsf_chains,
packet_sink.take(),
mux_start_gate,
interrupt_state,
packet_pool,
input_controller,
mux_stream_nodes,
sq_mux_plan,
scheduler_status.clone(),
scheduler_result,
mux_done,
progress,
) {
debug!("Muxer init failed: {e}");
}
break;
}
}
if let Some(guard) = guard.take() {
wait_enc_registered();
drop(guard);
if packet_sink.dispose() {
error!(
"packet sink consumer state panicked during teardown; the job result is preserved"
);
}
match slot_guard.take() {
Some(slot_guard) => slot_guard.release(),
None => thread_sync.thread_done_with(|| {
scheduler_status.store(STATUS_END, Ordering::Release);
}),
}
}
});
if let Err(e) = result {
error!("Mux init thread exited with error: {e}");
return Err(MuxingOperationError::ThreadExited.into());
}
Ok(Some(sender))
} else {
Ok(None)
}
}
fn mux_task_start(
mux_idx: usize,
guard: MuxTeardownGuard,
slot_guard: MuxSlotGuard,
queue_sender: Option<Sender<PacketBox>>,
start_time_us: Option<i64>,
recording_time_us: Option<i64>,
stream_count: usize,
format_opts: Option<HashMap<CString, CString>>,
strict_avoptions: bool,
bsf_chains: StreamBsfChains,
packet_sink: BundleDisposal,
mux_start_gate: Arc<crate::core::context::MuxStartGate>,
interrupt_state: Arc<crate::core::context::InterruptState>,
packet_pool: ObjPool<Packet>,
input_controller: Arc<InputController>,
mux_stream_nodes: Vec<Arc<SchNode>>,
sq_mux_plan: Option<SqMuxPlan>,
scheduler_status: Arc<AtomicUsize>,
scheduler_result: Arc<Mutex<Option<crate::error::Result<()>>>>,
mux_done: MuxDoneGuard,
progress: Arc<OutputTelemetry>,
) -> crate::error::Result<()> {
let Some(queue_sender) = queue_sender else {
debug_assert!(
packet_sink.is_empty(),
"zero-stream packet sink must fail typed at build()"
);
if let Err(open_ret) = unsafe { open_muxer_output(guard.ctx().as_ptr()) } {
fail_mux_init(
&scheduler_status,
&scheduler_result,
MuxInitQueues::NONE,
guard,
slot_guard,
crate::error::Error::OpenOutput(OpenOutputError::from(open_ret)),
packet_sink,
);
error!("Error opening output: {}", av_err2str(open_ret));
return Err(crate::error::Error::OpenOutput(OpenOutputError::from(
open_ret,
)));
}
drop(guard);
slot_guard.release();
return Ok(());
};
let src_pre_receivers = _mux_init(
mux_idx,
guard,
slot_guard,
start_time_us,
recording_time_us,
stream_count,
format_opts,
strict_avoptions,
bsf_chains,
packet_sink,
interrupt_state,
packet_pool,
input_controller,
mux_stream_nodes,
sq_mux_plan,
scheduler_status,
scheduler_result,
mux_done,
progress,
)?;
mux_start_gate.start_with(|| {
let mut queues: Vec<VecDeque<PacketBox>> = src_pre_receivers
.iter()
.map(|receiver| receiver.drain_all())
.collect();
loop {
let mut min_stream = None;
let mut min_ts: Option<(i64, AVRational)> = None;
for (i, queue) in queues.iter().enumerate() {
let Some(front) = queue.front() else { continue };
let (dts, tb) = unsafe {
let pkt = front.packet.as_ptr();
((*pkt).dts, (*pkt).time_base)
};
if dts == AV_NOPTS_VALUE || tb.num <= 0 || tb.den <= 0 {
min_stream = Some(i);
break;
}
match min_ts {
Some((min_dts, min_tb))
if unsafe { av_compare_ts(min_dts, min_tb, dts, tb) } <= 0 => {}
_ => {
min_stream = Some(i);
min_ts = Some((dts, tb));
}
}
}
let Some(i) = min_stream else { break };
let packet_box = queues[i].pop_front().unwrap();
let _ = queue_sender.send(packet_box);
}
});
Ok(())
}
fn _mux_init(
mux_idx: usize,
guard: MuxTeardownGuard,
slot_guard: MuxSlotGuard,
start_time_us: Option<i64>,
recording_time_us: Option<i64>,
stream_count: usize,
format_opts: Option<HashMap<CString, CString>>,
#[cfg_attr(not(feature = "cli"), allow(unused_variables))] strict_avoptions: bool,
bsf_chains: StreamBsfChains,
packet_sink: BundleDisposal,
interrupt_state: Arc<crate::core::context::InterruptState>,
packet_pool: ObjPool<Packet>,
input_controller: Arc<InputController>,
mux_stream_nodes: Vec<Arc<SchNode>>,
sq_mux_plan: Option<SqMuxPlan>,
scheduler_status: Arc<AtomicUsize>,
scheduler_result: Arc<Mutex<Option<crate::error::Result<()>>>>,
mux_done: MuxDoneGuard,
progress: Arc<OutputTelemetry>,
) -> crate::error::Result<Vec<PreMuxQueueReceiver>> {
let slot_guard = slot_guard;
let mut guard = guard;
let mux_done = mux_done;
let mut sink_worker_slot: Option<SinkDisposal> = None;
let mut packet_sink = packet_sink;
let _panic_status = MuxPanicStatusGuard {
scheduler_status: scheduler_status.clone(),
scheduler_result: scheduler_result.clone(),
};
let mut pkt_receiver = guard.take_pkt_receiver();
let src_pre_receivers = guard.take_pre_receivers();
let _panic_error_first = MuxPanicErrorGuard {
scheduler_result: scheduler_result.clone(),
};
let out_fmt_ctx_ptr = unsafe { guard.ctx().as_ptr() };
let mut opts = DictGuard::new(hashmap_to_avdictionary(&format_opts));
let stream_bsfs =
match unsafe { init_bitstream_filters(out_fmt_ctx_ptr, &bsf_chains, stream_count) } {
Ok(bsfs) => bsfs,
Err((name, bsf_ret)) => {
error!(
"Could not initialize bitstream filter chain '{name}': {}",
av_err2str(bsf_ret)
);
fail_mux_init(
&scheduler_status,
&scheduler_result,
MuxInitQueues {
pkt_receiver: pkt_receiver.take(),
pre_receivers: src_pre_receivers,
},
guard,
slot_guard,
Muxing(MuxingOperationError::BitstreamFilterInit(
name.clone(),
MuxingError::from(bsf_ret),
)),
packet_sink,
);
return Err(Muxing(MuxingOperationError::BitstreamFilterInit(
name,
MuxingError::from(bsf_ret),
)));
}
};
if let Some(sink) = packet_sink.take_inner() {
match unsafe {
crate::core::packet_sink::strict::PacketSinkWorker::collect(
out_fmt_ctx_ptr,
stream_count,
sink,
&scheduler_status,
&scheduler_result,
)
} {
Ok(worker) => {
sink_worker_slot = Some(SinkDisposal::new(worker));
debug!(
"Packet sink configuration collected for output {mux_idx}: {stream_count} streams"
);
}
Err(boxed) => {
let e = {
let (sink, e) = *boxed;
packet_sink.0 = Some(sink);
e
};
error!("Packet sink configuration invalid: {e}");
fail_mux_init(
&scheduler_status,
&scheduler_result,
MuxInitQueues {
pkt_receiver: pkt_receiver.take(),
pre_receivers: src_pre_receivers,
},
guard,
slot_guard,
crate::error::Error::PacketSink(e.clone()),
packet_sink,
);
return Err(crate::error::Error::PacketSink(e));
}
}
}
if sink_worker_slot.is_none() {
if let Err(open_ret) = unsafe { open_muxer_output(out_fmt_ctx_ptr) } {
error!("Error opening output: {}", av_err2str(open_ret));
fail_mux_init(
&scheduler_status,
&scheduler_result,
MuxInitQueues {
pkt_receiver: pkt_receiver.take(),
pre_receivers: src_pre_receivers,
},
guard,
slot_guard,
crate::error::Error::OpenOutput(OpenOutputError::from(open_ret)),
packet_sink,
);
return Err(crate::error::Error::OpenOutput(OpenOutputError::from(
open_ret,
)));
}
let ret = unsafe { avformat_write_header(out_fmt_ctx_ptr, opts.as_double_ptr()) };
if ret < 0 {
error!(
"Could not write header (incorrect codec parameters ?): {}",
av_err2str(ret)
);
fail_mux_init(
&scheduler_status,
&scheduler_result,
MuxInitQueues {
pkt_receiver: pkt_receiver.take(),
pre_receivers: src_pre_receivers,
},
guard,
slot_guard,
Muxing(MuxingOperationError::WriteHeader(WriteHeaderError::from(
ret,
))),
packet_sink,
);
return Err(Muxing(MuxingOperationError::WriteHeader(
WriteHeaderError::from(ret),
)));
}
for key in opts.leftover_keys() {
#[cfg(feature = "cli")]
if strict_avoptions {
let err = crate::error::Error::UnconsumedCliOption {
site: format!("the muxer of output {mux_idx}"),
option: key.clone(),
};
fail_mux_init(
&scheduler_status,
&scheduler_result,
MuxInitQueues {
pkt_receiver: pkt_receiver.take(),
pre_receivers: src_pre_receivers,
},
guard,
slot_guard,
crate::error::Error::UnconsumedCliOption {
site: format!("the muxer of output {mux_idx}"),
option: key,
},
packet_sink,
);
return Err(err);
}
warn!("Option '{key}' was not recognized by output {mux_idx}");
}
unsafe {
if ((*(*out_fmt_ctx_ptr).oformat).flags & AVFMT_NOFILE) == 0
&& (progress.is_observed() || !guard.ctx().is_custom_io())
{
progress.set_total_size(muxer_filesize((*out_fmt_ctx_ptr).pb));
}
}
}
let oformat_flags = unsafe {
let oformat = (*out_fmt_ctx_ptr).oformat;
(*oformat).flags
};
let format_name = if sink_worker_slot.is_some() {
"packet_sink"
} else {
unsafe {
CStr::from_ptr((*(*out_fmt_ctx_ptr).oformat).name)
.to_str()
.unwrap_or("unknown")
}
};
let scheduler_status_spawn = scheduler_status.clone();
let scheduler_result_spawn = scheduler_result.clone();
let thread_name = format!("muxer{mux_idx}:{format_name}");
guard.restore_pkt_receiver(pkt_receiver.take());
let guard_slot: MuxWorkerHandoff = Arc::new(Mutex::new((
Some(guard),
Some(slot_guard),
sink_worker_slot.take(),
Some(mux_done),
)));
let worker_guard_slot = Arc::clone(&guard_slot);
let result = std::thread::Builder::new().name(thread_name).spawn(move || {
let slot_guard = worker_guard_slot
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.1
.take()
.expect("mux worker started without its slot guard");
let mut guard = worker_guard_slot
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.0
.take()
.expect("mux worker started without its teardown guard");
let mut mux_done = Some(
worker_guard_slot
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.3
.take()
.expect("mux worker started without its mux-done guard"),
);
let mut sink_worker = worker_guard_slot
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.2
.take()
.unwrap_or_else(SinkDisposal::empty);
let _panic_status = MuxPanicStatusGuard {
scheduler_status: scheduler_status.clone(),
scheduler_result: scheduler_result.clone(),
};
let pkt_receiver = guard
.take_pkt_receiver()
.expect("mux worker without a packet queue");
let _panic_error_first = MuxPanicErrorGuard {
scheduler_result: scheduler_result.clone(),
};
let _panic_progress = MuxPanicProgressGuard {
progress: progress.clone(),
};
let out_fmt_ctx: &FormatContext = guard.ctx();
let mut stream_bsfs = stream_bsfs;
let has_bsf = !stream_bsfs.is_empty();
let mut stream_pkt_templates: Vec<Option<PacketData>> = if has_bsf {
(0..stream_count).map(|_| None).collect()
} else {
Vec::new()
};
let mut stream_started: Vec<bool> = vec![false; stream_count];
let mut stream_eof: Vec<bool> = vec![false; stream_count];
let mut st_rescale_delta_last: Vec<i64> = vec![0; stream_count];
let mut st_last_dts: Vec<i64> = vec![AV_NOPTS_VALUE; stream_count];
if sink_worker.is_empty() {
let selected_video = unsafe {
(0..stream_count).find(|&i| {
let stream = *(*out_fmt_ctx.as_ptr()).streams.add(i);
!stream.is_null()
&& !(*stream).codecpar.is_null()
&& (*(*stream).codecpar).codec_type == AVMEDIA_TYPE_VIDEO
})
};
progress.set_video_stream(selected_video);
}
let sq_mux = sq_mux_plan.map(|plan| build_sq_mux(plan, stream_count));
let mut nb_done = 0;
let mut ret = 0;
if let Some(sink) = sink_worker.worker_mut() {
ret = sink.deliver_stream_info();
}
let cfg = MuxWriteCfg {
has_bsf,
oformat_flags,
stream_count,
out_fmt_ctx: &out_fmt_ctx,
packet_pool: &packet_pool,
mux_stream_nodes: &mux_stream_nodes,
input_controller: &input_controller,
scheduler_status: &scheduler_status,
progress: &progress,
};
let mut state = MuxWriteState {
stream_pkt_templates: &mut stream_pkt_templates,
st_rescale_delta_last: &mut st_rescale_delta_last,
st_last_dts: &mut st_last_dts,
stream_eof: &mut stream_eof,
nb_done: &mut nb_done,
};
if let Some(mut sq) = sq_mux {
debug_assert!(sink_worker.is_empty(), "packet sink with an sq_mux plan");
let mut released: Vec<PacketBox> = Vec::new();
let mut nf: Vec<usize> = Vec::new();
let fin_tb = AVRational { num: 1, den: 1 };
for i in 0..stream_count {
if sq.sq_idx.get(i).copied().flatten().is_none() {
if let Err(e) = unsafe {
finish_output_stream(
i,
&cfg,
&mut state,
&mut stream_bsfs,
)
} {
ret = e;
}
}
}
while *state.nb_done < stream_count && ret >= 0 {
let result = pkt_receiver.recv_timeout(Duration::from_millis(100));
if is_stopping(wait_until_not_paused(&scheduler_status)) {
info!("Muxer receiver end command, finishing.");
break;
}
let mut packet_box = match result {
Ok(pb) => pb,
Err(RecvTimeoutError::Disconnected) => {
debug!("Encoder thread exit.");
break;
}
Err(RecvTimeoutError::Timeout) => {
match sq_mux_pump(
&mut sq, &mut released, &mut nf, &cfg, &mut state, &mut stream_bsfs,
) {
Ok(true) => break,
Ok(false) => continue,
Err(e) => { ret = e; break; }
}
}
};
let pkt = packet_box.packet.as_ptr();
let raw_stream_index = unsafe { (*pkt).stream_index };
if raw_stream_index < 0 {
let eof_stream = packet_box.packet_data.output_stream_index;
packet_pool.release(packet_box.packet);
if eof_stream >= 0 {
if let Some(Some(sq_i)) =
sq.sq_idx.get(eof_stream as usize).copied()
{
sq.queue.send(sq_i, None, None, fin_tb, 0);
}
}
match sq_mux_pump(
&mut sq, &mut released, &mut nf, &cfg, &mut state, &mut stream_bsfs,
) {
Ok(true) => break,
Ok(false) => continue,
Err(e) => { ret = e; break; }
}
}
let stream_index = raw_stream_index as usize;
if stream_index >= mux_stream_nodes.len() {
error!("Invalid stream_index: {} >= {}", stream_index, mux_stream_nodes.len());
packet_pool.release(packet_box.packet);
continue;
}
let is_marker = unsafe {
let has_side_data = (*pkt).side_data_elems > 0;
packet_is_null(&packet_box.packet)
|| (packet_box.packet.is_empty() && !has_side_data)
};
if is_marker {
if scheduler_status.load(Ordering::Acquire) == STATUS_ABORT {
debug!("Muxer detected abort from stream {}, exiting without trailer", stream_index);
packet_pool.release(packet_box.packet);
break;
}
packet_pool.release(packet_box.packet);
if !state.stream_eof[stream_index] {
if let Some(Some(sq_i)) = sq.sq_idx.get(stream_index).copied() {
sq.queue.send(sq_i, None, None, fin_tb, 0);
}
}
match sq_mux_pump(
&mut sq, &mut released, &mut nf, &cfg, &mut state, &mut stream_bsfs,
) {
Ok(true) => break,
Ok(false) => continue,
Err(e) => { ret = e; break; }
}
}
unsafe {
update_last_dts(&mux_stream_nodes[stream_index], &input_controller, &scheduler_status, pkt);
}
if state.stream_eof[stream_index] {
packet_pool.release(packet_box.packet);
continue;
}
let sq_i = match sq.sq_idx.get(stream_index).copied().flatten() {
Some(i) => i,
None => {
let wret = unsafe {
mux_write_released(
&mut packet_box, &cfg, &mut state, &mut stream_bsfs,
)
};
packet_pool.release(packet_box.packet);
if wret == AVERROR_EOF { break; }
if wret < 0 { ret = wret; error!("Error muxing a packet: stream_index={stream_index}, ret={wret}"); break; }
continue;
}
};
if packet_box.packet_data.is_copy {
let started = &mut stream_started[stream_index];
let rret = unsafe {
streamcopy_rescale(
packet_box.packet.as_mut_ptr(),
&packet_box.packet_data,
&start_time_us,
&recording_time_us,
started,
)
};
if rret == AVERROR(EAGAIN) {
packet_pool.release(packet_box.packet);
continue;
} else if rret == AVERROR_EOF {
packet_pool.release(packet_box.packet);
sq.queue.send(sq_i, None, None, fin_tb, 0);
match sq_mux_pump(
&mut sq, &mut released, &mut nf, &cfg, &mut state, &mut stream_bsfs,
) {
Ok(true) => break,
Ok(false) => continue,
Err(e) => { ret = e; break; }
}
}
}
let (end_ts, tb, nb_samples) =
unsafe { sq_pkt_end(packet_box.packet.as_ptr()) };
sq.queue.send(sq_i, Some(packet_box), end_ts, tb, nb_samples);
match sq_mux_pump(
&mut sq, &mut released, &mut nf, &cfg, &mut state, &mut stream_bsfs,
) {
Ok(true) => break,
Ok(false) => {}
Err(e) => { ret = e; break; }
}
}
} else {
loop {
if ret < 0 {
break;
}
let result = pkt_receiver.recv_timeout(Duration::from_millis(100));
if is_stopping(wait_until_not_paused(&scheduler_status)) {
info!("Muxer receiver end command, finishing.");
break;
}
if let Err(e) = result {
if e == RecvTimeoutError::Disconnected {
debug!("Encoder thread exit.");
break;
}
continue;
}
let mut packet_box = result.unwrap();
let pkt = packet_box.packet.as_ptr();
let packet_data = &packet_box.packet_data;
let raw_stream_index = unsafe { (*pkt).stream_index };
if raw_stream_index < 0 {
let eof_stream = packet_box.packet_data.output_stream_index;
if eof_stream >= 0 {
if let Err(fret) = unsafe {
finish_output_stream(eof_stream as usize, &cfg, &mut state, &mut stream_bsfs)
} {
ret = fret;
error!("Error flushing bitstream filter at EOF: stream={eof_stream}, ret={fret}");
packet_pool.release(packet_box.packet);
break;
}
}
packet_pool.release(packet_box.packet);
if *state.nb_done == stream_count {
trace!("All streams finished (demux EOF signal)");
break;
}
continue;
}
let stream_index = raw_stream_index as usize;
if stream_index >= mux_stream_nodes.len() {
error!("Invalid stream_index: {} >= {}", stream_index, mux_stream_nodes.len());
packet_pool.release(packet_box.packet);
continue;
}
let mux_stream_node = &mux_stream_nodes[stream_index];
unsafe {
let has_side_data = (*packet_box.packet.as_ptr()).side_data_elems > 0;
if packet_is_null(&packet_box.packet) || (packet_box.packet.is_empty() && !has_side_data) {
let current_status = scheduler_status.load(Ordering::Acquire);
if current_status == STATUS_ABORT {
debug!("Muxer detected abort from stream {}, exiting without trailer", stream_index);
packet_pool.release(packet_box.packet);
break;
}
if let Err(fret) = finish_output_stream(
stream_index,
&cfg,
&mut state,
&mut stream_bsfs,
) {
ret = fret;
error!("Error flushing bitstream filter at EOF: stream={stream_index}, ret={fret}");
packet_pool.release(packet_box.packet);
break;
}
packet_pool.release(packet_box.packet);
if *state.nb_done == stream_count {
trace!("All streams finished");
break;
} else {
continue;
}
}
update_last_dts(mux_stream_node, &input_controller, &scheduler_status, pkt);
if state.stream_eof[stream_index] {
packet_pool.release(packet_box.packet);
continue;
}
if !packet_is_null(&packet_box.packet) && packet_data.is_copy {
let started = &mut stream_started[stream_index];
let rret = streamcopy_rescale(
packet_box.packet.as_mut_ptr(),
packet_data,
&start_time_us,
&recording_time_us,
started,
);
if rret == AVERROR(EAGAIN) {
packet_pool.release(packet_box.packet);
continue;
}
ret = rret;
if ret == AVERROR_EOF {
if let Err(fret) = finish_output_stream(
stream_index,
&cfg,
&mut state,
&mut stream_bsfs,
) {
ret = fret;
error!("Error flushing bitstream filter at EOF: stream={stream_index}, ret={fret}");
packet_pool.release(packet_box.packet);
break;
}
packet_pool.release(packet_box.packet);
if *state.nb_done == stream_count {
trace!("All streams finished (recording_time)");
break;
}
continue;
}
}
if !packet_is_null(&packet_box.packet)
&& (*packet_box.packet.as_ptr()).stream_index >= 0
{
if let Some(sink) = sink_worker.worker_mut() {
ret = sink.process_and_deliver(&mut packet_box);
} else if has_bsf {
if stream_bsfs[stream_index].is_some() {
state.stream_pkt_templates[stream_index] =
Some(packet_box.packet_data);
}
ret = mux_filter_and_write_packet(
&cfg,
&mut state,
&mut packet_box,
stream_bsfs[stream_index].as_mut(),
);
} else {
ret = write_packet(
&cfg,
&mut state,
&mut packet_box,
);
}
packet_pool.release(packet_box.packet);
if ret == AVERROR_EOF {
trace!("Muxer returned EOF");
break;
} else if ret < 0 {
if sink_worker.cancelled_cleanly() {
trace!("Sink delivery stopped cooperatively: stream_index={stream_index}");
} else {
error!("Error muxing a packet: stream_index={stream_index}, ret={ret}");
}
break;
}
}
}
}
}
if ret < 0 && ret != AVERROR_EOF {
let sink_error = sink_worker.pending_error_cloned();
match sink_error {
Some(e) => set_scheduler_error(
&scheduler_status,
&scheduler_result,
crate::error::Error::PacketSink(e),
),
None => {
let cancelled = sink_worker.cancelled_cleanly();
if !cancelled {
set_scheduler_error(
&scheduler_status,
&scheduler_result,
Muxing(MuxingOperationError::InterleavedWriteError(
MuxingError::from(ret),
)),
);
}
}
}
}
let _finalizing = ((ret >= 0 || ret == AVERROR_EOF) && sink_worker.is_empty())
.then(|| interrupt_state.begin_output_finalize());
if !sink_worker.is_empty() {
let mut sink = sink_worker;
if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
debug!("Packet sink muxer finished.");
})) {
dispose_panic_payload(payload);
}
drop(pkt_receiver);
for node in &mux_stream_nodes {
if let SchNode::MuxStream {
source_finished, ..
} = node.as_ref()
{
source_finished.store(true, Ordering::Release);
}
}
let locally_complete =
ret >= 0 && nb_done == stream_count && sink.pending_error_cloned().is_none();
drop(mux_done.take());
input_controller.update_locked(&scheduler_status);
drop(guard);
if locally_complete {
slot_guard.wait_for_peers_settled();
} else {
slot_guard.register_settled();
}
let aborted = scheduler_status.load(Ordering::Acquire) == STATUS_ABORT;
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let job_error = if !aborted {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
scheduler_result
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.and_then(|result| result.as_ref().err())
.map(JobFailureSummary::from_error)
}))
.unwrap_or_else(|payload| {
dispose_panic_payload(payload);
Some(JobFailureSummary::formatting_panicked())
})
} else {
None
};
let finish_panicked = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(
|| sink.finish(nb_done == stream_count, ret, aborted, job_error),
)) {
Ok(()) => false,
Err(payload) => {
dispose_panic_payload(payload);
true
}
};
let teardown_panicked = sink.dispose();
if finish_panicked {
error!(
"packet sink terminal callback panicked; the settled job result is preserved"
);
}
if teardown_panicked {
error!(
"packet sink consumer state panicked during teardown; the settled job result is preserved"
);
}
}));
if let Err(payload) = outcome {
dispose_panic_payload(payload);
}
slot_guard.release();
return;
}
let final_status = scheduler_status.load(Ordering::Acquire);
if final_status != STATUS_ABORT {
unsafe {
let ret = av_write_trailer(out_fmt_ctx.as_ptr());
if ret < 0 {
error!("Error writing trailer: {}", av_err2str(ret));
set_scheduler_error(
&scheduler_status,
&scheduler_result,
Muxing(MuxingOperationError::TrailerWriteError(MuxingError::from(
ret,
))),
);
}
if (oformat_flags & AVFMT_NOFILE) == 0
&& (progress.is_observed() || !out_fmt_ctx.is_custom_io())
{
progress.set_total_size(muxer_filesize((*out_fmt_ctx.as_ptr()).pb));
}
}
} else {
debug!("Muxer skipping trailer due to abort");
}
progress.mark_all_streams_finished();
debug!("Muxer finished.");
drop(pkt_receiver);
for node in &mux_stream_nodes {
if let SchNode::MuxStream {
source_finished, ..
} = node.as_ref()
{
source_finished.store(true, Ordering::Release);
}
}
drop(mux_done.take());
input_controller.update_locked(&scheduler_status);
drop(guard);
slot_guard.release();
});
if let Err(e) = result {
fail_mux_worker_spawn(
&scheduler_status_spawn,
&scheduler_result_spawn,
src_pre_receivers,
&guard_slot,
);
error!("Muxer thread exited with error: {e}");
return Err(MuxingOperationError::ThreadExited.into());
}
Ok(src_pre_receivers)
}
pub(crate) fn release_mux_slot(
scheduler_status: &Arc<AtomicUsize>,
thread_sync: &ThreadSynchronizer,
) {
thread_sync.thread_done_with(|| {
scheduler_status.store(STATUS_END, Ordering::Release);
});
}
fn fail_mux_worker_spawn(
scheduler_status: &Arc<AtomicUsize>,
scheduler_result: &Arc<Mutex<Option<crate::error::Result<()>>>>,
src_pre_receivers: Vec<PreMuxQueueReceiver>,
guard_slot: &MuxWorkerHandoff,
) {
set_scheduler_error(
scheduler_status,
scheduler_result,
Muxing(MuxingOperationError::ThreadExited),
);
drop(src_pre_receivers);
let (guard, slot_guard, sink_worker, mux_done) = {
let mut handoff = guard_slot
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
(
handoff
.0
.take()
.expect("mux worker spawn failed but the closure consumed the guard"),
handoff
.1
.take()
.expect("mux worker spawn failed but the closure consumed the slot guard"),
handoff.2.take(),
handoff.3.take(),
)
};
drop(guard);
drop(sink_worker);
drop(mux_done);
slot_guard.release();
}
unsafe fn open_muxer_output(out_fmt_ctx: *mut AVFormatContext) -> std::result::Result<(), i32> {
if !(*out_fmt_ctx).pb.is_null() {
return Ok(());
}
if (*(*out_fmt_ctx).oformat).flags & AVFMT_NOFILE != 0 {
return Ok(());
}
let ret = avio_open2(
&mut (*out_fmt_ctx).pb,
(*out_fmt_ctx).url,
AVIO_FLAG_WRITE,
&(*out_fmt_ctx).interrupt_callback,
std::ptr::null_mut(),
);
if ret < 0 {
Err(ret)
} else {
Ok(())
}
}
struct MuxInitQueues {
pkt_receiver: Option<Receiver<PacketBox>>,
pre_receivers: Vec<PreMuxQueueReceiver>,
}
impl MuxInitQueues {
const NONE: Self = Self {
pkt_receiver: None,
pre_receivers: Vec::new(),
};
fn close(self) {
drop(self.pkt_receiver);
drop(self.pre_receivers);
}
}
fn fail_mux_init(
scheduler_status: &Arc<AtomicUsize>,
scheduler_result: &Arc<Mutex<Option<crate::error::Result<()>>>>,
queues: MuxInitQueues,
guard: MuxTeardownGuard,
slot_guard: MuxSlotGuard,
error: crate::error::Error,
mut packet_sink: BundleDisposal,
) {
set_scheduler_error(scheduler_status, scheduler_result, error);
queues.close();
drop(guard);
let sink_teardown_panicked = packet_sink.dispose();
slot_guard.release();
if sink_teardown_panicked {
error!(
"packet sink consumer state panicked during teardown; the settled job result is preserved"
);
}
}
struct MuxDoneGuard {
remaining: Arc<AtomicUsize>,
scheduler_status: Arc<AtomicUsize>,
}
impl MuxDoneGuard {
fn new(remaining: Arc<AtomicUsize>, scheduler_status: Arc<AtomicUsize>) -> Self {
Self {
remaining,
scheduler_status,
}
}
}
impl Drop for MuxDoneGuard {
fn drop(&mut self) {
if self.remaining.fetch_sub(1, Ordering::AcqRel) != 1 {
return;
}
let mut current = self.scheduler_status.load(Ordering::Acquire);
while !is_stopping(current) {
match self.scheduler_status.compare_exchange_weak(
current,
STATUS_END,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => break,
Err(actual) => current = actual,
}
}
}
}
struct MuxSlotGuard {
armed: bool,
thread_sync: ThreadSynchronizer,
scheduler_status: Arc<AtomicUsize>,
settled_registered: std::cell::Cell<bool>,
}
impl MuxSlotGuard {
fn armed(thread_sync: ThreadSynchronizer, scheduler_status: Arc<AtomicUsize>) -> Self {
Self {
armed: true,
thread_sync,
scheduler_status,
settled_registered: std::cell::Cell::new(false),
}
}
fn disarm(&mut self) {
self.armed = false;
}
fn register_settled(&self) {
if !self.settled_registered.replace(true) {
self.thread_sync.register_settled();
}
}
fn wait_for_peers_settled(&self) {
self.register_settled();
self.thread_sync.wait_peers_settled();
}
fn release(mut self) {
self.disarm();
let status = self.scheduler_status.clone();
self.thread_sync
.thread_done_with_settled(self.settled_registered.get(), move || {
status.store(STATUS_END, Ordering::Release);
});
}
}
impl Drop for MuxSlotGuard {
fn drop(&mut self) {
if !self.armed {
return;
}
let status = self.scheduler_status.clone();
self.thread_sync
.thread_done_with_settled(self.settled_registered.get(), move || {
status.store(STATUS_END, Ordering::Release);
});
}
}
struct SinkDisposal(Option<crate::core::packet_sink::strict::PacketSinkWorker>);
impl SinkDisposal {
fn new(worker: crate::core::packet_sink::strict::PacketSinkWorker) -> Self {
Self(Some(worker))
}
fn empty() -> Self {
Self(None)
}
fn is_empty(&self) -> bool {
self.0.is_none()
}
fn worker_mut(
&mut self,
) -> Option<&mut crate::core::packet_sink::strict::PacketSinkWorker> {
self.0.as_mut()
}
fn cancelled_cleanly(&self) -> bool {
self.0.as_ref().is_some_and(|worker| worker.cancelled_cleanly())
}
fn pending_error_cloned(&self) -> Option<crate::error::PacketSinkError> {
self.0.as_ref().and_then(|worker| worker.pending_error_cloned())
}
fn finish(
&mut self,
all_streams_terminal: bool,
ret: i32,
aborted: bool,
job_error: Option<JobFailureSummary>,
) {
if let Some(worker) = self.0.as_mut() {
worker.finish(all_streams_terminal, ret, aborted, job_error);
}
}
fn dispose(&mut self) -> bool {
self.0
.take()
.is_some_and(|worker| worker.dispose_contained())
}
}
impl Drop for SinkDisposal {
fn drop(&mut self) {
self.dispose();
}
}
struct BundleDisposal(Option<crate::core::packet_sink::PacketSink>);
impl BundleDisposal {
fn new(bundle: Option<crate::core::packet_sink::PacketSink>) -> Self {
Self(bundle)
}
fn is_empty(&self) -> bool {
self.0.is_none()
}
fn take(&mut self) -> Self {
Self(self.0.take())
}
fn take_inner(&mut self) -> Option<crate::core::packet_sink::PacketSink> {
self.0.take()
}
fn dispose(&mut self) -> bool {
self.0
.take()
.is_some_and(|bundle| bundle.dispose_contained())
}
}
impl Drop for BundleDisposal {
fn drop(&mut self) {
self.dispose();
}
}
struct MuxPanicErrorGuard {
scheduler_result: Arc<Mutex<Option<crate::error::Result<()>>>>,
}
impl Drop for MuxPanicErrorGuard {
fn drop(&mut self) {
if !std::thread::panicking() {
return;
}
crate::core::scheduler::ffmpeg_scheduler::set_scheduler_result_only(
&self.scheduler_result,
crate::error::Error::WorkerPanicked(
std::thread::current().name().unwrap_or("muxer").to_string(),
),
);
}
}
struct MuxPanicProgressGuard {
progress: Arc<OutputTelemetry>,
}
impl Drop for MuxPanicProgressGuard {
fn drop(&mut self) {
if !std::thread::panicking() {
return;
}
self.progress.mark_all_streams_finished();
}
}
struct MuxPanicStatusGuard {
scheduler_status: Arc<AtomicUsize>,
scheduler_result: Arc<Mutex<Option<crate::error::Result<()>>>>,
}
impl Drop for MuxPanicStatusGuard {
fn drop(&mut self) {
if !std::thread::panicking() {
return;
}
crate::core::scheduler::ffmpeg_scheduler::set_scheduler_result_only(
&self.scheduler_result,
crate::error::Error::WorkerPanicked(
std::thread::current().name().unwrap_or("muxer").to_string(),
),
);
let mut current = self.scheduler_status.load(Ordering::Acquire);
while !is_stopping(current) {
match self.scheduler_status.compare_exchange_weak(
current,
STATUS_END,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => break,
Err(actual) => current = actual,
}
}
crate::core::scheduler::ffmpeg_scheduler::notify_pause_waiters();
}
}
struct MuxRegistrationBarrier {
enc_registered: Arc<std::sync::atomic::AtomicBool>,
}
impl Drop for MuxRegistrationBarrier {
fn drop(&mut self) {
while !self.enc_registered.load(Ordering::Acquire) {
std::thread::sleep(Duration::from_millis(1));
}
}
}
struct MuxTeardownGuard {
pkt_receiver: Option<Receiver<PacketBox>>,
pre_receivers: Vec<PreMuxQueueReceiver>,
enc_handle_receiver: Receiver<std::thread::JoinHandle<()>>,
out_fmt_ctx: Option<FormatContext>,
}
impl MuxTeardownGuard {
fn new(
pkt_receiver: Option<Receiver<PacketBox>>,
pre_receivers: Vec<PreMuxQueueReceiver>,
enc_handle_receiver: Receiver<std::thread::JoinHandle<()>>,
out_fmt_ctx: FormatContext,
) -> Self {
Self {
pkt_receiver,
pre_receivers,
enc_handle_receiver,
out_fmt_ctx: Some(out_fmt_ctx),
}
}
fn ctx(&self) -> &FormatContext {
self.out_fmt_ctx
.as_ref()
.expect("mux teardown guard lost its output context")
}
fn take_pkt_receiver(&mut self) -> Option<Receiver<PacketBox>> {
self.pkt_receiver.take()
}
fn restore_pkt_receiver(&mut self, receiver: Option<Receiver<PacketBox>>) {
debug_assert!(self.pkt_receiver.is_none());
self.pkt_receiver = receiver;
}
fn take_pre_receivers(&mut self) -> Vec<PreMuxQueueReceiver> {
std::mem::take(&mut self.pre_receivers)
}
#[cfg(test)]
fn for_test() -> Self {
Self {
pkt_receiver: None,
pre_receivers: Vec::new(),
enc_handle_receiver: crossbeam_channel::unbounded().1,
out_fmt_ctx: None,
}
}
}
impl Drop for MuxTeardownGuard {
fn drop(&mut self) {
drop(self.pkt_receiver.take());
self.pre_receivers.clear();
while let Ok(handle) = self.enc_handle_receiver.try_recv() {
let _ = handle.join();
}
drop(self.out_fmt_ctx.take());
}
}
unsafe fn update_last_dts(
mux_stream_node: &Arc<SchNode>,
input_controller: &Arc<InputController>,
scheduler_status: &Arc<AtomicUsize>,
pkt: *const AVPacket,
) {
if (*pkt).dts != AV_NOPTS_VALUE {
let dts = av_rescale_q(
(*pkt).dts + (*pkt).duration,
(*pkt).time_base,
AV_TIME_BASE_Q,
);
let node = mux_stream_node.as_ref();
let SchNode::MuxStream {
src: _,
last_dts,
source_finished: _,
} = node
else {
unreachable!()
};
last_dts.store(dts, Ordering::Release);
input_controller.update_locked(scheduler_status);
}
}
unsafe fn streamcopy_rescale(
pkt: *mut AVPacket,
packet_data: &PacketData,
start_time_us: &Option<i64>,
recording_time_us: &Option<i64>,
started: &mut bool,
) -> i32 {
if !packet_data.is_copy {
return 0;
}
let dts = packet_data.dts_est;
let start_time = start_time_us.unwrap_or(0);
if let Some(recording_time_us) = recording_time_us {
if dts >= recording_time_us + start_time {
return AVERROR_EOF;
}
}
if !*started && (*pkt).flags & AV_PKT_FLAG_KEY == 0 {
return AVERROR(EAGAIN);
}
if !*started && start_time_us.is_some() {
let no_pts = (*pkt).pts == AV_NOPTS_VALUE;
let not_start = if no_pts {
dts < start_time
} else {
(*pkt).pts < av_rescale_q(start_time, AV_TIME_BASE_Q, (*pkt).time_base)
};
if not_start {
return AVERROR(EAGAIN);
}
}
let ts_offset = av_rescale_q(start_time, AV_TIME_BASE_Q, (*pkt).time_base);
if (*pkt).pts != AV_NOPTS_VALUE {
(*pkt).pts -= ts_offset;
}
if (*pkt).dts == AV_NOPTS_VALUE {
(*pkt).dts = av_rescale_q(dts, AV_TIME_BASE_Q, (*pkt).time_base);
} else if packet_data.codec_type == AVMEDIA_TYPE_AUDIO {
(*pkt).pts = (*pkt).dts - ts_offset;
}
(*pkt).dts -= ts_offset;
*started = true;
0
}
struct MuxWriteCfg<'a> {
has_bsf: bool,
oformat_flags: i32,
stream_count: usize,
out_fmt_ctx: &'a FormatContext,
packet_pool: &'a ObjPool<Packet>,
mux_stream_nodes: &'a [Arc<SchNode>],
input_controller: &'a Arc<InputController>,
scheduler_status: &'a Arc<AtomicUsize>,
progress: &'a OutputTelemetry,
}
struct MuxWriteState<'a> {
stream_pkt_templates: &'a mut [Option<PacketData>],
st_rescale_delta_last: &'a mut [i64],
st_last_dts: &'a mut [i64],
stream_eof: &'a mut [bool],
nb_done: &'a mut usize,
}
#[cfg(test)]
pub(crate) mod tcp_write_probe {
use std::sync::atomic::{AtomicI32, AtomicU64, Ordering};
use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError};
pub(crate) static ENTERED: AtomicU64 = AtomicU64::new(0);
pub(crate) static RETURNED: AtomicU64 = AtomicU64::new(0);
pub(crate) static LAST_RET: AtomicI32 = AtomicI32::new(0);
pub(crate) static LAST_ERR_GEN: AtomicU64 = AtomicU64::new(0);
pub(crate) static CUT_GEN: AtomicU64 = AtomicU64::new(0);
thread_local! {
pub(crate) static IN_FLIGHT: std::cell::Cell<Option<u64>> =
const { std::cell::Cell::new(None) };
}
pub(crate) fn note_cut_election() {
if let Some(generation) = IN_FLIGHT.with(std::cell::Cell::get) {
CUT_GEN.store(generation, Ordering::Release);
}
}
pub(crate) fn exclusive() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(PoisonError::into_inner)
}
}
unsafe fn write_packet(
cfg: &MuxWriteCfg,
state: &mut MuxWriteState,
sq_packet_box: &mut PacketBox,
) -> i32 {
mux_fixup_ts(cfg, state, sq_packet_box);
(*sq_packet_box.packet.as_mut_ptr()).stream_index =
sq_packet_box.packet_data.output_stream_index;
let progress_stream_index = sq_packet_box.packet_data.output_stream_index as usize;
let progress_ts_us = {
let pkt = sq_packet_box.packet.as_ptr();
let ts = if (*pkt).pts != AV_NOPTS_VALUE {
(*pkt).pts
} else {
(*pkt).dts
};
(ts != AV_NOPTS_VALUE).then(|| av_rescale_q(ts, (*pkt).time_base, AV_TIME_BASE_Q))
};
#[cfg(test)]
let counted = {
let url = (*cfg.out_fmt_ctx.as_ptr()).url;
let tcp = !url.is_null()
&& std::ffi::CStr::from_ptr(url)
.to_bytes()
.starts_with(b"tcp://");
tcp.then(|| tcp_write_probe::ENTERED.fetch_add(1, Ordering::Release) + 1)
};
#[cfg(test)]
if let Some(generation) = counted {
tcp_write_probe::IN_FLIGHT.with(|slot| slot.set(Some(generation)));
}
let ret =
av_interleaved_write_frame(cfg.out_fmt_ctx.as_ptr(), sq_packet_box.packet.as_mut_ptr());
#[cfg(test)]
if let Some(generation) = counted {
tcp_write_probe::IN_FLIGHT.with(|slot| slot.set(None));
tcp_write_probe::LAST_RET.store(ret, Ordering::Release);
if ret < 0 {
tcp_write_probe::LAST_ERR_GEN.store(generation, Ordering::Release);
}
tcp_write_probe::RETURNED.fetch_add(1, Ordering::Release);
}
if ret >= 0 {
cfg.progress
.record_written(progress_stream_index, progress_ts_us);
if (cfg.oformat_flags & AVFMT_NOFILE) == 0 && cfg.progress.is_observed() {
#[cfg(test)]
cfg.progress.note_perpacket_size_probe();
cfg.progress
.set_total_size(muxer_logical_pos((*cfg.out_fmt_ctx.as_ptr()).pb));
}
}
ret
}
unsafe fn muxer_filesize(pb: *mut AVIOContext) -> i64 {
if pb.is_null() {
return -1;
}
let size = avio_size(pb);
if size > 0 {
size
} else {
avio_seek(pb, 0, ffmpeg_sys_next::SEEK_CUR)
}
}
unsafe fn muxer_logical_pos(pb: *mut AVIOContext) -> i64 {
if pb.is_null() {
return -1;
}
avio_seek(pb, 0, ffmpeg_sys_next::SEEK_CUR)
}
unsafe fn mux_write_released(
packet_box: &mut PacketBox,
cfg: &MuxWriteCfg,
state: &mut MuxWriteState,
stream_bsfs: &mut [Option<BitStreamFilter>],
) -> i32 {
let stream_index = packet_box.packet_data.output_stream_index as usize;
if cfg.has_bsf {
if stream_bsfs.get(stream_index).is_some_and(|b| b.is_some()) {
state.stream_pkt_templates[stream_index] = Some(packet_box.packet_data);
}
mux_filter_and_write_packet(cfg, state, packet_box, stream_bsfs[stream_index].as_mut())
} else {
write_packet(cfg, state, packet_box)
}
}
unsafe fn finish_output_stream(
ost: usize,
cfg: &MuxWriteCfg,
state: &mut MuxWriteState,
stream_bsfs: &mut [Option<BitStreamFilter>],
) -> Result<(), i32> {
if ost >= cfg.stream_count || state.stream_eof[ost] {
return Ok(());
}
if cfg.has_bsf {
let fret = flush_stream_bsf(cfg, state, stream_bsfs, ost);
if fret < 0 {
return Err(fret);
}
}
state.stream_eof[ost] = true;
cfg.progress.mark_stream_finished(ost);
*state.nb_done += 1;
if ost < cfg.mux_stream_nodes.len() {
if let SchNode::MuxStream {
source_finished, ..
} = cfg.mux_stream_nodes[ost].as_ref()
{
source_finished.store(true, Ordering::Release);
}
}
cfg.input_controller.update_locked(cfg.scheduler_status);
Ok(())
}
fn sq_mux_pump(
sq: &mut SqMux,
released: &mut Vec<PacketBox>,
nf: &mut Vec<usize>,
cfg: &MuxWriteCfg,
state: &mut MuxWriteState,
stream_bsfs: &mut [Option<BitStreamFilter>],
) -> Result<bool, i32> {
released.clear();
sq.queue.drain_all_releasable(released);
for mut pb in released.drain(..) {
let wret = unsafe { mux_write_released(&mut pb, cfg, state, stream_bsfs) };
cfg.packet_pool.release(pb.packet);
if wret == AVERROR_EOF {
return Ok(true);
} else if wret < 0 {
return Err(wret);
}
}
nf.clear();
sq.queue.newly_finished(nf);
for &sq_j in nf.iter() {
let ost = sq.ostream[sq_j];
unsafe {
finish_output_stream(ost, cfg, state, stream_bsfs)?;
}
}
Ok(*state.nb_done == cfg.stream_count)
}
unsafe fn init_bitstream_filters(
out_fmt_ctx: *mut AVFormatContext,
bsf_chains: &StreamBsfChains,
stream_count: usize,
) -> Result<Vec<Option<BitStreamFilter>>, (String, i32)> {
if bsf_chains.is_empty() {
return Ok(Vec::new());
}
let mut stream_bsfs: Vec<Option<BitStreamFilter>> = (0..stream_count).map(|_| None).collect();
for i in 0..stream_count {
let st = *(*out_fmt_ctx).streams.add(i);
let codec_type = (*(*st).codecpar).codec_type;
let Some(chain) = bsf_chains.for_media_type(codec_type) else {
continue;
};
let name = || chain.to_string_lossy().into_owned();
let mut bsf = BitStreamFilter::parse(chain.as_c_str()).map_err(|ret| (name(), ret))?;
let ctx = bsf.as_ptr();
let ret = avcodec_parameters_copy((*ctx).par_in, (*st).codecpar);
if ret < 0 {
return Err((name(), ret));
}
(*ctx).time_base_in = (*st).time_base;
let ret = bsf.init();
if ret < 0 {
return Err((name(), ret));
}
let ret = avcodec_parameters_copy((*st).codecpar, (*ctx).par_out);
if ret < 0 {
return Err((name(), ret));
}
let old_tb = (*st).time_base;
let old_duration = (*st).duration;
(*st).time_base = bsf.time_base_out();
if old_duration != AV_NOPTS_VALUE && old_tb.num > 0 && old_tb.den > 0 {
(*st).duration = av_rescale_q(old_duration, old_tb, (*st).time_base);
}
stream_bsfs[i] = Some(bsf);
}
Ok(stream_bsfs)
}
unsafe fn mux_filter_and_write_packet(
cfg: &MuxWriteCfg,
state: &mut MuxWriteState,
packet_box: &mut PacketBox,
bsf: Option<&mut BitStreamFilter>,
) -> i32 {
let Some(bsf) = bsf else {
return write_packet(cfg, state, packet_box);
};
let pkt = packet_box.packet.as_mut_ptr();
av_packet_rescale_ts(pkt, (*pkt).time_base, bsf.time_base_in());
let ret = bsf.send_packet(pkt);
if ret < 0 {
return ret;
}
match drain_bsf_write(cfg, state, bsf, &packet_box.packet_data) {
BsfDrain::Exhausted => 0,
BsfDrain::Flushed => AVERROR_EOF,
BsfDrain::Err(ret) => ret,
}
}
enum BsfDrain {
Exhausted,
Flushed,
Err(i32),
}
unsafe fn drain_bsf_write(
cfg: &MuxWriteCfg,
state: &mut MuxWriteState,
bsf: &mut BitStreamFilter,
template: &PacketData,
) -> BsfDrain {
loop {
let ret = bsf.receive_packet();
if ret == AVERROR(EAGAIN) {
return BsfDrain::Exhausted;
} else if ret == AVERROR_EOF {
return BsfDrain::Flushed;
} else if ret < 0 {
error!("Error receiving a packet from a bitstream filter (skipping): ret={ret}");
return BsfDrain::Exhausted;
}
let mut out_pkt = match cfg.packet_pool.get() {
Ok(p) => p,
Err(_) => return BsfDrain::Err(AVERROR(ENOMEM)),
};
av_packet_move_ref(out_pkt.as_mut_ptr(), bsf.pkt_ptr());
(*out_pkt.as_mut_ptr()).time_base = bsf.time_base_out();
let mut out_box = PacketBox {
packet: out_pkt,
packet_data: *template,
};
let wret = write_packet(cfg, state, &mut out_box);
cfg.packet_pool.release(out_box.packet);
if wret < 0 {
return BsfDrain::Err(wret);
}
}
}
unsafe fn flush_stream_bsf(
cfg: &MuxWriteCfg,
state: &mut MuxWriteState,
stream_bsfs: &mut [Option<BitStreamFilter>],
stream_index: usize,
) -> i32 {
let Some(bsf) = stream_bsfs[stream_index].as_mut() else {
return 0;
};
let template = match &state.stream_pkt_templates[stream_index] {
Some(t) => *t,
None => {
let st = *(*cfg.out_fmt_ctx.as_ptr()).streams.add(stream_index);
PacketData {
dts_est: 0,
codec_type: (*(*st).codecpar).codec_type,
output_stream_index: stream_index as i32,
is_copy: false,
}
}
};
let ret = bsf.send_packet(std::ptr::null_mut());
if ret < 0 {
return ret;
}
match drain_bsf_write(cfg, state, bsf, &template) {
BsfDrain::Flushed | BsfDrain::Exhausted => 0,
BsfDrain::Err(ret) => ret,
}
}
unsafe fn mux_fixup_ts(cfg: &MuxWriteCfg, state: &mut MuxWriteState, packet_box: &mut PacketBox) {
let out_fmt_ctx = cfg.out_fmt_ctx.as_ptr();
let pkt = packet_box.packet.as_mut_ptr();
let packet_data = &packet_box.packet_data;
let stream_index = packet_data.output_stream_index;
if packet_data.codec_type == AVMEDIA_TYPE_AUDIO && packet_data.is_copy {
let codecpar = (**(*out_fmt_ctx).streams.add(stream_index as usize)).codecpar;
let mut duration = av_get_audio_frame_duration2(codecpar, (*pkt).size);
if duration == 0 {
duration = (*codecpar).frame_size;
}
let ts_rescale_delta_last = &mut state.st_rescale_delta_last[stream_index as usize];
(*pkt).dts = av_rescale_delta(
(*pkt).time_base,
(*pkt).dts,
AVRational {
num: 1,
den: (*codecpar).sample_rate,
},
duration,
ts_rescale_delta_last,
(**(*out_fmt_ctx).streams.add(stream_index as usize)).time_base,
);
(*pkt).pts = (*pkt).dts;
(*pkt).duration = av_rescale_q(
(*pkt).duration,
(*pkt).time_base,
(**(*out_fmt_ctx).streams.add(stream_index as usize)).time_base,
);
} else {
av_packet_rescale_ts(
pkt,
(*pkt).time_base,
(**(*out_fmt_ctx).streams.add(stream_index as usize)).time_base,
);
}
(*pkt).time_base = (**(*out_fmt_ctx).streams.add(stream_index as usize)).time_base;
let last_mux_dts = &mut state.st_last_dts[stream_index as usize];
if (cfg.oformat_flags & AVFMT_NOTIMESTAMPS) == 0 {
if (*pkt).dts != AV_NOPTS_VALUE && (*pkt).pts != AV_NOPTS_VALUE && (*pkt).dts > (*pkt).pts {
warn!(
"Invalid DTS: {} PTS: {}, replacing by guess",
(*pkt).dts,
(*pkt).pts
);
(*pkt).pts = (*pkt).pts + (*pkt).dts + *last_mux_dts + 1
- min3((*pkt).pts, (*pkt).dts, *last_mux_dts + 1)
- max3((*pkt).pts, (*pkt).dts, *last_mux_dts + 1);
(*pkt).dts = (*pkt).pts;
}
if (packet_data.codec_type == AVMEDIA_TYPE_AUDIO
|| packet_data.codec_type == AVMEDIA_TYPE_VIDEO
|| packet_data.codec_type == AVMEDIA_TYPE_SUBTITLE)
&& (*pkt).dts != AV_NOPTS_VALUE
&& *last_mux_dts != AV_NOPTS_VALUE
{
let max = *last_mux_dts + ((cfg.oformat_flags & AVFMT_TS_NONSTRICT) == 0) as i64;
if (*pkt).dts < max {
let loglevel =
if max - (*pkt).dts > 2 || packet_data.codec_type == AVMEDIA_TYPE_VIDEO {
AV_LOG_WARNING
} else {
AV_LOG_DEBUG
};
if loglevel == AV_LOG_WARNING {
warn!(
"Non-monotonic DTS; previous: {}, current: {}; ",
*last_mux_dts,
(*pkt).dts
);
warn!(
"changing to {}. This may result in incorrect timestamps in the output file.",
max
);
} else {
debug!(
"Non-monotonic DTS; previous: {}, current: {}; ",
*last_mux_dts,
(*pkt).dts
);
debug!(
"changing to {}. This may result in incorrect timestamps in the output file.",
max
);
}
if (*pkt).pts >= (*pkt).dts {
(*pkt).pts = std::cmp::max((*pkt).pts, max);
}
(*pkt).dts = max;
}
}
}
*last_mux_dts = (*pkt).dts;
}
fn min3(a: i64, b: i64, c: i64) -> i64 {
std::cmp::min(a, std::cmp::min(b, c))
}
fn max3(a: i64, b: i64, c: i64) -> i64 {
std::cmp::max(a, std::cmp::max(b, c))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::scheduler::ffmpeg_scheduler::{
is_stopping, STATUS_ABORT, STATUS_END, STATUS_RUN,
};
use std::sync::mpsc;
#[test]
fn registration_barrier_waits_for_enc_registered() {
let flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let barrier = MuxRegistrationBarrier {
enc_registered: flag.clone(),
};
let setter_flag = flag.clone();
let setter = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(100));
setter_flag.store(true, Ordering::Release);
});
drop(barrier);
assert!(
flag.load(Ordering::Acquire),
"the barrier returned before enc_registered was published"
);
setter.join().unwrap();
}
#[test]
fn fail_mux_init_releases_slot_and_records_error() {
let thread_sync = ThreadSynchronizer::new();
let scheduler_status = Arc::new(AtomicUsize::new(STATUS_RUN));
let scheduler_result: Arc<Mutex<Option<crate::error::Result<()>>>> =
Arc::new(Mutex::new(None));
thread_sync.thread_start();
fail_mux_init(
&scheduler_status,
&scheduler_result,
MuxInitQueues::NONE,
MuxTeardownGuard::for_test(),
MuxSlotGuard::armed(thread_sync.clone(), scheduler_status.clone()),
Muxing(MuxingOperationError::ThreadExited),
BundleDisposal::new(None),
);
let (done_tx, done_rx) = mpsc::channel();
let sync_clone = thread_sync.clone();
std::thread::spawn(move || {
sync_clone.wait_for_all_threads();
let _ = done_tx.send(());
});
assert!(
done_rx.recv_timeout(Duration::from_secs(5)).is_ok(),
"mux thread slot leaked: wait_for_all_threads did not return"
);
assert!(is_stopping(scheduler_status.load(Ordering::Acquire)));
assert!(matches!(&*scheduler_result.lock().unwrap(), Some(Err(_))));
}
#[test]
fn mux_worker_unwind_records_error_before_the_slot_drains() {
let thread_sync = ThreadSynchronizer::new();
let scheduler_status = Arc::new(AtomicUsize::new(STATUS_RUN));
let scheduler_result: Arc<Mutex<Option<crate::error::Result<()>>>> =
Arc::new(Mutex::new(None));
let mux_done_remaining = Arc::new(AtomicUsize::new(1));
thread_sync.thread_start();
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _slot_guard = MuxSlotGuard::armed(thread_sync.clone(), scheduler_status.clone());
let _mux_done = MuxDoneGuard::new(mux_done_remaining.clone(), scheduler_status.clone());
let _panic_status = MuxPanicStatusGuard {
scheduler_status: scheduler_status.clone(),
scheduler_result: scheduler_result.clone(),
};
panic!("test-injected mux worker panic");
}));
assert!(outcome.is_err(), "the injected panic must unwind");
let (done_tx, done_rx) = mpsc::channel();
let sync_clone = thread_sync.clone();
std::thread::spawn(move || {
sync_clone.wait_for_all_threads();
let _ = done_tx.send(());
});
assert!(
done_rx.recv_timeout(Duration::from_secs(5)).is_ok(),
"mux worker slot leaked on the panic path"
);
assert!(
matches!(&*scheduler_result.lock().unwrap(), Some(Err(_))),
"the worker panic must be recorded as the job error"
);
assert!(is_stopping(scheduler_status.load(Ordering::Acquire)));
}
#[test]
fn release_mux_slot_unblocks_wait_without_error() {
let thread_sync = ThreadSynchronizer::new();
let scheduler_status = Arc::new(AtomicUsize::new(STATUS_RUN));
thread_sync.thread_start();
release_mux_slot(&scheduler_status, &thread_sync);
let (done_tx, done_rx) = mpsc::channel();
let sync_clone = thread_sync.clone();
std::thread::spawn(move || {
sync_clone.wait_for_all_threads();
let _ = done_tx.send(());
});
assert!(
done_rx.recv_timeout(Duration::from_secs(5)).is_ok(),
"zero-stream mux slot leaked: wait_for_all_threads did not return"
);
assert!(is_stopping(scheduler_status.load(Ordering::Acquire)));
}
#[test]
fn streamless_release_is_not_premature_with_pre_counted_slots() {
let thread_sync = ThreadSynchronizer::new();
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
thread_sync.thread_start();
thread_sync.thread_start();
release_mux_slot(&status, &thread_sync);
assert_eq!(
status.load(Ordering::Acquire),
STATUS_RUN,
"releasing one of two pre-counted mux slots must not publish a terminal status"
);
release_mux_slot(&status, &thread_sync);
assert!(
is_stopping(status.load(Ordering::Acquire)),
"the last mux slot release publishes STATUS_END"
);
}
#[test]
fn mux_done_guard_publishes_end_only_after_last_muxer() {
let remaining = Arc::new(AtomicUsize::new(2));
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let g1 = MuxDoneGuard::new(remaining.clone(), status.clone());
let g2 = MuxDoneGuard::new(remaining.clone(), status.clone());
drop(g1);
assert_eq!(
status.load(Ordering::Acquire),
STATUS_RUN,
"one of two muxers finishing must not terminate the scheduler"
);
drop(g2);
assert_eq!(
status.load(Ordering::Acquire),
STATUS_END,
"the last muxer finishing must publish STATUS_END"
);
}
#[test]
fn mux_done_guard_never_downgrades_abort() {
let remaining = Arc::new(AtomicUsize::new(1));
let status = Arc::new(AtomicUsize::new(STATUS_ABORT));
drop(MuxDoneGuard::new(remaining, status.clone()));
assert_eq!(
status.load(Ordering::Acquire),
STATUS_ABORT,
"the last muxer must not overwrite an abort with STATUS_END"
);
}
#[test]
fn last_mux_done_releases_a_choked_demuxer() {
use crate::util::sch_waiter::SchWaiter;
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let waiter = Arc::new(SchWaiter::new());
waiter.set(true);
let (tx, rx) = mpsc::channel();
let w = Arc::clone(&waiter);
let st = Arc::clone(&status);
std::thread::spawn(move || {
w.wait_with_scheduler_status(&st, false);
let _ = tx.send(());
});
std::thread::sleep(Duration::from_millis(150));
assert!(
rx.try_recv().is_err(),
"the demuxer must stay parked until a terminal status is published"
);
let remaining = Arc::new(AtomicUsize::new(1));
drop(MuxDoneGuard::new(remaining, status.clone()));
rx.recv_timeout(Duration::from_secs(2))
.expect("the choked demuxer must be released when the last muxer finishes");
}
#[test]
fn mux_slot_guard_releases_slot_on_armed_drop() {
let thread_sync = ThreadSynchronizer::new();
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
thread_sync.thread_start();
drop(MuxSlotGuard::armed(thread_sync.clone(), status.clone()));
let (tx, rx) = mpsc::channel();
let sync = thread_sync.clone();
std::thread::spawn(move || {
sync.wait_for_all_threads();
let _ = tx.send(());
});
assert!(
rx.recv_timeout(Duration::from_secs(2)).is_ok(),
"an armed MuxSlotGuard drop must release the slot (else wait() hangs)"
);
assert!(
is_stopping(status.load(Ordering::Acquire)),
"the armed drop also publishes a terminal status"
);
}
#[test]
fn mux_slot_guard_disarmed_drop_is_a_noop() {
let thread_sync = ThreadSynchronizer::new();
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
thread_sync.thread_start();
let mut guard = MuxSlotGuard::armed(thread_sync.clone(), status.clone());
guard.disarm();
drop(guard);
let (tx, rx) = mpsc::channel();
let sync = thread_sync.clone();
std::thread::spawn(move || {
sync.wait_for_all_threads();
let _ = tx.send(());
});
assert!(
rx.recv_timeout(Duration::from_millis(200)).is_err(),
"a disarmed MuxSlotGuard must NOT release the slot (the manual path owns it)"
);
assert_eq!(
status.load(Ordering::Acquire),
STATUS_RUN,
"a disarmed guard must not publish a terminal status"
);
thread_sync.thread_done_with(|| {});
}
#[test]
fn teardown_guard_unparks_pre_mux_sender_and_joins_encoder() {
use crate::core::context::pre_mux_queue::{channel, PreMuxQueueConfig, PreQueueTryPush};
use crate::core::context::PacketData;
use ffmpeg_sys_next::AVMediaType::AVMEDIA_TYPE_VIDEO;
use std::sync::atomic::AtomicBool;
let (pre_sender, pre_receiver) = channel(PreMuxQueueConfig {
max_packets: 1,
data_threshold: 1,
});
let first = PacketBox {
packet: Packet::empty(),
packet_data: PacketData {
dts_est: 0,
codec_type: AVMEDIA_TYPE_VIDEO,
output_stream_index: 0,
is_copy: false,
},
};
assert!(
matches!(pre_sender.try_push(first), PreQueueTryPush::Sent),
"the first packet must be admitted (queue below max_packets)"
);
let parked = Arc::new(AtomicBool::new(false));
let joined_cleanly = Arc::new(AtomicBool::new(false));
let parked_probe = Arc::clone(&parked);
let joined_probe = Arc::clone(&joined_cleanly);
let handle = std::thread::spawn(move || {
parked_probe.store(true, Ordering::SeqCst);
pre_sender.wait_for_space(2, Duration::from_secs(30));
joined_probe.store(true, Ordering::SeqCst);
});
let (handle_tx, handle_rx) = crossbeam_channel::unbounded();
handle_tx.send(handle).unwrap();
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while !parked.load(Ordering::SeqCst) {
assert!(std::time::Instant::now() < deadline, "encoder never parked");
std::thread::yield_now();
}
std::thread::sleep(Duration::from_millis(100));
assert!(
!joined_cleanly.load(Ordering::SeqCst),
"the encoder was supposed to be parked on the full queue, \
but it already ran to completion"
);
let guard = MuxTeardownGuard {
pkt_receiver: None,
pre_receivers: vec![pre_receiver],
enc_handle_receiver: handle_rx,
out_fmt_ctx: None,
};
let start = std::time::Instant::now();
drop(guard);
let elapsed = start.elapsed();
assert!(
joined_cleanly.load(Ordering::SeqCst),
"the guard's join completed without the encoder having run to completion"
);
assert!(
elapsed < Duration::from_secs(5),
"the guard's drop took {elapsed:?}: the parked sender was not woken by the \
pre-mux close (join waited out the 30s park timeout instead)"
);
}
#[test]
fn mux_panic_status_guard_records_and_publishes_on_unwind() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let result: Arc<Mutex<Option<crate::error::Result<()>>>> = Arc::new(Mutex::new(None));
let guard = MuxPanicStatusGuard {
scheduler_status: status.clone(),
scheduler_result: result.clone(),
};
let handle = std::thread::Builder::new()
.name("panicky-muxer".to_string())
.spawn(move || {
let _guard = guard;
panic!("test-injected mux panic");
})
.unwrap();
let _ = handle.join();
assert!(is_stopping(status.load(Ordering::Acquire)));
let recorded = result.lock().unwrap().take();
match recorded {
Some(Err(crate::error::Error::WorkerPanicked(name))) => {
assert_eq!(name, "panicky-muxer");
}
other => panic!("expected WorkerPanicked, got {other:?}"),
}
}
#[test]
fn mux_init_internal_panic_publishes_before_join_and_releases_slot_once() {
use std::sync::atomic::AtomicBool;
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let result: Arc<Mutex<Option<crate::error::Result<()>>>> = Arc::new(Mutex::new(None));
let thread_sync = ThreadSynchronizer::new();
thread_sync.thread_start();
let enc_status = status.clone();
let enc_joined = Arc::new(AtomicBool::new(false));
let enc_joined_probe = enc_joined.clone();
let enc_handle = std::thread::spawn(move || {
let deadline = std::time::Instant::now() + Duration::from_secs(10);
while !is_stopping(enc_status.load(Ordering::Acquire)) {
if std::time::Instant::now() >= deadline {
return;
}
std::thread::sleep(Duration::from_millis(2));
}
enc_joined_probe.store(true, Ordering::SeqCst);
});
let (handle_tx, handle_rx) = crossbeam_channel::unbounded();
handle_tx.send(enc_handle).unwrap();
let remaining = Arc::new(AtomicUsize::new(2));
let status_t = status.clone();
let result_t = result.clone();
let sync_t = thread_sync.clone();
let panicker = std::thread::Builder::new()
.name("mux-init-panicker".to_string())
.spawn(move || {
let slot_guard = MuxSlotGuard::armed(sync_t.clone(), status_t.clone());
let guard = MuxTeardownGuard {
pkt_receiver: None,
pre_receivers: Vec::new(),
enc_handle_receiver: handle_rx,
out_fmt_ctx: None,
};
let mux_done = MuxDoneGuard::new(remaining, status_t.clone());
let _panic_status = MuxPanicStatusGuard {
scheduler_status: status_t.clone(),
scheduler_result: result_t.clone(),
};
let _ = (&slot_guard, &guard, &mux_done);
panic!("test-injected panic inside _mux_init");
})
.unwrap();
let _ = panicker.join();
assert!(
enc_joined.load(Ordering::SeqCst),
"the ordered unwind must publish the terminal status before the encoder join"
);
assert!(is_stopping(status.load(Ordering::Acquire)));
match result.lock().unwrap().take() {
Some(Err(crate::error::Error::WorkerPanicked(name))) => {
assert_eq!(name, "mux-init-panicker");
}
other => panic!("expected WorkerPanicked, got {other:?}"),
}
let (tx, rx) = mpsc::channel();
let sync = thread_sync.clone();
std::thread::spawn(move || {
sync.wait_for_all_threads();
let _ = tx.send(());
});
assert!(
rx.recv_timeout(Duration::from_secs(5)).is_ok(),
"the mux slot must be released exactly once on the internal-panic unwind"
);
}
#[test]
fn mux_worker_spawn_failure_unparks_and_joins_before_releasing_slot() {
use crate::core::context::pre_mux_queue::{channel, PreMuxQueueConfig, PreQueueTryPush};
use crate::core::context::PacketData;
use ffmpeg_sys_next::AVMediaType::AVMEDIA_TYPE_VIDEO;
use std::sync::atomic::AtomicBool;
let thread_sync = ThreadSynchronizer::new();
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let result: Arc<Mutex<Option<crate::error::Result<()>>>> = Arc::new(Mutex::new(None));
thread_sync.thread_start();
let (pre_sender, pre_receiver) = channel(PreMuxQueueConfig {
max_packets: 1,
data_threshold: 1,
});
let first = PacketBox {
packet: Packet::empty(),
packet_data: PacketData {
dts_est: 0,
codec_type: AVMEDIA_TYPE_VIDEO,
output_stream_index: 0,
is_copy: false,
},
};
assert!(matches!(pre_sender.try_push(first), PreQueueTryPush::Sent));
let joined = Arc::new(AtomicBool::new(false));
let joined_probe = Arc::clone(&joined);
let enc = std::thread::spawn(move || {
pre_sender.wait_for_space(2, Duration::from_secs(30));
joined_probe.store(true, Ordering::SeqCst);
});
let (handle_tx, handle_rx) = crossbeam_channel::unbounded();
handle_tx.send(enc).unwrap();
let src_pre_receivers = vec![pre_receiver];
let guard = MuxTeardownGuard {
pkt_receiver: None,
pre_receivers: Vec::new(),
enc_handle_receiver: handle_rx,
out_fmt_ctx: None,
};
let guard_slot = Arc::new(Mutex::new((
Some(guard),
Some(MuxSlotGuard::armed(thread_sync.clone(), status.clone())),
None,
None,
)));
let status_c = status.clone();
let result_c = result.clone();
let (done_tx, done_rx) = mpsc::channel();
std::thread::spawn(move || {
fail_mux_worker_spawn(&status_c, &result_c, src_pre_receivers, &guard_slot);
let _ = done_tx.send(());
});
assert!(
done_rx.recv_timeout(Duration::from_secs(5)).is_ok(),
"fail_mux_worker_spawn hung: the pre-mux close did not unpark the encoder before the join"
);
assert!(
joined.load(Ordering::SeqCst),
"the encoder must be unparked (pre-mux close) before the guard's join"
);
assert!(is_stopping(status.load(Ordering::Acquire)));
assert!(matches!(&*result.lock().unwrap(), Some(Err(_))));
let (tx, rx) = mpsc::channel();
let sync = thread_sync.clone();
std::thread::spawn(move || {
sync.wait_for_all_threads();
let _ = tx.send(());
});
assert!(
rx.recv_timeout(Duration::from_secs(5)).is_ok(),
"the pre-counted slot must be released exactly once on spawn failure"
);
}
#[test]
fn mux_panic_status_guard_never_downgrades_abort() {
let status = Arc::new(AtomicUsize::new(STATUS_ABORT));
let result: Arc<Mutex<Option<crate::error::Result<()>>>> = Arc::new(Mutex::new(None));
let guard = MuxPanicStatusGuard {
scheduler_status: status.clone(),
scheduler_result: result.clone(),
};
let _ = std::thread::spawn(move || {
let _guard = guard;
panic!("test-injected mux panic under abort");
})
.join();
assert_eq!(
status.load(Ordering::Acquire),
STATUS_ABORT,
"the panic publish must not overwrite an abort"
);
assert!(
result.lock().unwrap().is_some(),
"the panic is still recorded"
);
}
#[test]
fn mux_panic_status_guard_is_a_noop_without_a_panic() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let result: Arc<Mutex<Option<crate::error::Result<()>>>> = Arc::new(Mutex::new(None));
drop(MuxPanicStatusGuard {
scheduler_status: status.clone(),
scheduler_result: result.clone(),
});
assert_eq!(status.load(Ordering::Acquire), STATUS_RUN);
assert!(result.lock().unwrap().is_none());
}
#[test]
fn build_sq_mux_maps_members_with_attachment_gap() {
let plan = SqMuxPlan {
buf_size_us: 5_000_000,
streams: vec![(0, true, None), (1, true, Some(7)), (3, true, None)],
};
let sq = build_sq_mux(plan, 4);
assert_eq!(sq.sq_idx, vec![Some(0), Some(1), None, Some(2)]);
assert_eq!(sq.ostream, vec![0, 1, 3]);
}
}