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}