Skip to main content

ez_ffmpeg/core/context/
mod.rs

1use crate::error::AllocFrameError;
2use ffmpeg_sys_next::AVMediaType::{
3    AVMEDIA_TYPE_ATTACHMENT, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_DATA, AVMEDIA_TYPE_SUBTITLE,
4    AVMEDIA_TYPE_VIDEO,
5};
6// Only SideDataList::push_clone (gated to FFmpeg 8+) clones entries; the
7// free side runs on every real lane via Drop. Both symbols are FFmpeg 7.0+,
8// and docs.rs generates its bindings against an older apt FFmpeg — so like
9// every other >=7.0 symbol in this crate they stay out of docsrs builds.
10#[cfg(ffmpeg_8_0)]
11use ffmpeg_sys_next::av_frame_side_data_clone;
12#[cfg(not(docsrs))]
13use ffmpeg_sys_next::av_frame_side_data_free;
14use ffmpeg_sys_next::{
15    av_freep, av_gettime_relative, avcodec_free_context, avformat_close_input,
16    avformat_free_context, avio_closep, avio_context_free, AVCodecContext, AVFormatContext,
17    AVFrameSideData, AVIOContext, AVMediaType, AVRational, AVStream, AVFMT_NOFILE,
18};
19use std::ffi::c_void;
20use std::ptr::null_mut;
21use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
22use std::sync::Arc;
23
24/// How long output I/O may continue after STATUS_END before the interrupt
25/// callback cuts it: long enough for a healthy muxer to finish its trailer,
26/// short enough that stop() on a dead network peer returns within a second.
27/// While a muxer holds an [`OutputFinalizeGuard`] the window does not apply
28/// at all — a `movflags=+faststart` trailer rewrites the whole file and can
29/// legitimately take far longer than any fixed grace.
30const OUTPUT_END_GRACE_US: i64 = 500_000;
31
32/// Default size of the AVIO buffer backing a custom read/write callback. FFmpeg
33/// hands the callback one buffer-sized chunk at a time, so a larger buffer means
34/// fewer Rust↔FFmpeg round-trips for sequential/network IO; the 64 KiB default
35/// keeps first-packet latency low for live/low-latency use. Configurable per
36/// Input/Output via set_io_buffer_size (sys-06).
37pub(crate) const DEFAULT_CUSTOM_IO_BUFFER_SIZE: usize = 64 * 1024;
38
39/// Shared state behind the AVIO interrupt callbacks (fftools installs
40/// decode_interrupt_cb on inputs and outputs, ffmpeg_mux_init.c:3326,3371).
41///
42/// Inputs are interrupted as soon as the scheduler is stopping: nothing
43/// meaningful is read after that. Outputs distinguish the terminal states:
44/// STATUS_ABORT cuts I/O immediately (the caller gave up on the files), while
45/// STATUS_END grants a grace window so trailers still get written — only an
46/// output that stays blocked past the window (dead network peer) is cut.
47pub(crate) struct InterruptState {
48    scheduler_status: Arc<AtomicUsize>,
49    // Microsecond timestamp of the first output callback that observed
50    // STATUS_END; 0 = not observed yet.
51    end_grace_start_us: AtomicI64,
52    /// Number of mux workers currently inside their finalize window
53    /// (av_write_trailer through the output-context free). While > 0,
54    /// STATUS_END does not interrupt output I/O — only STATUS_ABORT does: a
55    /// graceful stop() must not corrupt a trailer that is being written
56    /// (H3), and abort() stays the hard-cancel escape hatch.
57    finalizing_outputs: AtomicUsize,
58}
59
60impl InterruptState {
61    pub(crate) fn new(scheduler_status: Arc<AtomicUsize>) -> Self {
62        Self {
63            scheduler_status,
64            end_grace_start_us: AtomicI64::new(0),
65            finalizing_outputs: AtomicUsize::new(0),
66        }
67    }
68
69    /// Marks one output as finalizing for the guard's lifetime.
70    pub(crate) fn begin_output_finalize(self: &Arc<Self>) -> OutputFinalizeGuard {
71        self.finalizing_outputs.fetch_add(1, Ordering::AcqRel);
72        OutputFinalizeGuard {
73            state: Arc::clone(self),
74        }
75    }
76
77    fn should_interrupt_input(&self) -> bool {
78        crate::core::scheduler::ffmpeg_scheduler::is_stopping(
79            self.scheduler_status.load(Ordering::Acquire),
80        )
81    }
82
83    fn should_interrupt_output(&self) -> bool {
84        let status = self.scheduler_status.load(Ordering::Acquire);
85        if status == crate::core::scheduler::ffmpeg_scheduler::STATUS_ABORT {
86            return true;
87        }
88        if status != crate::core::scheduler::ffmpeg_scheduler::STATUS_END {
89            return false;
90        }
91        if self.finalizing_outputs.load(Ordering::Acquire) > 0 {
92            // A trailer (e.g. a faststart moov rewrite) is in flight somewhere
93            // on this scheduler; only abort() may cut output I/O now. Checked
94            // BEFORE the grace CAS so the clock does not even start while a
95            // finalize is running; once the last guard drops, a still-blocked
96            // OTHER output starts its grace normally.
97            return false;
98        }
99        let now = unsafe { av_gettime_relative() };
100        let start = match self.end_grace_start_us.compare_exchange(
101            0,
102            now,
103            Ordering::AcqRel,
104            Ordering::Acquire,
105        ) {
106            Ok(_) => now,
107            Err(previous) => previous,
108        };
109        now - start > OUTPUT_END_GRACE_US
110    }
111}
112
113/// RAII finalize marker. The decrement is mandatory even on unwind — a stuck
114/// counter would exempt every later STATUS_END grace cut for this scheduler,
115/// and stop() on a blocked sibling output would hang forever.
116pub(crate) struct OutputFinalizeGuard {
117    state: Arc<InterruptState>,
118}
119
120impl Drop for OutputFinalizeGuard {
121    fn drop(&mut self) {
122        self.state.finalizing_outputs.fetch_sub(1, Ordering::AcqRel);
123    }
124}
125
126/// # Safety
127/// `opaque` must point to an `InterruptState` that outlives every
128/// AVFormatContext carrying this callback (the owning FfmpegContext holds the
129/// Arc and outlives all worker threads).
130pub(crate) unsafe extern "C" fn input_interrupt_cb(opaque: *mut c_void) -> libc::c_int {
131    let state = &*(opaque as *const InterruptState);
132    state.should_interrupt_input() as libc::c_int
133}
134
135/// # Safety
136/// Same contract as [`input_interrupt_cb`].
137pub(crate) unsafe extern "C" fn output_interrupt_cb(opaque: *mut c_void) -> libc::c_int {
138    let state = &*(opaque as *const InterruptState);
139    state.should_interrupt_output() as libc::c_int
140}
141
142/// Gate for the muxer's deferred start. fftools: `SchMux.mux_started` +
143/// `PreMuxQueue` under `Scheduler.mux_ready_lock` (ffmpeg_sched.c).
144///
145/// Until the muxer thread is running, encoders park packets in a bounded
146/// pre-queue; at start the muxer drains that queue and flips `started`.
147/// Without a lock the flip races the send: an encoder that read
148/// `started == false` can enqueue into the pre-queue AFTER the drain
149/// finished, and that packet is never delivered. Pre-queue sends therefore
150/// happen under the same lock as the drain-and-flip.
151pub(crate) struct MuxStartGate {
152    started: std::sync::atomic::AtomicBool,
153    lock: std::sync::Mutex<()>,
154}
155
156/// What a gated pre-queue send resolved to.
157pub(crate) enum PreSendOutcome {
158    /// Parked in the pre-queue; the drain will deliver it.
159    Sent,
160    /// The gate opened first: send to the live queue instead.
161    Started(PacketBox),
162    /// Pre-queue full: park on the queue condvar and retry (the gate lock
163    /// must not be held across the wait, or the drain could never run).
164    Full(PacketBox),
165    /// Pre-queue receiver is gone (muxer never started).
166    Disconnected(PacketBox),
167}
168
169impl MuxStartGate {
170    pub(crate) fn new() -> Self {
171        Self {
172            started: std::sync::atomic::AtomicBool::new(false),
173            lock: std::sync::Mutex::new(()),
174        }
175    }
176
177    pub(crate) fn is_started(&self) -> bool {
178        self.started.load(Ordering::Acquire)
179    }
180
181    /// Runs the pre-queue drain and opens the gate as one atomic step.
182    pub(crate) fn start_with(&self, drain: impl FnOnce()) {
183        let _guard = self.lock.lock().unwrap();
184        drain();
185        self.started.store(true, Ordering::Release);
186    }
187
188    /// Attempts a pre-queue send while the gate is verifiably closed.
189    pub(crate) fn send_pre(
190        &self,
191        pre_sender: &pre_mux_queue::PreMuxQueueSender,
192        packet_box: PacketBox,
193    ) -> PreSendOutcome {
194        let _guard = self.lock.lock().unwrap();
195        if self.started.load(Ordering::Acquire) {
196            return PreSendOutcome::Started(packet_box);
197        }
198        match pre_sender.try_push(packet_box) {
199            pre_mux_queue::PreQueueTryPush::Sent => PreSendOutcome::Sent,
200            pre_mux_queue::PreQueueTryPush::Full(pb) => PreSendOutcome::Full(pb),
201            pre_mux_queue::PreQueueTryPush::Disconnected(pb) => PreSendOutcome::Disconnected(pb),
202        }
203    }
204}
205
206use ffmpeg_context::{InputOpaque, OutputOpaque};
207
208/// The **ffmpeg_context** module is responsible for assembling FFmpeg’s configuration:
209/// inputs, outputs, codecs, filters, and other parameters needed to construct a
210/// complete media processing pipeline.
211///
212/// # Example
213/// ```rust,ignore
214///
215/// // Build an FFmpeg context with one input, some filter settings, and one output
216/// let context = FfmpegContext::builder()
217///     .input("test.mp4")
218///     .filter_desc("hue=s=0")
219///     .output("output.mp4")
220///     .build()
221///     .unwrap();
222/// // The context now holds all info needed for an FFmpeg job.
223/// ```
224pub mod ffmpeg_context;
225
226/// The **ffmpeg_context_builder** module defines the builder pattern for creating
227/// [`FfmpegContext`](ffmpeg_context::FfmpegContext) objects.
228///
229/// It exposes the [`FfmpegContextBuilder`](ffmpeg_context_builder::FfmpegContextBuilder) struct, which allows you to:
230/// - Configure multiple [`Input`](input::Input) and
231///   [`Output`](output::Output) streams.
232/// - Attach filter descriptions via [`FilterComplex`](crate::core::context::filter_complex::FilterComplex)
233///   or inline strings (e.g., `"scale=1280:720"`, `"hue=s=0"`).
234/// - Produce a finished `FfmpegContext` that can then be executed by
235///   [`FfmpegScheduler`](crate::core::scheduler::ffmpeg_scheduler::FfmpegScheduler).
236///
237/// # Examples
238///
239/// ```rust,ignore
240/// // 1. Create a builder (usually via FfmpegContext::builder())
241/// let builder = FfmpegContext::builder();
242///
243/// // 2. Add inputs, outputs, and filters
244/// let ffmpeg_context = builder
245///     .input("input.mp4")
246///     .filter_desc("hue=s=0")
247///     .output("output.mp4")
248///     .build()
249///     .expect("Failed to build FfmpegContext");
250///
251/// // 3. Use `ffmpeg_context` with FfmpegScheduler (e.g., `.start()` and `.wait()`).
252/// ```
253pub mod ffmpeg_context_builder;
254
255/// The **input** module defines the [`Input`](crate::core::context::input::Input) struct,
256/// representing an FFmpeg input source. An input can be:
257/// - A file path or URL (e.g., `"video.mp4"`, `rtmp://example.com/live/stream`).
258/// - A **custom data source** via a `read_callback` (and optionally `seek_callback`) for
259///   advanced scenarios like in-memory buffers or network protocols.
260///
261/// You can also specify **frame pipelines** to apply custom [`FrameFilter`](crate::core::filter::frame_filter::FrameFilter)
262/// transformations **after decoding** but **before** the frames move on to the rest of the pipeline.
263///
264/// # Example
265///
266/// ```rust,ignore
267/// use ez_ffmpeg::core::context::input::Input;
268///
269/// // Basic file or network URL:
270/// let file_input: Input = "example.mp4".into();
271///
272/// // Or a custom read callback:
273/// let custom_input = Input::new_by_read_callback(|buf| {
274///     // Fill `buf` with data from your source
275///     // Return the number of bytes read, or negative for errors
276///     0
277/// });
278/// ```
279pub mod input;
280
281/// The **output** module defines the [`Output`](crate::core::context::output::Output) struct,
282/// representing an FFmpeg output destination. An output may be:
283/// - A file path or URL (e.g., `"output.mp4"`, `rtmp://...`).
284/// - A **custom write callback** that processes encoded data (e.g., storing it
285///   in-memory or sending it over a custom network protocol).
286///
287/// You can specify additional details such as:
288/// - **Container format** (e.g., `"mp4"`, `"flv"`, `"mkv"`).
289/// - **Video/Audio/Subtitle codecs** (e.g., `"h264"`, `"aac"`, `"mov_text"`).
290/// - **Frame pipelines** to apply [`FrameFilter`](crate::core::filter::frame_filter::FrameFilter)
291///   transformations **before encoding**.
292///
293/// # Example
294///
295/// ```rust,ignore
296/// use ez_ffmpeg::core::context::output::Output;
297///
298/// // Basic file/URL output:
299/// let file_output: Output = "output.mp4".into();
300///
301/// // Or a custom write callback:
302/// let custom_output = Output::new_by_write_callback(|encoded_data| {
303///     // Write `encoded_data` somewhere
304///     encoded_data.len() as i32
305/// }).set_format("mp4");
306/// ```
307pub mod output;
308
309/// The **filter_complex** module defines the [`FilterComplex`](crate::core::context::filter_complex::FilterComplex)
310/// struct, which encapsulates one or more FFmpeg filter descriptions (e.g., `"scale=1280:720"`,
311/// `"hue=s=0"`, etc.). You can use `FilterComplex` to construct more advanced or multi-step
312/// filter graphs than simple inline strings allow.
313///
314/// `FilterComplex` can also associate a particular hardware device (e.g., for GPU-based
315/// filtering) via `hw_device`.
316///
317/// # Example
318///
319/// ```rust,ignore
320/// use ez_ffmpeg::core::context::filter_complex::FilterComplex;
321///
322/// // Build a FilterComplex from a string:
323/// let my_filters = FilterComplex::from("scale=1280:720");
324///
325/// // Optionally specify a hardware device (e.g., "cuda"):
326/// // my_filters.set_hw_device("cuda");
327/// ```
328pub mod filter_complex;
329
330pub(super) mod attachment;
331pub(super) mod decoder_stream;
332pub(super) mod demuxer;
333pub(super) mod encoder_stream;
334pub(super) mod filter_graph;
335pub(super) mod input_filter;
336pub(super) mod muxer;
337pub(super) mod obj_pool;
338pub(super) mod output_filter;
339pub(super) mod pre_mux_queue;
340
341/// The **null_output** module provides a custom null output implementation for FFmpeg
342/// that discards all data while supporting seeking.
343///
344/// It exposes the [`create_null_output`](null_output::create_null_output) function, which returns an
345/// [`Output`](crate::Output) object configured to:
346/// - Discard all written data, behaving like `/dev/null`.
347/// - Maintain a seekable position state using atomic operations for thread-safe, high-performance access.
348/// - Support scenarios such as testing or processing streaming inputs (e.g., RTMP) where no output file is needed.
349///
350/// # Usage Scenario
351/// This module is useful when processing FFmpeg input streams without generating an output file, such as
352/// when handling RTMP streams that require a seekable output format like MP4, even if the output is discarded.
353///
354/// # Examples
355///
356/// ```rust,ignore
357/// use ez_ffmpeg::Output;
358/// let output: Output = create_null_output();
359/// // Pass `output` to an FFmpeg context for processing
360/// ```
361///
362/// # Performance
363/// - Utilizes `AtomicU64` with `Relaxed` ordering for lock-free position tracking, ensuring efficient concurrent access.
364/// - Write and seek operations are optimized to minimize overhead by avoiding locks.
365///
366/// # Notes
367/// - The default output format is "mp4", but this can be modified using `set_format` as needed.
368/// - Write operations assume individual buffers do not exceed `i32::MAX` bytes, which aligns with typical FFmpeg usage.
369pub mod null_output;
370
371pub(crate) struct CodecContext {
372    inner: *mut AVCodecContext,
373}
374
375// SAFETY: CodecContext can be sent to another thread. The raw AVCodecContext pointer
376// is only accessed from the thread that owns the CodecContext, and the crate ensures
377// single-threaded access to codec operations.
378unsafe impl Send for CodecContext {}
379
380impl CodecContext {
381    pub(crate) fn new(avcodec_context: *mut AVCodecContext) -> Self {
382        Self {
383            inner: avcodec_context,
384        }
385    }
386
387    pub(crate) fn null() -> Self {
388        Self { inner: null_mut() }
389    }
390
391    pub(crate) fn as_mut_ptr(&self) -> *mut AVCodecContext {
392        self.inner
393    }
394
395    pub(crate) fn as_ptr(&self) -> *const AVCodecContext {
396        self.inner as *const AVCodecContext
397    }
398}
399
400impl Drop for CodecContext {
401    fn drop(&mut self) {
402        unsafe {
403            avcodec_free_context(&mut self.inner);
404        }
405    }
406}
407
408#[derive(Copy, Clone)]
409pub(crate) struct Stream {
410    pub(crate) inner: *mut AVStream,
411}
412
413// SAFETY: Stream can be sent to another thread. The raw AVStream pointer is owned
414// by the parent AVFormatContext, and the crate ensures the format context outlives
415// all Stream references.
416unsafe impl Send for Stream {}
417
418pub(crate) struct FrameBox {
419    pub(crate) frame: ffmpeg_next::Frame,
420    // stream copy or filtergraph
421    pub(crate) frame_data: FrameData,
422}
423
424// SAFETY: FrameBox can be sent to another thread. It contains an ffmpeg_next::Frame
425// (which wraps AVFrame) and FrameData, both of which are only accessed from the owning thread.
426unsafe impl Send for FrameBox {}
427
428pub fn frame_alloc() -> crate::error::Result<ffmpeg_next::Frame> {
429    unsafe {
430        let frame = ffmpeg_next::Frame::empty();
431        if frame.as_ptr().is_null() {
432            return Err(AllocFrameError::OutOfMemory.into());
433        }
434        Ok(frame)
435    }
436}
437
438pub fn null_frame() -> ffmpeg_next::Frame {
439    unsafe { ffmpeg_next::Frame::wrap(null_mut()) }
440}
441
442/// Owned array of global (stream-level) side data entries — the Rust shape of
443/// the `AVFrameSideData **side_data / int nb_side_data` pairs that fftools
444/// threads through InputFilterPriv, OutputFilterPriv and FrameData
445/// (ffmpeg_filter.c, commits e61b9d4094 / 7b18beb477). Entries are
446/// deep-cloned in and freed exactly once on drop.
447pub(crate) struct SideDataList {
448    entries: *mut *mut AVFrameSideData,
449    count: i32,
450}
451
452impl SideDataList {
453    pub(crate) fn new() -> Self {
454        Self {
455            entries: null_mut(),
456            count: 0,
457        }
458    }
459
460    /// Deep-clones one entry onto the list (flags as in
461    /// `av_frame_side_data_clone`). Returns 0 or a negative AVERROR.
462    #[cfg(ffmpeg_8_0)]
463    pub(crate) fn push_clone(&mut self, sd: *const AVFrameSideData, flags: u32) -> i32 {
464        unsafe { av_frame_side_data_clone(&mut self.entries, &mut self.count, sd, flags) }
465    }
466
467    pub(crate) fn clear(&mut self) {
468        // docs.rs bindings predate av_frame_side_data_free (FFmpeg 7.0), but
469        // there the list is provably empty — push_clone is ffmpeg_8_0-gated
470        // and that cfg never exists on docs.rs — so skipping the call is the
471        // exact semantics, not a stub.
472        #[cfg(not(docsrs))]
473        unsafe {
474            av_frame_side_data_free(&mut self.entries, &mut self.count)
475        }
476    }
477
478    #[cfg(ffmpeg_8_0)]
479    pub(crate) fn len(&self) -> i32 {
480        self.count
481    }
482
483    /// Raw entry array for FFmpeg parameter structs
484    /// (e.g. `AVBufferSrcParameters.side_data`); callees deep-copy what they
485    /// need, the list keeps ownership. Takes `&mut self` so handing out a
486    /// `*mut` view is justified by the signature, not by convention — which
487    /// also keeps the Sync claim honest (shared refs never leak mutability).
488    #[cfg(ffmpeg_8_0)]
489    pub(crate) fn as_mut_ptr(&mut self) -> *mut *mut AVFrameSideData {
490        self.entries
491    }
492
493    #[cfg(ffmpeg_8_0)]
494    pub(crate) fn iter(&self) -> impl Iterator<Item = *const AVFrameSideData> + '_ {
495        (0..self.count)
496            .map(|i| unsafe { *self.entries.offset(i as isize) as *const AVFrameSideData })
497    }
498}
499
500impl Drop for SideDataList {
501    fn drop(&mut self) {
502        self.clear();
503    }
504}
505
506// SAFETY: the list exclusively owns its deep-cloned entries. All mutation
507// happens on the thread that is building it; once frozen behind an Arc it is
508// only read, and readers clone entries out instead of mutating — so both
509// moving it across threads and sharing references are sound.
510unsafe impl Send for SideDataList {}
511unsafe impl Sync for SideDataList {}
512
513#[derive(Clone)]
514pub(crate) struct FrameData {
515    pub(crate) framerate: Option<AVRational>,
516    pub(crate) bits_per_raw_sample: i32,
517    pub(crate) input_stream_width: i32,
518    pub(crate) input_stream_height: i32,
519    /// Owned copy of the decoder's subtitle header (e.g. ASS script info),
520    /// shared across fan-out sends without reallocation. Owning the bytes
521    /// keeps the header valid after the decoder context is freed.
522    pub(crate) subtitle_header: Option<Arc<[u8]>>,
523
524    pub(crate) fg_input_index: usize,
525
526    /// Graph-level global side data snapshot, attached by the filtergraph
527    /// output for the frame that opens the encoder and consumed by
528    /// `enc_open` on FFmpeg 8+ (fftools FrameData.side_data, 7b18beb477).
529    /// Always `None` on FFmpeg 7.x, where the encoder scans frame side data
530    /// instead (n7.1 behavior) — hence dead there by design.
531    #[cfg_attr(not(ffmpeg_8_0), allow(dead_code))]
532    pub(crate) side_data: Option<Arc<SideDataList>>,
533}
534// Send + Sync are auto-derived. For side_data that rests on SideDataList's
535// hand-written unsafe impls (frozen-behind-Arc discipline); every other
536// field is plain owned data.
537
538pub(crate) struct PacketBox {
539    pub(crate) packet: ffmpeg_next::Packet,
540    pub(crate) packet_data: PacketData,
541}
542
543// SAFETY: PacketBox can be sent to another thread. It contains an ffmpeg_next::Packet
544// and PacketData, both only accessed from the owning thread.
545unsafe impl Send for PacketBox {}
546
547// optionally attached as opaque_ref to decoded AVFrames
548#[derive(Clone, Copy)]
549pub(crate) struct PacketData {
550    // demuxer-estimated dts in AV_TIME_BASE_Q,
551    // to be used when real dts is missing
552    pub(crate) dts_est: i64,
553    pub(crate) codec_type: AVMediaType,
554    pub(crate) output_stream_index: i32,
555    pub(crate) is_copy: bool,
556}
557
558pub(crate) fn out_fmt_ctx_free(out_fmt_ctx: *mut AVFormatContext, is_set_write_callback: bool) {
559    if out_fmt_ctx.is_null() {
560        return;
561    }
562    unsafe {
563        if is_set_write_callback {
564            free_output_opaque((*out_fmt_ctx).pb);
565        } else if (*(*out_fmt_ctx).oformat).flags & AVFMT_NOFILE == 0 {
566            // AVFMT_NOFILE lives on the *output format* flags, not the context flags
567            // (where the same bit 0x1 is AVFMT_FLAG_GENPTS). Reading it off the
568            // context could skip avio_closep and leak the pb when GENPTS is set.
569            let mut pb = (*out_fmt_ctx).pb;
570            if !pb.is_null() {
571                avio_closep(&mut pb);
572            }
573        }
574        avformat_free_context(out_fmt_ctx);
575    }
576}
577
578pub(crate) unsafe fn free_output_opaque(mut avio_ctx: *mut AVIOContext) {
579    if avio_ctx.is_null() {
580        return;
581    }
582    if !(*avio_ctx).buffer.is_null() {
583        av_freep(&mut (*avio_ctx).buffer as *mut _ as *mut c_void);
584    }
585    let opaque_ptr = (*avio_ctx).opaque as *mut OutputOpaque;
586    if !opaque_ptr.is_null() {
587        let _ = Box::from_raw(opaque_ptr);
588    }
589    avio_context_free(&mut avio_ctx);
590}
591
592pub(crate) fn in_fmt_ctx_free(mut in_fmt_ctx: *mut AVFormatContext, is_set_read_callback: bool) {
593    if in_fmt_ctx.is_null() {
594        return;
595    }
596    unsafe {
597        // Close the input FIRST: the demuxer's read_close may still touch
598        // s->pb (the official custom-IO example frees the AVIOContext only
599        // after avformat_close_input). With AVFMT_FLAG_CUSTOM_IO the close
600        // leaves pb alone, so capture it beforehand and free it after.
601        let avio_ctx = if is_set_read_callback {
602            (*in_fmt_ctx).pb
603        } else {
604            null_mut()
605        };
606        avformat_close_input(&mut in_fmt_ctx);
607        free_input_opaque(avio_ctx);
608    }
609}
610
611pub(crate) unsafe fn free_input_opaque(mut avio_ctx: *mut AVIOContext) {
612    if !avio_ctx.is_null() {
613        let opaque_ptr = (*avio_ctx).opaque as *mut InputOpaque;
614        if !opaque_ptr.is_null() {
615            let _ = Box::from_raw(opaque_ptr);
616        }
617        av_freep(&mut (*avio_ctx).buffer as *mut _ as *mut c_void);
618        avio_context_free(&mut avio_ctx);
619    }
620}
621
622/// RAII guard for a partially-initialized `AVFormatContext` (input or output).
623///
624/// During `open_input_file` / `open_output_file` the raw context (and, for
625/// custom-IO, its `AVIOContext` + callback `Box`) is owned by nobody until a
626/// [`Demuxer`]/[`Muxer`] takes it. Any `?`/early return in that window used to
627/// leak it. [`arm`](FmtCtxGuard::arm) it once the context is valid; on drop it
628/// frees via the same [`in_fmt_ctx_free`]/[`out_fmt_ctx_free`] paths a
629/// success-path [`crate::raw::FormatContext`] drop uses — unless
630/// [`release`](FmtCtxGuard::release) is called when ownership transfers.
631///
632/// The teardown path is selected by [`crate::raw::Mode`] (the same discriminant
633/// `FormatContext` carries), replacing the two former bool-keyed guards
634/// (`OutFmtCtxGuard`/`InFmtCtxGuard`).
635///
636/// Unlike `FormatContext`, this guard is re-armable and covers contexts that are
637/// only *partially* initialized (allocated but not yet opened, or mid custom-IO
638/// setup) — which is why it stays a separate type rather than reusing
639/// `FormatContext`'s already-opened constructors.
640pub(crate) struct FmtCtxGuard {
641    ctx: *mut AVFormatContext,
642    mode: crate::raw::Mode,
643}
644
645impl FmtCtxGuard {
646    pub(crate) fn disarmed() -> Self {
647        // `mode` is irrelevant while `ctx` is null (Drop no-ops on null).
648        Self {
649            ctx: null_mut(),
650            mode: crate::raw::Mode::Input,
651        }
652    }
653
654    /// Take ownership of a now-valid context so any early return frees it, with
655    /// the teardown path selected by `mode`.
656    pub(crate) fn arm(&mut self, ctx: *mut AVFormatContext, mode: crate::raw::Mode) {
657        self.ctx = ctx;
658        self.mode = mode;
659    }
660
661    /// Relinquish ownership (a Demuxer/Muxer/FormatContext now owns the context).
662    pub(crate) fn release(&mut self) -> *mut AVFormatContext {
663        let ctx = self.ctx;
664        self.ctx = null_mut();
665        ctx
666    }
667}
668
669impl Drop for FmtCtxGuard {
670    fn drop(&mut self) {
671        if self.ctx.is_null() {
672            return;
673        }
674        // Same dispatch as `FormatContext::Drop`.
675        match self.mode {
676            crate::raw::Mode::Input => in_fmt_ctx_free(self.ctx, false),
677            crate::raw::Mode::InputCustomIo => in_fmt_ctx_free(self.ctx, true),
678            crate::raw::Mode::Output => out_fmt_ctx_free(self.ctx, false),
679            crate::raw::Mode::OutputCustomIo => out_fmt_ctx_free(self.ctx, true),
680        }
681    }
682}
683
684#[allow(dead_code)]
685pub(crate) fn type_to_linklabel(media_type: AVMediaType, index: usize) -> Option<String> {
686    match media_type {
687        AVMediaType::AVMEDIA_TYPE_UNKNOWN => None,
688        AVMEDIA_TYPE_VIDEO => Some(format!("{index}:v")),
689        AVMEDIA_TYPE_AUDIO => Some(format!("{index}:a")),
690        AVMEDIA_TYPE_DATA => Some(format!("{index}:d")),
691        AVMEDIA_TYPE_SUBTITLE => Some(format!("{index}:s")),
692        AVMEDIA_TYPE_ATTACHMENT => Some(format!("{index}:t")),
693        AVMediaType::AVMEDIA_TYPE_NB => None,
694    }
695}
696
697#[cfg(test)]
698mod tests {
699    use super::*;
700    use crate::core::scheduler::ffmpeg_scheduler::{STATUS_ABORT, STATUS_END, STATUS_RUN};
701
702    fn state_with(status: usize) -> Arc<InterruptState> {
703        Arc::new(InterruptState::new(Arc::new(AtomicUsize::new(status))))
704    }
705
706    /// Rewinds the grace clock so the window is already expired — no sleeps.
707    fn expire_grace(state: &InterruptState) {
708        let past = unsafe { av_gettime_relative() } - OUTPUT_END_GRACE_US - 1;
709        state.end_grace_start_us.store(past, Ordering::Release);
710    }
711
712    #[test]
713    fn output_grace_cut_fires_after_the_window() {
714        let state = state_with(STATUS_END);
715        expire_grace(&state);
716        assert!(state.should_interrupt_output());
717    }
718
719    #[test]
720    fn finalize_guard_holds_the_grace_cut_open() {
721        let state = state_with(STATUS_END);
722        expire_grace(&state);
723        let guard = state.begin_output_finalize();
724        assert!(
725            !state.should_interrupt_output(),
726            "a finalizing output must not be cut by the STATUS_END grace"
727        );
728        drop(guard);
729        assert!(
730            state.should_interrupt_output(),
731            "after the last finalize guard drops, the expired grace cuts again"
732        );
733    }
734
735    #[test]
736    fn abort_cuts_through_a_finalize_window() {
737        let state = state_with(STATUS_ABORT);
738        let _guard = state.begin_output_finalize();
739        assert!(
740            state.should_interrupt_output(),
741            "abort() is the hard cancel: it must cut even a finalizing output"
742        );
743    }
744
745    #[test]
746    fn running_scheduler_never_cuts_output() {
747        let state = state_with(STATUS_RUN);
748        assert!(!state.should_interrupt_output());
749    }
750
751    #[test]
752    fn overlapping_finalize_windows_hold_until_the_last_drops() {
753        let state = state_with(STATUS_END);
754        expire_grace(&state);
755        let g1 = state.begin_output_finalize();
756        let g2 = state.begin_output_finalize();
757        drop(g1);
758        assert!(
759            !state.should_interrupt_output(),
760            "one of two overlapping finalize windows dropping must keep the hold"
761        );
762        drop(g2);
763        assert!(state.should_interrupt_output());
764    }
765}