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