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};
6use ffmpeg_sys_next::{
7    av_freep, av_gettime_relative, avcodec_free_context, avformat_close_input,
8    avformat_free_context, avio_closep, avio_context_free, AVCodecContext, AVFormatContext,
9    AVIOContext, AVMediaType, AVRational, AVStream, AVFMT_NOFILE,
10};
11use std::ffi::c_void;
12use std::ptr::null_mut;
13use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
14use std::sync::Arc;
15
16/// How long output I/O may continue after STATUS_END before the interrupt
17/// callback cuts it: long enough for a healthy muxer to finish its trailer,
18/// short enough that stop() on a dead network peer returns within a second.
19const OUTPUT_END_GRACE_US: i64 = 500_000;
20
21/// Shared state behind the AVIO interrupt callbacks (fftools installs
22/// decode_interrupt_cb on inputs and outputs, ffmpeg_mux_init.c:3326,3371).
23///
24/// Inputs are interrupted as soon as the scheduler is stopping: nothing
25/// meaningful is read after that. Outputs distinguish the terminal states:
26/// STATUS_ABORT cuts I/O immediately (the caller gave up on the files), while
27/// STATUS_END grants a grace window so trailers still get written — only an
28/// output that stays blocked past the window (dead network peer) is cut.
29pub(crate) struct InterruptState {
30    scheduler_status: Arc<AtomicUsize>,
31    // Microsecond timestamp of the first output callback that observed
32    // STATUS_END; 0 = not observed yet.
33    end_grace_start_us: AtomicI64,
34}
35
36impl InterruptState {
37    pub(crate) fn new(scheduler_status: Arc<AtomicUsize>) -> Self {
38        Self {
39            scheduler_status,
40            end_grace_start_us: AtomicI64::new(0),
41        }
42    }
43
44    fn should_interrupt_input(&self) -> bool {
45        crate::core::scheduler::ffmpeg_scheduler::is_stopping(
46            self.scheduler_status.load(Ordering::Acquire),
47        )
48    }
49
50    fn should_interrupt_output(&self) -> bool {
51        let status = self.scheduler_status.load(Ordering::Acquire);
52        if status == crate::core::scheduler::ffmpeg_scheduler::STATUS_ABORT {
53            return true;
54        }
55        if status != crate::core::scheduler::ffmpeg_scheduler::STATUS_END {
56            return false;
57        }
58        let now = unsafe { av_gettime_relative() };
59        let start = match self.end_grace_start_us.compare_exchange(
60            0,
61            now,
62            Ordering::AcqRel,
63            Ordering::Acquire,
64        ) {
65            Ok(_) => now,
66            Err(previous) => previous,
67        };
68        now - start > OUTPUT_END_GRACE_US
69    }
70}
71
72/// # Safety
73/// `opaque` must point to an `InterruptState` that outlives every
74/// AVFormatContext carrying this callback (the owning FfmpegContext holds the
75/// Arc and outlives all worker threads).
76pub(crate) unsafe extern "C" fn input_interrupt_cb(opaque: *mut c_void) -> libc::c_int {
77    let state = &*(opaque as *const InterruptState);
78    state.should_interrupt_input() as libc::c_int
79}
80
81/// # Safety
82/// Same contract as [`input_interrupt_cb`].
83pub(crate) unsafe extern "C" fn output_interrupt_cb(opaque: *mut c_void) -> libc::c_int {
84    let state = &*(opaque as *const InterruptState);
85    state.should_interrupt_output() as libc::c_int
86}
87
88/// Gate for the muxer's deferred start. fftools: `SchMux.mux_started` +
89/// `PreMuxQueue` under `Scheduler.mux_ready_lock` (ffmpeg_sched.c).
90///
91/// Until the muxer thread is running, encoders park packets in a bounded
92/// pre-queue; at start the muxer drains that queue and flips `started`.
93/// Without a lock the flip races the send: an encoder that read
94/// `started == false` can enqueue into the pre-queue AFTER the drain
95/// finished, and that packet is never delivered. Pre-queue sends therefore
96/// happen under the same lock as the drain-and-flip.
97pub(crate) struct MuxStartGate {
98    started: std::sync::atomic::AtomicBool,
99    lock: std::sync::Mutex<()>,
100}
101
102/// What a gated pre-queue send resolved to.
103pub(crate) enum PreSendOutcome {
104    /// Parked in the pre-queue; the drain will deliver it.
105    Sent,
106    /// The gate opened first: send to the live queue instead.
107    Started(PacketBox),
108    /// Pre-queue full: back off and retry (the lock must not be held across
109    /// a blocking send, or the drain could never run).
110    Full(PacketBox),
111    /// Pre-queue receiver is gone (muxer never started).
112    Disconnected(PacketBox),
113}
114
115impl MuxStartGate {
116    pub(crate) fn new() -> Self {
117        Self {
118            started: std::sync::atomic::AtomicBool::new(false),
119            lock: std::sync::Mutex::new(()),
120        }
121    }
122
123    pub(crate) fn is_started(&self) -> bool {
124        self.started.load(Ordering::Acquire)
125    }
126
127    /// Runs the pre-queue drain and opens the gate as one atomic step.
128    pub(crate) fn start_with(&self, drain: impl FnOnce()) {
129        let _guard = self.lock.lock().unwrap();
130        drain();
131        self.started.store(true, Ordering::Release);
132    }
133
134    /// Attempts a pre-queue send while the gate is verifiably closed.
135    pub(crate) fn send_pre(
136        &self,
137        pre_sender: &crossbeam_channel::Sender<PacketBox>,
138        packet_box: PacketBox,
139    ) -> PreSendOutcome {
140        let _guard = self.lock.lock().unwrap();
141        if self.started.load(Ordering::Acquire) {
142            return PreSendOutcome::Started(packet_box);
143        }
144        match pre_sender.try_send(packet_box) {
145            Ok(()) => PreSendOutcome::Sent,
146            Err(crossbeam_channel::TrySendError::Full(pb)) => PreSendOutcome::Full(pb),
147            Err(crossbeam_channel::TrySendError::Disconnected(pb)) => {
148                PreSendOutcome::Disconnected(pb)
149            }
150        }
151    }
152}
153
154use ffmpeg_context::{InputOpaque, OutputOpaque};
155
156
157/// The **ffmpeg_context** module is responsible for assembling FFmpeg’s configuration:
158/// inputs, outputs, codecs, filters, and other parameters needed to construct a
159/// complete media processing pipeline.
160///
161/// # Example
162/// ```rust,ignore
163///
164/// // Build an FFmpeg context with one input, some filter settings, and one output
165/// let context = FfmpegContext::builder()
166///     .input("test.mp4")
167///     .filter_desc("hue=s=0")
168///     .output("output.mp4")
169///     .build()
170///     .unwrap();
171/// // The context now holds all info needed for an FFmpeg job.
172/// ```
173pub mod ffmpeg_context;
174
175/// The **ffmpeg_context_builder** module defines the builder pattern for creating
176/// [`FfmpegContext`](ffmpeg_context::FfmpegContext) objects.
177///
178/// It exposes the [`FfmpegContextBuilder`](ffmpeg_context_builder::FfmpegContextBuilder) struct, which allows you to:
179/// - Configure multiple [`Input`](input::Input) and
180///   [`Output`](output::Output) streams.
181/// - Attach filter descriptions via [`FilterComplex`](crate::core::context::filter_complex::FilterComplex)
182///   or inline strings (e.g., `"scale=1280:720"`, `"hue=s=0"`).
183/// - Produce a finished `FfmpegContext` that can then be executed by
184///   [`FfmpegScheduler`](crate::core::scheduler::ffmpeg_scheduler::FfmpegScheduler).
185///
186/// # Examples
187///
188/// ```rust,ignore
189/// // 1. Create a builder (usually via FfmpegContext::builder())
190/// let builder = FfmpegContext::builder();
191///
192/// // 2. Add inputs, outputs, and filters
193/// let ffmpeg_context = builder
194///     .input("input.mp4")
195///     .filter_desc("hue=s=0")
196///     .output("output.mp4")
197///     .build()
198///     .expect("Failed to build FfmpegContext");
199///
200/// // 3. Use `ffmpeg_context` with FfmpegScheduler (e.g., `.start()` and `.wait()`).
201/// ```
202pub mod ffmpeg_context_builder;
203
204/// The **input** module defines the [`Input`](crate::core::context::input::Input) struct,
205/// representing an FFmpeg input source. An input can be:
206/// - A file path or URL (e.g., `"video.mp4"`, `rtmp://example.com/live/stream`).
207/// - A **custom data source** via a `read_callback` (and optionally `seek_callback`) for
208///   advanced scenarios like in-memory buffers or network protocols.
209///
210/// You can also specify **frame pipelines** to apply custom [`FrameFilter`](crate::core::filter::frame_filter::FrameFilter)
211/// transformations **after decoding** but **before** the frames move on to the rest of the pipeline.
212///
213/// # Example
214///
215/// ```rust,ignore
216/// use ez_ffmpeg::core::context::input::Input;
217///
218/// // Basic file or network URL:
219/// let file_input: Input = "example.mp4".into();
220///
221/// // Or a custom read callback:
222/// let custom_input = Input::new_by_read_callback(|buf| {
223///     // Fill `buf` with data from your source
224///     // Return the number of bytes read, or negative for errors
225///     0
226/// });
227/// ```
228pub mod input;
229
230/// The **output** module defines the [`Output`](crate::core::context::output::Output) struct,
231/// representing an FFmpeg output destination. An output may be:
232/// - A file path or URL (e.g., `"output.mp4"`, `rtmp://...`).
233/// - A **custom write callback** that processes encoded data (e.g., storing it
234///   in-memory or sending it over a custom network protocol).
235///
236/// You can specify additional details such as:
237/// - **Container format** (e.g., `"mp4"`, `"flv"`, `"mkv"`).
238/// - **Video/Audio/Subtitle codecs** (e.g., `"h264"`, `"aac"`, `"mov_text"`).
239/// - **Frame pipelines** to apply [`FrameFilter`](crate::core::filter::frame_filter::FrameFilter)
240///   transformations **before encoding**.
241///
242/// # Example
243///
244/// ```rust,ignore
245/// use ez_ffmpeg::core::context::output::Output;
246///
247/// // Basic file/URL output:
248/// let file_output: Output = "output.mp4".into();
249///
250/// // Or a custom write callback:
251/// let custom_output = Output::new_by_write_callback(|encoded_data| {
252///     // Write `encoded_data` somewhere
253///     encoded_data.len() as i32
254/// }).set_format("mp4");
255/// ```
256pub mod output;
257
258/// The **filter_complex** module defines the [`FilterComplex`](crate::core::context::filter_complex::FilterComplex)
259/// struct, which encapsulates one or more FFmpeg filter descriptions (e.g., `"scale=1280:720"`,
260/// `"hue=s=0"`, etc.). You can use `FilterComplex` to construct more advanced or multi-step
261/// filter graphs than simple inline strings allow.
262///
263/// `FilterComplex` can also associate a particular hardware device (e.g., for GPU-based
264/// filtering) via `hw_device`.
265///
266/// # Example
267///
268/// ```rust,ignore
269/// use ez_ffmpeg::core::context::filter_complex::FilterComplex;
270///
271/// // Build a FilterComplex from a string:
272/// let my_filters = FilterComplex::from("scale=1280:720");
273///
274/// // Optionally specify a hardware device (e.g., "cuda"):
275/// // my_filters.set_hw_device("cuda");
276/// ```
277pub mod filter_complex;
278
279
280pub(super) mod decoder_stream;
281pub(super) mod demuxer;
282pub(super) mod encoder_stream;
283pub(super) mod filter_graph;
284pub(super) mod input_filter;
285pub(super) mod muxer;
286pub(super) mod obj_pool;
287pub(super) mod output_filter;
288
289/// The **null_output** module provides a custom null output implementation for FFmpeg
290/// that discards all data while supporting seeking.
291///
292/// It exposes the [`create_null_output`](null_output::create_null_output) function, which returns an
293/// [`Output`](crate::Output) object configured to:
294/// - Discard all written data, behaving like `/dev/null`.
295/// - Maintain a seekable position state using atomic operations for thread-safe, high-performance access.
296/// - Support scenarios such as testing or processing streaming inputs (e.g., RTMP) where no output file is needed.
297///
298/// # Usage Scenario
299/// This module is useful when processing FFmpeg input streams without generating an output file, such as
300/// when handling RTMP streams that require a seekable output format like MP4, even if the output is discarded.
301///
302/// # Examples
303///
304/// ```rust,ignore
305/// use ez_ffmpeg::Output;
306/// let output: Output = create_null_output();
307/// // Pass `output` to an FFmpeg context for processing
308/// ```
309///
310/// # Performance
311/// - Utilizes `AtomicU64` with `Relaxed` ordering for lock-free position tracking, ensuring efficient concurrent access.
312/// - Write and seek operations are optimized to minimize overhead by avoiding locks.
313///
314/// # Notes
315/// - The default output format is "mp4", but this can be modified using `set_format` as needed.
316/// - Write operations assume individual buffers do not exceed `i32::MAX` bytes, which aligns with typical FFmpeg usage.
317pub mod null_output;
318
319pub(crate) struct CodecContext {
320    inner: *mut AVCodecContext,
321}
322
323// SAFETY: CodecContext can be sent to another thread. The raw AVCodecContext pointer
324// is only accessed from the thread that owns the CodecContext, and the crate ensures
325// single-threaded access to codec operations.
326unsafe impl Send for CodecContext {}
327
328impl CodecContext {
329    pub(crate) fn new(avcodec_context: *mut AVCodecContext) -> Self {
330        Self {
331            inner: avcodec_context,
332        }
333    }
334
335    pub(crate) fn replace(&mut self, avcodec_context: *mut AVCodecContext) -> *mut AVCodecContext {
336        let mut tmp = self.inner;
337        if !tmp.is_null() {
338            unsafe {
339                avcodec_free_context(&mut tmp);
340            }
341        }
342        self.inner = avcodec_context;
343        tmp
344    }
345
346    pub(crate) fn null() -> Self {
347        Self { inner: null_mut() }
348    }
349
350    pub(crate) fn as_mut_ptr(&self) -> *mut AVCodecContext {
351        self.inner
352    }
353
354    pub(crate) fn as_ptr(&self) -> *const AVCodecContext {
355        self.inner as *const AVCodecContext
356    }
357}
358
359impl Drop for CodecContext {
360    fn drop(&mut self) {
361        unsafe {
362            avcodec_free_context(&mut self.inner);
363        }
364    }
365}
366
367#[derive(Copy, Clone)]
368pub(crate) struct Stream {
369    pub(crate) inner: *mut AVStream,
370}
371
372// SAFETY: Stream can be sent to another thread. The raw AVStream pointer is owned
373// by the parent AVFormatContext, and the crate ensures the format context outlives
374// all Stream references.
375unsafe impl Send for Stream {}
376
377pub(crate) struct FrameBox {
378    pub(crate) frame: ffmpeg_next::Frame,
379    // stream copy or filtergraph
380    pub(crate) frame_data: FrameData,
381}
382
383// SAFETY: FrameBox can be sent to another thread. It contains an ffmpeg_next::Frame
384// (which wraps AVFrame) and FrameData, both of which are only accessed from the owning thread.
385unsafe impl Send for FrameBox {}
386
387pub fn frame_alloc() -> crate::error::Result<ffmpeg_next::Frame> {
388    unsafe {
389        let frame = ffmpeg_next::Frame::empty();
390        if frame.as_ptr().is_null() {
391            return Err(AllocFrameError::OutOfMemory.into());
392        }
393        Ok(frame)
394    }
395}
396
397pub fn null_frame() -> ffmpeg_next::Frame {
398    unsafe { ffmpeg_next::Frame::wrap(null_mut()) }
399}
400
401#[derive(Clone)]
402pub(crate) struct FrameData {
403    pub(crate) framerate: Option<AVRational>,
404    pub(crate) bits_per_raw_sample: i32,
405    pub(crate) input_stream_width: i32,
406    pub(crate) input_stream_height: i32,
407    /// Owned copy of the decoder's subtitle header (e.g. ASS script info),
408    /// shared across fan-out sends without reallocation. Owning the bytes
409    /// keeps the header valid after the decoder context is freed.
410    pub(crate) subtitle_header: Option<Arc<[u8]>>,
411
412    pub(crate) fg_input_index: usize,
413}
414// Send + Sync are auto-derived: every field is owned data.
415
416pub(crate) struct PacketBox {
417    pub(crate) packet: ffmpeg_next::Packet,
418    pub(crate) packet_data: PacketData,
419}
420
421// SAFETY: PacketBox can be sent to another thread. It contains an ffmpeg_next::Packet
422// and PacketData, both only accessed from the owning thread.
423unsafe impl Send for PacketBox {}
424
425// optionally attached as opaque_ref to decoded AVFrames
426#[derive(Clone)]
427pub(crate) struct PacketData {
428    // demuxer-estimated dts in AV_TIME_BASE_Q,
429    // to be used when real dts is missing
430    pub(crate) dts_est: i64,
431    pub(crate) codec_type: AVMediaType,
432    pub(crate) output_stream_index: i32,
433    pub(crate) is_copy: bool,
434}
435
436// Send + Sync are auto-derived: every field is plain owned data. The muxer
437// reads codec parameters from its own output streams instead of carrying a
438// cross-thread pointer here.
439
440pub(crate) struct AVFormatContextBox {
441    pub(crate) fmt_ctx: *mut AVFormatContext,
442    pub(crate) is_input: bool,
443    pub(crate) is_set_callback: bool,
444}
445// SAFETY: AVFormatContextBox can be sent to another thread. The fmt_ctx pointer is only
446// accessed from the thread that owns the box, and the crate ensures proper cleanup.
447unsafe impl Send for AVFormatContextBox {}
448
449impl AVFormatContextBox {
450    pub(crate) fn new(
451        fmt_ctx: *mut AVFormatContext,
452        is_input: bool,
453        is_set_callback: bool,
454    ) -> Self {
455        Self {
456            fmt_ctx,
457            is_input,
458            is_set_callback,
459        }
460    }
461}
462
463impl Drop for AVFormatContextBox {
464    fn drop(&mut self) {
465        if self.fmt_ctx.is_null() {
466            return;
467        }
468        if self.is_input {
469            in_fmt_ctx_free(self.fmt_ctx, self.is_set_callback)
470        } else {
471            out_fmt_ctx_free(self.fmt_ctx, self.is_set_callback)
472        }
473    }
474}
475
476pub(crate) fn out_fmt_ctx_free(out_fmt_ctx: *mut AVFormatContext, is_set_write_callback: bool) {
477    if out_fmt_ctx.is_null() {
478        return;
479    }
480    unsafe {
481        if is_set_write_callback {
482            free_output_opaque((*out_fmt_ctx).pb);
483        } else if (*out_fmt_ctx).flags & AVFMT_NOFILE == 0 {
484            let mut pb = (*out_fmt_ctx).pb;
485            if !pb.is_null() {
486                avio_closep(&mut pb);
487            }
488        }
489        avformat_free_context(out_fmt_ctx);
490    }
491}
492
493pub(crate) unsafe fn free_output_opaque(mut avio_ctx: *mut AVIOContext) {
494    if avio_ctx.is_null() {
495        return;
496    }
497    if !(*avio_ctx).buffer.is_null() {
498        av_freep(&mut (*avio_ctx).buffer as *mut _ as *mut c_void);
499    }
500    let opaque_ptr = (*avio_ctx).opaque as *mut OutputOpaque;
501    if !opaque_ptr.is_null() {
502        let _ = Box::from_raw(opaque_ptr);
503    }
504    avio_context_free(&mut avio_ctx);
505}
506
507pub(crate) fn in_fmt_ctx_free(mut in_fmt_ctx: *mut AVFormatContext, is_set_read_callback: bool) {
508    if in_fmt_ctx.is_null() {
509        return;
510    }
511    unsafe {
512        // Close the input FIRST: the demuxer's read_close may still touch
513        // s->pb (the official custom-IO example frees the AVIOContext only
514        // after avformat_close_input). With AVFMT_FLAG_CUSTOM_IO the close
515        // leaves pb alone, so capture it beforehand and free it after.
516        let avio_ctx = if is_set_read_callback {
517            (*in_fmt_ctx).pb
518        } else {
519            null_mut()
520        };
521        avformat_close_input(&mut in_fmt_ctx);
522        free_input_opaque(avio_ctx);
523    }
524}
525
526pub(crate) unsafe fn free_input_opaque(mut avio_ctx: *mut AVIOContext) {
527    if !avio_ctx.is_null() {
528        let opaque_ptr = (*avio_ctx).opaque as *mut InputOpaque;
529        if !opaque_ptr.is_null() {
530            let _ = Box::from_raw(opaque_ptr);
531        }
532        av_freep(&mut (*avio_ctx).buffer as *mut _ as *mut c_void);
533        avio_context_free(&mut avio_ctx);
534    }
535}
536
537#[allow(dead_code)]
538pub(crate) fn type_to_linklabel(media_type: AVMediaType, index: usize) -> Option<String> {
539    match media_type {
540        AVMediaType::AVMEDIA_TYPE_UNKNOWN => None,
541        AVMEDIA_TYPE_VIDEO => Some(format!("{index}:v")),
542        AVMEDIA_TYPE_AUDIO => Some(format!("{index}:a")),
543        AVMEDIA_TYPE_DATA => Some(format!("{index}:d")),
544        AVMEDIA_TYPE_SUBTITLE => Some(format!("{index}:s")),
545        AVMEDIA_TYPE_ATTACHMENT => Some(format!("{index}:t")),
546        AVMediaType::AVMEDIA_TYPE_NB => None,
547    }
548}