Skip to main content

ez_ffmpeg/core/
mod.rs

1//! The **core** module provides the foundational building blocks for configuring and running FFmpeg
2//! pipelines. It encompasses:
3//!
4//! - **Input & Output Handling** (in [`context`]): Structures and logic (`Input`, `Output`) for
5//!   specifying where media data originates and where it should be written.
6//! - **Filter Descriptions**: Define filter graphs with `FilterComplex` or attach custom [`FrameFilter`](filter::frame_filter::FrameFilter)
7//!   implementations at the input/output stage.
8//! - **Stream and Device Queries** (in [`stream_info`] and [`device`]): Utilities for retrieving
9//!   information about media streams and available input devices.
10//! - **Hardware Acceleration** (in [`hwaccel`]): Enumerate/configure GPU-accelerated codecs (CUDA, VAAPI, etc.).
11//! - **Codec Discovery** (in [`codec`]): List encoders/decoders supported by FFmpeg.
12//! - **Custom Filters** (in [`filter`]): Implement user-defined [`FrameFilter`](filter::frame_filter::FrameFilter) logic for frames.
13//! - **Lifecycle Orchestration** (in [`scheduler`]): [`FfmpegScheduler`](scheduler::ffmpeg_scheduler::FfmpegScheduler) that runs the configured pipeline
14//!   (synchronously or asynchronously if the `async` feature is enabled).
15//!
16//! # Submodules
17//!
18//! - [`context`]: Houses [`FfmpegContext`](context::ffmpeg_context::FfmpegContext)—the central struct for assembling inputs, outputs, and filters.
19//! - [`scheduler`]: Defines [`FfmpegScheduler`](scheduler::ffmpeg_scheduler::FfmpegScheduler), managing the execution of an `FfmpegContext` pipeline.
20//! - [`container_info`]: Utilities to extract information about the container, such as duration and format details.
21//! - [`stream_info`]: Inspect media streams (e.g., find video/audio streams in a file).
22//! - [`device`]: Query audio/video input devices (cameras, microphones, etc.) on various platforms.
23//! - [`hwaccel`]: Helpers for hardware-accelerated encoding/decoding setup.
24//! - [`codec`]: Tools to discover which encoders/decoders your FFmpeg build supports.
25//! - [`filter`]: Query FFmpeg's built-in filters and infrastructure for building custom frame-processing filters.
26//!
27//! # Example Workflow
28//!
29//! 1. **Build a context** using [`FfmpegContext::builder()`](crate::core::context::ffmpeg_context::FfmpegContext::builder)
30//!    specifying your input, any filters, and your output.
31//! 2. **Create a scheduler** with [`FfmpegScheduler::new`](crate::core::scheduler::ffmpeg_scheduler::FfmpegScheduler::new),
32//!    then call `.start()` to begin processing.
33//! 3. **Wait** (or `.await` if `async` feature is enabled) for the job to complete. Use the returned
34//!    `Result` to detect success or failure.
35//!
36//! # Example
37//! ```rust,ignore
38//!
39//! fn main() -> Result<(), Box<dyn std::error::Error>> {
40//!     // 1. Build an FfmpegContext with an input, a simple filter, and an output
41//!     let context = FfmpegContext::builder()
42//!         .input("test.mp4")
43//!         .filter_desc("hue=s=0") // Example: desaturate video
44//!         .output("output.mp4")
45//!         .build()?;
46//!
47//!     // 2. Create a scheduler and start the job
48//!     let scheduler = FfmpegScheduler::new(context).start()?;
49//!
50//!     // 3. Block until it's finished
51//!     scheduler.wait()?;
52//!     Ok(())
53//! }
54//! ```
55
56/// The **context** module provides tools for assembling an entire FFmpeg pipeline,
57/// culminating in the [`FfmpegContext`](context::ffmpeg_context::FfmpegContext). This includes:
58///
59/// - **Inputs**: [`Input`](context::input::Input) objects representing files, URLs, or custom I/O callbacks.
60/// - **Outputs**: [`Output`](context::output::Output) objects representing target files, streams, or custom sinks.
61/// - **Filter Descriptions**: Simple inline filters via `filter_desc` or more complex
62///   [`FilterComplex`](context::filter_complex::FilterComplex) graphs.
63/// - **Builders**: e.g., [`FfmpegContextBuilder`](context::ffmpeg_context_builder::FfmpegContextBuilder) for constructing a complete context
64///   with multiple inputs, outputs, and filter settings.
65///
66/// Once you’ve built an [`FfmpegContext`](context::ffmpeg_context::FfmpegContext), you can execute it via the [`FfmpegScheduler`](scheduler::ffmpeg_scheduler::FfmpegScheduler).
67///
68/// # Example
69///
70/// ```rust,ignore
71/// // Build an FFmpeg context with one input, some filter settings, and one output.
72/// let context = FfmpegContext::builder()
73///     .input("test.mp4")
74///     .filter_desc("hue=s=0")
75///     .output("output.mp4")
76///     .build()
77///     .unwrap();
78/// // The context now holds all info needed for an FFmpeg job.
79/// ```
80pub mod context;
81
82/// Push raw video frames from Rust code into a full FFmpeg pipeline
83/// ([`VideoWriter`](writer::VideoWriter)).
84pub mod writer;
85
86/// Display matrix helpers shared by stream probing and filter graph setup.
87pub(crate) mod display;
88
89/// The **scheduler** module orchestrates the execution of a configured [`FfmpegContext`](context::ffmpeg_context::FfmpegContext).
90/// It provides the [`FfmpegScheduler`](scheduler::ffmpeg_scheduler::FfmpegScheduler) struct, which:
91///
92/// - **Starts** the FFmpeg pipeline via [`FfmpegScheduler::start()`](scheduler::ffmpeg_scheduler::FfmpegScheduler<crate::core::scheduler::ffmpeg_scheduler::Initialization>::start()).
93/// - **Manages** thread or subprocess creation, ensuring all streams and filters run.
94/// - **Waits** for completion (blocking or asynchronous, depending on whether the `async` feature is enabled).
95/// - **Returns** the final result, indicating success or failure.
96///
97/// # Synchronous Example
98///
99/// ```rust,ignore
100/// let context = FfmpegContext::builder()
101///     .input("test.mp4")
102///     .filter_desc("hue=s=0")
103///     .output("output.mp4")
104///     .build()
105///     .unwrap();
106///
107/// let result = FfmpegScheduler::new(context)
108///     .start()
109///     .unwrap()
110///     .wait();
111///
112/// assert!(result.is_ok(), "FFmpeg job failed unexpectedly");
113/// ```
114///
115/// # Asynchronous Example (requires `async` feature)
116///
117/// ```rust,ignore
118/// #[tokio::main]
119/// async fn main() {
120///     let context = FfmpegContext::builder()
121///         .input("test.mp4")
122///         .output("output.mp4")
123///         .build()
124///         .unwrap();
125///
126///     let mut scheduler = FfmpegScheduler::new(context)
127///         .start()
128///         .expect("Failed to start FFmpeg job");
129///
130///     // Asynchronous wait
131///     scheduler.await.expect("FFmpeg job failed unexpectedly");
132/// }
133/// ```
134pub mod scheduler;
135
136/// The **container_info** module provides utilities for retrieving metadata related to the media container,
137/// such as duration, format, and other general properties of the media file.
138///
139/// This module helps to query the overall properties of a media container file (e.g., `.mp4`, `.avi`, `.mkv`)
140/// without diving into individual streams (audio, video, etc.). It is useful when you need information
141/// about the file as a whole, such as total duration, format type, and container-specific properties.
142///
143/// # Examples
144///
145/// ```rust,ignore
146/// // Retrieve the duration in microseconds for the media file "test.mp4"
147/// let duration = get_duration_us("test.mp4").unwrap();
148/// println!("Duration: {} us", duration);
149///
150/// // Retrieve the format name for "test.mp4"
151/// let format = get_format("test.mp4").unwrap();
152/// println!("Format: {}", format);
153///
154/// // Retrieve the metadata for "test.mp4"
155/// let metadata = get_metadata("test.mp4").unwrap();
156/// for (key, value) in metadata {
157///     println!("{}: {}", key, value);
158/// }
159/// ```
160///
161/// These helper functions return the container-level metadata, and they handle any errors that may arise
162/// (e.g., if the file can't be opened or if there is an issue reading the data).
163pub mod container_info;
164
165/// The **stream_info** module provides utilities to retrieve detailed information
166/// about media streams (video, audio, and more) from an input source (e.g., a local file
167/// path, an RTMP URL, etc.). It queries FFmpeg for metadata regarding stream types, codec
168/// parameters, duration, and other relevant details.
169///
170/// # Examples
171///
172/// ```rust,ignore
173/// // Retrieve information about the first video stream in "test.mp4"
174/// let maybe_video_info = find_video_stream_info("test.mp4").unwrap();
175/// if let Some(video_info) = maybe_video_info {
176///     println!("Found video stream: {:?}", video_info);
177/// } else {
178///     println!("No video stream found.");
179/// }
180///
181/// // Retrieve information about the first audio stream in "test.mp4"
182/// let maybe_audio_info = find_audio_stream_info("test.mp4").unwrap();
183/// if let Some(audio_info) = maybe_audio_info {
184///     println!("Found audio stream: {:?}", audio_info);
185/// } else {
186///     println!("No audio stream found.");
187/// }
188///
189/// // Retrieve information about all streams (video, audio, etc.) in "test.mp4"
190/// let all_infos = find_all_stream_infos("test.mp4").unwrap();
191/// println!("Total streams found: {}", all_infos.len());
192/// for info in all_infos {
193///     println!("{:?}", info);
194/// }
195/// ```
196///
197/// These helper functions return `Result<Option<StreamInfo>, Error>` or `Result<Vec<StreamInfo>, Error>`
198/// depending on the call, allowing you to differentiate between "no stream found" (returns `Ok(None)`)
199/// and encountering an actual error (returns `Err(...)`).
200pub mod stream_info;
201
202/// The **packet_scanner** module provides a lightweight packet-level scanner for media files.
203///
204/// Unlike the full demuxing pipeline, `PacketScanner` iterates over raw demuxed packets
205/// without any decoding. This is useful for inspecting packet metadata such as timestamps,
206/// keyframe flags, sizes, and stream indices.
207///
208/// # Examples
209///
210/// ```rust,ignore
211/// use ez_ffmpeg::packet_scanner::PacketScanner;
212///
213/// let mut scanner = PacketScanner::open("test.mp4")?;
214/// for packet in scanner.packets() {
215///     let packet = packet?;
216///     println!(
217///         "stream={} pts={:?} size={} keyframe={}",
218///         packet.stream_index(),
219///         packet.pts(),
220///         packet.size(),
221///         packet.is_keyframe(),
222///     );
223/// }
224/// ```
225pub mod packet_scanner;
226
227/// The **packet_sink** module delivers **encoded packets** to user callbacks
228/// instead of muxing them into container bytes — the packet-domain
229/// counterpart of the byte-domain `write_callback` output.
230///
231/// Build a [`PacketSink`](packet_sink::PacketSink) (or a
232/// [`PacketSink::channel`](packet_sink::PacketSink::channel) adapter) and hand
233/// it to [`Output::new_by_packet_sink`](crate::Output::new_by_packet_sink).
234/// The v1 strict tier is WebCodecs-aligned: avcC H.264 access units
235/// (libx264, h264_nvenc, h264_videotoolbox with `bf=0`, libopenh264) and raw AAC frames with their AudioSpecificConfig. See the
236/// module documentation for the callback order and the blocking-backpressure
237/// contract.
238pub mod packet_sink;
239
240/// The **device** module provides cross-platform methods to query available audio and video
241/// input devices on the system. Depending on the target operating system, it internally
242/// delegates to different platform APIs or FFmpeg’s device capabilities:
243///
244/// - **macOS**: Leverages AVFoundation for enumerating devices such as cameras ("vide")
245///   and microphones ("soun").
246/// - **Other OSes**: Uses FFmpeg’s `avdevice` to list input devices for video and audio.
247///
248/// These functions can be used to programmatically discover devices before choosing one
249/// for capture or recording in an FFmpeg-based pipeline.
250///
251/// # Examples
252///
253/// ```rust,ignore
254/// // Query video input devices (e.g., cameras)
255/// let video_devices = get_input_video_devices().unwrap();
256/// for device in &video_devices {
257///     println!("Available video device: {}", device);
258/// }
259///
260/// // Query audio input devices (e.g., microphones)
261/// let audio_devices = get_input_audio_devices().unwrap();
262/// for device in &audio_devices {
263///     println!("Available audio device: {}", device);
264/// }
265/// ```
266///
267/// # Notes
268///
269/// - If the query process fails (e.g., missing permissions or no devices available),
270///   the functions return an appropriate error from `crate::error`.
271/// - On macOS, the `AVFoundation` framework is used directly. On other platforms, FFmpeg’s
272///   `avdevice` functionality is used. Implementation details differ, but the returned
273///   results have a uniform format: a list of human-readable device names.
274/// - For more advanced device details (e.g., supported formats or resolutions), you may need
275///   to perform additional FFmpeg queries or platform-specific calls.
276pub mod device;
277/// The **hwaccel** module provides functionality for working with hardware-accelerated
278/// codecs in FFmpeg. It allows you to detect and configure various hardware devices
279/// (like NVENC, VAAPI, DXVA2, or VideoToolbox) so that FFmpeg can offload encoding or
280/// decoding tasks to GPU or specialized hardware.
281///
282/// # Public API
283///
284/// - [`get_hwaccels()`](hwaccel::get_hwaccels): Enumerates the hardware acceleration backends available on the
285///   current system, returning a list of [`HWAccelInfo`](hwaccel::HWAccelInfo) items. Each item contains a
286///   readable name (e.g., `"cuda"`, `"vaapi"`) and the corresponding `AVHWDeviceType`.
287///
288/// # Example
289///
290/// ```rust,ignore
291/// // Query hardware acceleration backends
292/// let hwaccels = get_hwaccels();
293/// for accel in hwaccels {
294///     println!("Found HW Accel: {} (type: {:?})", accel.name, accel.hw_device_type);
295/// }
296/// ```
297///
298/// # Notes
299///
300/// - While only [`get_hwaccels()`](hwaccel::get_hwaccels) is directly exposed, internally the module contains
301///   various helpers to initialize and manage hardware devices (e.g., `hw_device_init_from_string`).
302///   These are used behind the scenes or in more advanced scenarios where explicit control
303///   over device creation is required.
304/// - Hardware acceleration support depends on both FFmpeg’s compilation configuration
305///   and the underlying system drivers/frameworks. Not all listed accelerations may be
306///   fully functional on every platform.
307///
308/// # Device lifetime and caching
309///
310/// Hardware device contexts are expensive to create — initialization loads
311/// vendor drivers and libraries, with CUDA context creation the classic
312/// costly case — so ez-ffmpeg manages them with a process-global cache:
313///
314/// - A device context is created on first use of a given configuration
315///   (accel type plus device spec) and cached; later jobs requesting the
316///   same configuration reuse the cached context instead of creating a
317///   new one.
318/// - The cache is bounded at 32 entries; adding a further distinct
319///   configuration evicts the least-recently-requested one. Eviction
320///   releases only the cache's own handle: the underlying FFmpeg device
321///   context is reference-counted and stays alive until no codec, filter
322///   graph, or frame still uses it, so eviction never affects a running
323///   job.
324/// - All cache-owned handles are released at process exit.
325///
326/// There is deliberately no idle-timeout release: recreation is the
327/// expensive direction, so a cached context is kept until the 32-entry
328/// bound evicts it or the process exits (the ffmpeg CLI likewise keeps
329/// its devices until final cleanup).
330pub mod hwaccel;
331
332/// The **codec** module provides helpers for enumerating and querying FFmpeg’s
333/// available audio/video **encoders** and **decoders**. This can be useful for
334/// discovering which codecs are supported in your current FFmpeg build, along
335/// with their core attributes.
336///
337/// # Public API
338///
339/// - [`get_encoders()`](codec::get_encoders): Returns a list of [`CodecInfo`](codec::CodecInfo) representing all
340///   encoders (e.g., H.264, AAC) recognized by FFmpeg.
341/// - [`get_decoders()`](codec::get_decoders): Returns a list of [`CodecInfo`](codec::CodecInfo) representing all
342///   decoders (e.g., H.264, AAC) recognized by FFmpeg.
343///
344/// # Example
345///
346/// ```rust,ignore
347/// // List all available encoders
348/// let encoders = get_encoders();
349/// for enc in &encoders {
350///     println!("Encoder: {} - {}", enc.codec_name, enc.codec_long_name);
351/// }
352///
353/// // List all available decoders
354/// let decoders = get_decoders();
355/// for dec in &decoders {
356///     println!("Decoder: {} - {}", dec.codec_name, dec.codec_long_name);
357/// }
358/// ```
359///
360/// # Data Structures
361///
362/// - [`CodecInfo`](codec::CodecInfo): Contains user-friendly fields such as:
363///   - `codec_name` / `codec_long_name`
364///   - `desc_name`: The descriptor name from FFmpeg.
365///   - `media_type` (audio/video/subtitle, etc.)
366///   - `codec_id` (internal FFmpeg ID)
367///   - `codec_capabilities` (bitmask indicating codec features)
368///
369/// # Notes
370///
371/// - The underlying `Codec` struct is for internal usage only (`pub(crate)`,
372///   not part of the documented API), bridging to the raw FFmpeg APIs. In most
373///   cases, you only need the higher-level [`CodecInfo`](codec::CodecInfo)
374///   data from the public functions above.
375/// - The available encoders/decoders can vary depending on your FFmpeg build
376///   and any external libraries installed on the system.
377pub mod codec;
378
379/// The **capabilities** module provides lightweight probes for what the
380/// linked FFmpeg build contains — muxers, codecs, filters, and I/O
381/// protocols. Use these before configuring outputs that depend on optional
382/// components (e.g. `whip`, `srt`, `libx264`) to fail fast with an
383/// actionable error instead of a mid-pipeline failure.
384///
385/// # Public API
386///
387/// - [`is_muxer_available()`](capabilities::is_muxer_available): Checks for a
388///   muxer by short name (e.g. `"matroska"`, `"mpegts"`, `"whip"`).
389/// - [`is_output_protocol_available()`](capabilities::is_output_protocol_available):
390///   Checks for an I/O protocol that supports writing (e.g. `"file"`, `"srt"`).
391/// - [`is_input_protocol_available()`](capabilities::is_input_protocol_available):
392///   Checks for an I/O protocol that supports reading (e.g. `"file"`, `"https"`).
393/// - [`is_encoder_available()`](capabilities::is_encoder_available): Exact-name
394///   encoder probe (registered ≠ runtime-ready for hardware wrappers).
395/// - [`is_decoder_available()`](capabilities::is_decoder_available): Exact-name
396///   decoder probe.
397/// - [`is_filter_available()`](capabilities::is_filter_available): Filter probe
398///   (same answer as [`hwaccel::is_filter_available`]).
399/// - [`is_filter_option_available()`](capabilities::is_filter_option_available):
400///   Filter-option probe (name + option, including child classes such as
401///   SwsContext `intent`). Compiled-in ≠ the option performs a color transform.
402///
403/// # Example
404///
405/// ```rust,ignore
406/// if !ez_ffmpeg::capabilities::is_muxer_available("whip") {
407///     eprintln!("this FFmpeg build lacks the whip muxer");
408/// }
409/// ```
410///
411/// # Notes
412///
413/// - A `true` result only means the component is registered in the linked
414///   FFmpeg build; device/driver readiness, TLS backends, endpoint
415///   compatibility, and network reachability are separate concerns.
416/// - Muxer names and protocol names are separate namespaces (the `srt`
417///   muxer is the SubRip subtitle format, not the SRT streaming protocol).
418pub mod capabilities;
419
420/// The **filter** module provides a flexible framework for custom frame processing
421/// within the FFmpeg pipeline, along with the ability to query FFmpeg's built-in filters.
422/// It introduces the [`FrameFilter`](filter::frame_filter::FrameFilter) trait, which defines how to apply transformations
423/// (e.g., scaling, color adjustments, GPU-accelerated effects) to decoded frames.
424/// You can attach these filters to either the input or the output side
425/// (depending on your desired pipeline design) so that frames are automatically
426/// processed in your FFmpeg workflow.
427///
428/// # FFmpeg Built-in Filters
429///
430/// ```rust,ignore
431/// use ez_ffmpeg::core::filter::get_filters;
432///
433/// // Query available FFmpeg filters
434/// let filters = get_filters();
435/// for filter in filters {
436///     println!("Filter: {} - {}", filter.name, filter.description);
437/// }
438/// ```
439///
440/// # Defining and Using a Custom Filter
441///
442/// Below is a minimal example showing how to implement a custom filter and attach it to
443/// an `Output` so that every frame is processed before encoding. You could likewise
444/// attach it to an `Input` if you want the frames processed immediately after decoding.
445///
446/// ```rust,ignore
447///
448/// // 1. Define your custom filter by implementing the FrameFilter trait.
449/// struct FlipFilter;
450///
451/// impl FrameFilter for FlipFilter {
452///     fn media_type(&self) -> AVMediaType {
453///         // This filter operates on video frames.
454///         AVMediaType::AVMEDIA_TYPE_VIDEO
455///     }
456///
457///     fn filter_frame(
458///         &mut self,
459///         mut frame: Frame,
460///         _ctx: &mut FrameFilterContext,
461///     ) -> Result<Option<Frame>, Box<dyn std::error::Error + Send + Sync>> {
462///         // Forward an end-of-stream flush marker straight through.
463///         if ez_ffmpeg::util::ffmpeg_utils::frame_is_eof_marker(&frame) {
464///             return Ok(Some(frame));
465///         }
466///
467///         // Here you would implement the logic to transform the frame.
468///         // As a trivial example, we just return the original frame.
469///         // (Replace this with your actual transformation code.)
470///
471///         Ok(Some(frame))
472///     }
473/// }
474///
475/// fn main() -> Result<(), Box<dyn std::error::Error>> {
476///     // 2. Create a pipeline builder for video frames.
477///     let mut pipeline_builder: FramePipelineBuilder = AVMediaType::AVMEDIA_TYPE_VIDEO.into();
478///
479///     // 3. Add your custom filter to the pipeline, giving it a unique name.
480///     pipeline_builder = pipeline_builder.filter("flip-filter", Box::new(FlipFilter));
481///
482///     // 4. Attach the pipeline to an Output (could also attach to an Input).
483///     let mut output: Output = "output.mp4".into();
484///     output.add_frame_pipeline(pipeline_builder);
485///
486///     // 5. Build the FFmpeg context with both input and output.
487///     let context = FfmpegContext::builder()
488///         .input("input.mp4")
489///         .output(output)
490///         .build()?;
491///
492///     // 6. Run the FFmpeg job via the scheduler.
493///     FfmpegScheduler::new(context)
494///         .start()?
495///         .wait()?;
496///
497///     Ok(())
498/// }
499/// ```
500///
501/// In this example:
502/// 1. We define a **`FlipFilter`** that implements the [`FrameFilter`](filter::frame_filter::FrameFilter) trait and specifies
503///    `AVMediaType::AVMEDIA_TYPE_VIDEO`.
504/// 2. We create a **`FramePipelineBuilder`** for `VIDEO` frames and add our filter to it.
505/// 3. We attach that pipeline to the **`Output`** configuration, so frames will be processed
506///    (in this case, “flipped”) before encoding.
507/// 4. Finally, we build the FFmpeg context and run it with the **`FfmpegScheduler`**.
508///
509/// # More Advanced Filters
510///
511/// For a more complex, GPU-accelerated example, see the wgpu-based filters in the
512/// `wgpu_filter` module (feature `"wgpu"`). There, you can use custom WGSL fragment
513/// shaders to apply sophisticated transformations or visual effects on video frames.
514/// (The former `opengl` module remains available but is deprecated.)
515///
516/// # Trait Overview
517///
518/// The [`FrameFilter`](filter::frame_filter::FrameFilter) trait exposes several methods you can override:
519/// - [`FrameFilter::media_type()`](filter::frame_filter::FrameFilter::media_type): Indicates which media type (video, audio, etc.) this filter handles.
520/// - [`FrameFilter::init()`](filter::frame_filter::FrameFilter::init): Called once when the filter is first created (e.g., allocate resources).
521/// - [`FrameFilter::filter_frame()`](filter::frame_filter::FrameFilter::filter_frame): The primary method for transforming an incoming frame.
522/// - [`FrameFilter::request_frame()`](filter::frame_filter::FrameFilter::request_frame): If your filter generates frames on its own, you can override this.
523/// - [`FrameFilter::uninit()`](filter::frame_filter::FrameFilter::uninit): Called during cleanup when the filter is removed or the pipeline ends.
524///
525/// By chaining multiple filters in a pipeline, you can create sophisticated processing
526/// chains for your media data.
527pub mod filter;
528
529/// The **metadata** module provides internal metadata handling for FFmpeg operations.
530///
531/// **Internal Use Only**: This module contains unsafe FFmpeg C API wrappers.
532/// Users should use the safe public API on `Output` instead:
533/// - `Output::add_metadata()` for global metadata
534/// - `Output::add_stream_metadata()` for stream metadata
535/// - `Output::map_metadata_from_input()` for metadata mapping
536/// - `Output::disable_auto_copy_metadata()` for controlling auto-copy
537///
538/// # Example
539/// ```rust,ignore
540/// let output = Output::from("output.mp4")
541///     .add_metadata("title", "My Video")
542///     .add_metadata("author", "John Doe")
543///     .add_stream_metadata("v:0", "language", "eng")?;
544/// ```
545pub(crate) mod metadata;
546
547/// The **analysis** module surfaces typed detection results: native Rust crop /
548/// letterbox detection plus FFmpeg metadata from `blackdetect`,
549/// `silencedetect`, `scdet`, and `ebur128`.
550pub mod analysis;
551
552/// The **recipes** module provides one-shot helpers for common workflows
553/// (thumbnails/sprite sheets, animated GIF export, HLS ABR ladders) built on
554/// top of the ez-ffmpeg builder. The raw `filter_desc` escape hatch remains
555/// available for anything these do not cover.
556pub mod recipes;
557
558pub mod frame_export;
559
560#[cfg(feature = "cli")]
561pub mod cli;
562
563static INIT_FFMPEG: std::sync::Once = std::sync::Once::new();
564
565extern "C" fn cleanup() {
566    let _ = std::panic::catch_unwind(|| {
567        hwaccel::hw_device_free_all();
568        unsafe {
569            ffmpeg_sys_next::avformat_network_deinit();
570        }
571
572        log::debug!("FFmpeg cleaned up");
573    });
574}
575
576// C adjusts an array-typed `va_list` parameter to a pointer. Bindgen preserves
577// that adjustment in FFmpeg's function signatures, while its public `va_list`
578// alias remains the original one-element array. Extract the generated element
579// type so the callback follows the same ABI without naming bindgen internals.
580#[cfg(any(
581    all(target_arch = "powerpc", not(target_os = "uefi"), not(windows)),
582    target_arch = "s390x",
583    all(target_arch = "x86_64", not(target_os = "uefi"), not(windows)),
584))]
585trait VaListArray {
586    type Element;
587}
588
589#[cfg(any(
590    all(target_arch = "powerpc", not(target_os = "uefi"), not(windows)),
591    target_arch = "s390x",
592    all(target_arch = "x86_64", not(target_os = "uefi"), not(windows)),
593))]
594impl<T, const N: usize> VaListArray for [T; N] {
595    type Element = T;
596}
597
598#[cfg(any(
599    all(target_arch = "powerpc", not(target_os = "uefi"), not(windows)),
600    target_arch = "s390x",
601    all(target_arch = "x86_64", not(target_os = "uefi"), not(windows)),
602))]
603type VaListType = *mut <ffmpeg_sys_next::va_list as VaListArray>::Element;
604
605#[cfg(not(any(
606    all(target_arch = "powerpc", not(target_os = "uefi"), not(windows)),
607    target_arch = "s390x",
608    all(target_arch = "x86_64", not(target_os = "uefi"), not(windows)),
609)))]
610type VaListType = ffmpeg_sys_next::va_list;
611
612/// Log target used for every message forwarded from FFmpeg, so applications
613/// can tune FFmpeg's verbosity independently of ez-ffmpeg's own logs,
614/// e.g. `RUST_LOG=ez_ffmpeg=info,ez_ffmpeg::ffmpeg=warn`.
615pub const FFMPEG_LOG_TARGET: &str = "ez_ffmpeg::ffmpeg";
616
617/// Highest FFmpeg log level forwarded to the Rust `log` facade.
618/// Defaults to [`FfmpegLogLevel::Info`], matching the historical behavior.
619static FFMPEG_LOG_MAX_LEVEL: std::sync::atomic::AtomicI32 =
620    std::sync::atomic::AtomicI32::new(ffmpeg_sys_next::AV_LOG_INFO);
621
622/// Verbosity levels of the FFmpeg logging system (mirrors `AV_LOG_*`).
623#[derive(Debug, Clone, Copy, PartialEq, Eq)]
624#[non_exhaustive]
625pub enum FfmpegLogLevel {
626    Quiet,
627    Panic,
628    Fatal,
629    Error,
630    Warning,
631    Info,
632    Verbose,
633    Debug,
634    Trace,
635}
636
637impl FfmpegLogLevel {
638    fn to_av_level(self) -> libc::c_int {
639        match self {
640            FfmpegLogLevel::Quiet => ffmpeg_sys_next::AV_LOG_QUIET,
641            FfmpegLogLevel::Panic => ffmpeg_sys_next::AV_LOG_PANIC,
642            FfmpegLogLevel::Fatal => ffmpeg_sys_next::AV_LOG_FATAL,
643            FfmpegLogLevel::Error => ffmpeg_sys_next::AV_LOG_ERROR,
644            FfmpegLogLevel::Warning => ffmpeg_sys_next::AV_LOG_WARNING,
645            FfmpegLogLevel::Info => ffmpeg_sys_next::AV_LOG_INFO,
646            FfmpegLogLevel::Verbose => ffmpeg_sys_next::AV_LOG_VERBOSE,
647            FfmpegLogLevel::Debug => ffmpeg_sys_next::AV_LOG_DEBUG,
648            FfmpegLogLevel::Trace => ffmpeg_sys_next::AV_LOG_TRACE,
649        }
650    }
651}
652
653/// Sets the highest FFmpeg log level forwarded to the `log` facade
654/// (under the [`FFMPEG_LOG_TARGET`] target).
655///
656/// Messages above this level are dropped before any formatting work.
657/// Defaults to [`FfmpegLogLevel::Info`]; raise to [`FfmpegLogLevel::Trace`]
658/// to receive FFmpeg's debug/trace diagnostics (they map to `log::trace!`),
659/// or lower to [`FfmpegLogLevel::Error`] to keep only errors.
660pub fn set_ffmpeg_log_level(level: FfmpegLogLevel) {
661    FFMPEG_LOG_MAX_LEVEL.store(level.to_av_level(), std::sync::atomic::Ordering::Relaxed);
662}
663
664fn av_level_to_rust(level: libc::c_int) -> log::Level {
665    if level <= ffmpeg_sys_next::AV_LOG_ERROR {
666        log::Level::Error
667    } else if level <= ffmpeg_sys_next::AV_LOG_WARNING {
668        log::Level::Warn
669    } else if level <= ffmpeg_sys_next::AV_LOG_INFO {
670        log::Level::Info
671    } else if level <= ffmpeg_sys_next::AV_LOG_VERBOSE {
672        log::Level::Debug
673    } else {
674        log::Level::Trace
675    }
676}
677
678/// Shared formatting state for [`ffmpeg_log_callback`].
679///
680/// `print_prefix` must survive across invocations: FFmpeg emits partial log
681/// lines (not ending in '\n') and uses this flag to decide whether the next
682/// chunk starts a new prefixed line; a per-call flag broke multi-part
683/// messages. The same lock serializes `av_log_format_line` across threads
684/// and carries the duplicate-folding state (`AV_LOG_SKIP_REPEATED`
685/// semantics), mirroring FFmpeg's own default callback (libavutil/log.c).
686struct FfmpegLogState {
687    print_prefix: libc::c_int,
688    last_level: libc::c_int,
689    repeated: u64,
690    last_msg: String,
691}
692
693static FFMPEG_LOG_STATE: std::sync::Mutex<FfmpegLogState> = std::sync::Mutex::new(FfmpegLogState {
694    print_prefix: 1,
695    last_level: ffmpeg_sys_next::AV_LOG_INFO,
696    repeated: 0,
697    last_msg: String::new(),
698});
699
700struct DeferredFfmpegLog {
701    level: log::Level,
702    message: String,
703}
704
705thread_local! {
706    static DEFERRED_FFMPEG_LOGS: std::cell::RefCell<Vec<Vec<DeferredFfmpegLog>>> =
707        const { std::cell::RefCell::new(Vec::new()) };
708}
709
710/// Defers FFmpeg callback records on this thread until [`flush`](Self::flush).
711/// Hardware-device creation uses this while holding its process-wide init
712/// lock because FFmpeg may log synchronously from inside the create call.
713pub(crate) struct FfmpegLogScope {
714    active: bool,
715    _thread_bound: std::marker::PhantomData<std::rc::Rc<()>>,
716}
717
718pub(crate) fn defer_ffmpeg_logs() -> FfmpegLogScope {
719    DEFERRED_FFMPEG_LOGS.with(|stacks| stacks.borrow_mut().push(Vec::new()));
720    FfmpegLogScope {
721        active: true,
722        _thread_bound: std::marker::PhantomData,
723    }
724}
725
726impl FfmpegLogScope {
727    /// Releases the deferral scope and emits its records in callback order.
728    /// Nested scopes append to their parent; only the outermost scope reaches
729    /// the user logger.
730    pub(crate) fn flush(mut self) {
731        let records = DEFERRED_FFMPEG_LOGS.with(|stacks| {
732            let mut stacks = stacks.borrow_mut();
733            let records = stacks.pop().expect("FFmpeg log deferral scope is active");
734            if let Some(parent) = stacks.last_mut() {
735                parent.extend(records);
736                Vec::new()
737            } else {
738                records
739            }
740        });
741        self.active = false;
742
743        for record in records {
744            log::log!(target: FFMPEG_LOG_TARGET, record.level, "{}", record.message);
745        }
746    }
747}
748
749impl Drop for FfmpegLogScope {
750    fn drop(&mut self) {
751        if self.active {
752            // Unwinding a protected operation must first release its lock;
753            // dispatching arbitrary logger code from this Drop could mask the
754            // original panic or abort on a second panic.
755            DEFERRED_FFMPEG_LOGS.with(|stacks| {
756                stacks.borrow_mut().pop();
757            });
758        }
759    }
760}
761
762fn defer_ffmpeg_log(level: log::Level, args: std::fmt::Arguments<'_>) -> bool {
763    DEFERRED_FFMPEG_LOGS.with(|stacks| {
764        let mut stacks = stacks.borrow_mut();
765        let Some(records) = stacks.last_mut() else {
766            return false;
767        };
768        records.push(DeferredFfmpegLog {
769            level,
770            message: args.to_string(),
771        });
772        true
773    })
774}
775
776unsafe extern "C" fn ffmpeg_log_callback(
777    ptr: *mut libc::c_void,
778    level: libc::c_int,
779    fmt: *const libc::c_char,
780    args: VaListType,
781) {
782    // Cheap early exits before any formatting: av_vlog does not filter by
783    // level for custom callbacks, so verbose/debug/trace chatter would
784    // otherwise be vsnprintf-formatted only to be thrown away.
785    if level > FFMPEG_LOG_MAX_LEVEL.load(std::sync::atomic::Ordering::Relaxed) {
786        return;
787    }
788    let rust_level = av_level_to_rust(level);
789    if rust_level > log::max_level() {
790        return;
791    }
792
793    // A panicked holder cannot exist (no panicking code below), but never
794    // propagate poisoning out of an extern "C" callback.
795    let mut state = FFMPEG_LOG_STATE
796        .lock()
797        .unwrap_or_else(|poisoned| poisoned.into_inner());
798
799    let mut buffer = [0u8; 1024];
800    ffmpeg_sys_next::av_log_format_line(
801        ptr,
802        level,
803        fmt,
804        args,
805        buffer.as_mut_ptr() as *mut libc::c_char,
806        buffer.len() as libc::c_int,
807        &mut state.print_prefix,
808    );
809
810    let Ok(msg) = std::ffi::CStr::from_ptr(buffer.as_ptr() as *const libc::c_char).to_str() else {
811        return;
812    };
813    let trimmed_msg = msg.trim_end_matches(['\n', '\r']);
814
815    // Fold consecutive duplicates, like ffmpeg CLI's AV_LOG_SKIP_REPEATED
816    // (e.g. per-frame h264 decode errors after a mid-GOP seek).
817    if level == state.last_level && trimmed_msg == state.last_msg {
818        state.repeated += 1;
819        return;
820    }
821    let flush_repeated = if state.repeated > 0 {
822        Some((av_level_to_rust(state.last_level), state.repeated))
823    } else {
824        None
825    };
826    state.repeated = 0;
827    state.last_level = level;
828    state.last_msg.clear();
829    state.last_msg.push_str(trimmed_msg);
830
831    // Emit outside the lock: a logger backend may itself call into FFmpeg
832    // (re-entering this callback and self-deadlocking the Mutex) or panic
833    // (which must not unwind while the state lock is held).
834    drop(state);
835
836    let emitted = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
837        if let Some((repeated_level, repeated)) = flush_repeated {
838            if !defer_ffmpeg_log(
839                repeated_level,
840                format_args!("FFmpeg: last message repeated {} times", repeated),
841            ) {
842                log::log!(
843                    target: FFMPEG_LOG_TARGET,
844                    repeated_level,
845                    "FFmpeg: last message repeated {} times",
846                    repeated
847                );
848            }
849        }
850        if !defer_ffmpeg_log(rust_level, format_args!("FFmpeg: {}", trimmed_msg)) {
851            log::log!(target: FFMPEG_LOG_TARGET, rust_level, "FFmpeg: {}", trimmed_msg);
852        }
853    }));
854    if let Err(payload) = emitted {
855        // A logger backend is user code. Keep its unwind inside this C
856        // callback, and publish the failure when FFmpeg called us from a
857        // scheduler-owned worker.
858        let reported = std::panic::catch_unwind(|| {
859            crate::util::thread_synchronizer::report_worker_callback_panic();
860        });
861        if let Err(report_payload) = reported {
862            crate::core::packet_sink::dispose_panic_payload(report_payload);
863        }
864        crate::core::packet_sink::dispose_panic_payload(payload);
865    }
866}
867
868fn initialize_ffmpeg() {
869    let mut first_init = false;
870    INIT_FFMPEG.call_once(|| {
871        unsafe {
872            libc::atexit(cleanup as extern "C" fn());
873            ffmpeg_sys_next::avdevice_register_all();
874            ffmpeg_sys_next::avformat_network_init();
875            ffmpeg_sys_next::av_log_set_callback(Some(ffmpeg_log_callback));
876        }
877        first_init = true;
878    });
879    // Log AFTER call_once: a panicking logger backend must not poison the
880    // Once (which would permanently fail every later FFmpeg entry point),
881    // and a logger that re-enters this crate must not deadlock the
882    // initializer. Same emit-outside-the-lock policy as ffmpeg_log_callback.
883    if first_init {
884        log::info!("FFmpeg initialized.");
885    }
886}