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/// Display matrix helpers shared by stream probing and filter graph setup.
83pub(crate) mod display;
84
85/// The **scheduler** module orchestrates the execution of a configured [`FfmpegContext`](context::ffmpeg_context::FfmpegContext).
86/// It provides the [`FfmpegScheduler`](scheduler::ffmpeg_scheduler::FfmpegScheduler) struct, which:
87///
88/// - **Starts** the FFmpeg pipeline via [`FfmpegScheduler::start()`](scheduler::ffmpeg_scheduler::FfmpegScheduler<crate::core::scheduler::ffmpeg_scheduler::Initialization>::start()).
89/// - **Manages** thread or subprocess creation, ensuring all streams and filters run.
90/// - **Waits** for completion (blocking or asynchronous, depending on whether the `async` feature is enabled).
91/// - **Returns** the final result, indicating success or failure.
92///
93/// # Synchronous Example
94///
95/// ```rust,ignore
96/// let context = FfmpegContext::builder()
97///     .input("test.mp4")
98///     .filter_desc("hue=s=0")
99///     .output("output.mp4")
100///     .build()
101///     .unwrap();
102///
103/// let result = FfmpegScheduler::new(context)
104///     .start()
105///     .unwrap()
106///     .wait();
107///
108/// assert!(result.is_ok(), "FFmpeg job failed unexpectedly");
109/// ```
110///
111/// # Asynchronous Example (requires `async` feature)
112///
113/// ```rust,ignore
114/// #[tokio::main]
115/// async fn main() {
116///     let context = FfmpegContext::builder()
117///         .input("test.mp4")
118///         .output("output.mp4")
119///         .build()
120///         .unwrap();
121///
122///     let mut scheduler = FfmpegScheduler::new(context)
123///         .start()
124///         .expect("Failed to start FFmpeg job");
125///
126///     // Asynchronous wait
127///     scheduler.await.expect("FFmpeg job failed unexpectedly");
128/// }
129/// ```
130pub mod scheduler;
131
132/// The **container_info** module provides utilities for retrieving metadata related to the media container,
133/// such as duration, format, and other general properties of the media file.
134///
135/// This module helps to query the overall properties of a media container file (e.g., `.mp4`, `.avi`, `.mkv`)
136/// without diving into individual streams (audio, video, etc.). It is useful when you need information
137/// about the file as a whole, such as total duration, format type, and container-specific properties.
138///
139/// # Examples
140///
141/// ```rust,ignore
142/// // Retrieve the duration in microseconds for the media file "test.mp4"
143/// let duration = get_duration_us("test.mp4").unwrap();
144/// println!("Duration: {} us", duration);
145///
146/// // Retrieve the format name for "test.mp4"
147/// let format = get_format("test.mp4").unwrap();
148/// println!("Format: {}", format);
149///
150/// // Retrieve the metadata for "test.mp4"
151/// let metadata = get_metadata("test.mp4").unwrap();
152/// for (key, value) in metadata {
153///     println!("{}: {}", key, value);
154/// }
155/// ```
156///
157/// These helper functions return the container-level metadata, and they handle any errors that may arise
158/// (e.g., if the file can't be opened or if there is an issue reading the data).
159pub mod container_info;
160
161/// The **stream_info** module provides utilities to retrieve detailed information
162/// about media streams (video, audio, and more) from an input source (e.g., a local file
163/// path, an RTMP URL, etc.). It queries FFmpeg for metadata regarding stream types, codec
164/// parameters, duration, and other relevant details.
165///
166/// # Examples
167///
168/// ```rust,ignore
169/// // Retrieve information about the first video stream in "test.mp4"
170/// let maybe_video_info = find_video_stream_info("test.mp4").unwrap();
171/// if let Some(video_info) = maybe_video_info {
172///     println!("Found video stream: {:?}", video_info);
173/// } else {
174///     println!("No video stream found.");
175/// }
176///
177/// // Retrieve information about the first audio stream in "test.mp4"
178/// let maybe_audio_info = find_audio_stream_info("test.mp4").unwrap();
179/// if let Some(audio_info) = maybe_audio_info {
180///     println!("Found audio stream: {:?}", audio_info);
181/// } else {
182///     println!("No audio stream found.");
183/// }
184///
185/// // Retrieve information about all streams (video, audio, etc.) in "test.mp4"
186/// let all_infos = find_all_stream_infos("test.mp4").unwrap();
187/// println!("Total streams found: {}", all_infos.len());
188/// for info in all_infos {
189///     println!("{:?}", info);
190/// }
191/// ```
192///
193/// These helper functions return `Result<Option<StreamInfo>, Error>` or `Result<Vec<StreamInfo>, Error>`
194/// depending on the call, allowing you to differentiate between "no stream found" (returns `Ok(None)`)
195/// and encountering an actual error (returns `Err(...)`).
196pub mod stream_info;
197
198/// The **packet_scanner** module provides a lightweight packet-level scanner for media files.
199///
200/// Unlike the full demuxing pipeline, `PacketScanner` iterates over raw demuxed packets
201/// without any decoding. This is useful for inspecting packet metadata such as timestamps,
202/// keyframe flags, sizes, and stream indices.
203///
204/// # Examples
205///
206/// ```rust,ignore
207/// use ez_ffmpeg::packet_scanner::PacketScanner;
208///
209/// let mut scanner = PacketScanner::open("test.mp4")?;
210/// for packet in scanner.packets() {
211///     let packet = packet?;
212///     println!(
213///         "stream={} pts={:?} size={} keyframe={}",
214///         packet.stream_index(),
215///         packet.pts(),
216///         packet.size(),
217///         packet.is_keyframe(),
218///     );
219/// }
220/// ```
221pub mod packet_scanner;
222
223/// The **device** module provides cross-platform methods to query available audio and video
224/// input devices on the system. Depending on the target operating system, it internally
225/// delegates to different platform APIs or FFmpeg’s device capabilities:
226///
227/// - **macOS**: Leverages AVFoundation for enumerating devices such as cameras ("vide")
228///   and microphones ("soun").
229/// - **Other OSes**: Uses FFmpeg’s `avdevice` to list input devices for video and audio.
230///
231/// These functions can be used to programmatically discover devices before choosing one
232/// for capture or recording in an FFmpeg-based pipeline.
233///
234/// # Examples
235///
236/// ```rust,ignore
237/// // Query video input devices (e.g., cameras)
238/// let video_devices = get_input_video_devices().unwrap();
239/// for device in &video_devices {
240///     println!("Available video device: {}", device);
241/// }
242///
243/// // Query audio input devices (e.g., microphones)
244/// let audio_devices = get_input_audio_devices().unwrap();
245/// for device in &audio_devices {
246///     println!("Available audio device: {}", device);
247/// }
248/// ```
249///
250/// # Notes
251///
252/// - If the query process fails (e.g., missing permissions or no devices available),
253///   the functions return an appropriate error from `crate::error`.
254/// - On macOS, the `AVFoundation` framework is used directly. On other platforms, FFmpeg’s
255///   `avdevice` functionality is used. Implementation details differ, but the returned
256///   results have a uniform format: a list of human-readable device names.
257/// - For more advanced device details (e.g., supported formats or resolutions), you may need
258///   to perform additional FFmpeg queries or platform-specific calls.
259pub mod device;
260/// The **hwaccel** module provides functionality for working with hardware-accelerated
261/// codecs in FFmpeg. It allows you to detect and configure various hardware devices
262/// (like NVENC, VAAPI, DXVA2, or VideoToolbox) so that FFmpeg can offload encoding or
263/// decoding tasks to GPU or specialized hardware.
264///
265/// # Public API
266///
267/// - [`get_hwaccels()`](hwaccel::get_hwaccels): Enumerates the hardware acceleration backends available on the
268///   current system, returning a list of [`HWAccelInfo`](hwaccel::HWAccelInfo) items. Each item contains a
269///   readable name (e.g., `"cuda"`, `"vaapi"`) and the corresponding `AVHWDeviceType`.
270///
271/// # Example
272///
273/// ```rust,ignore
274/// // Query hardware acceleration backends
275/// let hwaccels = get_hwaccels();
276/// for accel in hwaccels {
277///     println!("Found HW Accel: {} (type: {:?})", accel.name, accel.hw_device_type);
278/// }
279/// ```
280///
281/// # Notes
282///
283/// - While only [`get_hwaccels()`](hwaccel::get_hwaccels) is directly exposed, internally the module contains
284///   various helpers to initialize and manage hardware devices (e.g., `hw_device_init_from_string`).
285///   These are used behind the scenes or in more advanced scenarios where explicit control
286///   over device creation is required.
287/// - Hardware acceleration support depends on both FFmpeg’s compilation configuration
288///   and the underlying system drivers/frameworks. Not all listed accelerations may be
289///   fully functional on every platform.
290pub mod hwaccel;
291
292/// The **codec** module provides helpers for enumerating and querying FFmpeg’s
293/// available audio/video **encoders** and **decoders**. This can be useful for
294/// discovering which codecs are supported in your current FFmpeg build, along
295/// with their core attributes.
296///
297/// # Public API
298///
299/// - [`get_encoders()`](codec::get_encoders): Returns a list of [`CodecInfo`](codec::CodecInfo) representing all
300///   encoders (e.g., H.264, AAC) recognized by FFmpeg.
301/// - [`get_decoders()`](codec::get_decoders): Returns a list of [`CodecInfo`](codec::CodecInfo) representing all
302///   decoders (e.g., H.264, AAC) recognized by FFmpeg.
303///
304/// # Example
305///
306/// ```rust,ignore
307/// // List all available encoders
308/// let encoders = get_encoders();
309/// for enc in &encoders {
310///     println!("Encoder: {} - {}", enc.codec_name, enc.codec_long_name);
311/// }
312///
313/// // List all available decoders
314/// let decoders = get_decoders();
315/// for dec in &decoders {
316///     println!("Decoder: {} - {}", dec.codec_name, dec.codec_long_name);
317/// }
318/// ```
319///
320/// # Data Structures
321///
322/// - [`CodecInfo`](codec::CodecInfo): Contains user-friendly fields such as:
323///   - `codec_name` / `codec_long_name`
324///   - `desc_name`: The descriptor name from FFmpeg.
325///   - `media_type` (audio/video/subtitle, etc.)
326///   - `codec_id` (internal FFmpeg ID)
327///   - `codec_capabilities` (bitmask indicating codec features)
328///
329/// # Notes
330///
331/// - The underlying `Codec` struct is for internal usage only (`pub(crate)`,
332///   not part of the documented API), bridging to the raw FFmpeg APIs. In most
333///   cases, you only need the higher-level [`CodecInfo`](codec::CodecInfo)
334///   data from the public functions above.
335/// - The available encoders/decoders can vary depending on your FFmpeg build
336///   and any external libraries installed on the system.
337pub mod codec;
338
339/// The **filter** module provides a flexible framework for custom frame processing
340/// within the FFmpeg pipeline, along with the ability to query FFmpeg's built-in filters.
341/// It introduces the [`FrameFilter`](filter::frame_filter::FrameFilter) trait, which defines how to apply transformations
342/// (e.g., scaling, color adjustments, GPU-accelerated effects) to decoded frames.
343/// You can attach these filters to either the input or the output side
344/// (depending on your desired pipeline design) so that frames are automatically
345/// processed in your FFmpeg workflow.
346///
347/// # FFmpeg Built-in Filters
348///
349/// ```rust,ignore
350/// use ez_ffmpeg::core::filter::get_filters;
351///
352/// // Query available FFmpeg filters
353/// let filters = get_filters();
354/// for filter in filters {
355///     println!("Filter: {} - {}", filter.name, filter.description);
356/// }
357/// ```
358///
359/// # Defining and Using a Custom Filter
360///
361/// Below is a minimal example showing how to implement a custom filter and attach it to
362/// an `Output` so that every frame is processed before encoding. You could likewise
363/// attach it to an `Input` if you want the frames processed immediately after decoding.
364///
365/// ```rust,ignore
366///
367/// // 1. Define your custom filter by implementing the FrameFilter trait.
368/// struct FlipFilter;
369///
370/// impl FrameFilter for FlipFilter {
371///     fn media_type(&self) -> AVMediaType {
372///         // This filter operates on video frames.
373///         AVMediaType::AVMEDIA_TYPE_VIDEO
374///     }
375///
376///     fn filter_frame(
377///         &mut self,
378///         mut frame: Frame,
379///         _ctx: &mut FrameFilterContext,
380///     ) -> Result<Option<Frame>, Box<dyn std::error::Error + Send + Sync>> {
381///         // Forward an end-of-stream flush marker straight through.
382///         if ez_ffmpeg::util::ffmpeg_utils::frame_is_eof_marker(&frame) {
383///             return Ok(Some(frame));
384///         }
385///
386///         // Here you would implement the logic to transform the frame.
387///         // As a trivial example, we just return the original frame.
388///         // (Replace this with your actual transformation code.)
389///
390///         Ok(Some(frame))
391///     }
392/// }
393///
394/// fn main() -> Result<(), Box<dyn std::error::Error>> {
395///     // 2. Create a pipeline builder for video frames.
396///     let mut pipeline_builder: FramePipelineBuilder = AVMediaType::AVMEDIA_TYPE_VIDEO.into();
397///
398///     // 3. Add your custom filter to the pipeline, giving it a unique name.
399///     pipeline_builder = pipeline_builder.filter("flip-filter", Box::new(FlipFilter));
400///
401///     // 4. Attach the pipeline to an Output (could also attach to an Input).
402///     let mut output: Output = "output.mp4".into();
403///     output.add_frame_pipeline(pipeline_builder);
404///
405///     // 5. Build the FFmpeg context with both input and output.
406///     let context = FfmpegContext::builder()
407///         .input("input.mp4")
408///         .output(output)
409///         .build()?;
410///
411///     // 6. Run the FFmpeg job via the scheduler.
412///     FfmpegScheduler::new(context)
413///         .start()?
414///         .wait()?;
415///
416///     Ok(())
417/// }
418/// ```
419///
420/// In this example:
421/// 1. We define a **`FlipFilter`** that implements the [`FrameFilter`](filter::frame_filter::FrameFilter) trait and specifies
422///    `AVMediaType::AVMEDIA_TYPE_VIDEO`.
423/// 2. We create a **`FramePipelineBuilder`** for `VIDEO` frames and add our filter to it.
424/// 3. We attach that pipeline to the **`Output`** configuration, so frames will be processed
425///    (in this case, “flipped”) before encoding.
426/// 4. Finally, we build the FFmpeg context and run it with the **`FfmpegScheduler`**.
427///
428/// # More Advanced Filters
429///
430/// For a more complex, GPU-accelerated example, see the wgpu-based filters in the
431/// `wgpu_filter` module (feature `"wgpu"`). There, you can use custom WGSL fragment
432/// shaders to apply sophisticated transformations or visual effects on video frames.
433/// (The former `opengl` module remains available but is deprecated.)
434///
435/// # Trait Overview
436///
437/// The [`FrameFilter`](filter::frame_filter::FrameFilter) trait exposes several methods you can override:
438/// - [`FrameFilter::media_type()`](filter::frame_filter::FrameFilter::media_type): Indicates which media type (video, audio, etc.) this filter handles.
439/// - [`FrameFilter::init()`](filter::frame_filter::FrameFilter::init): Called once when the filter is first created (e.g., allocate resources).
440/// - [`FrameFilter::filter_frame()`](filter::frame_filter::FrameFilter::filter_frame): The primary method for transforming an incoming frame.
441/// - [`FrameFilter::request_frame()`](filter::frame_filter::FrameFilter::request_frame): If your filter generates frames on its own, you can override this.
442/// - [`FrameFilter::uninit()`](filter::frame_filter::FrameFilter::uninit): Called during cleanup when the filter is removed or the pipeline ends.
443///
444/// By chaining multiple filters in a pipeline, you can create sophisticated processing
445/// chains for your media data.
446pub mod filter;
447
448/// The **metadata** module provides internal metadata handling for FFmpeg operations.
449///
450/// **Internal Use Only**: This module contains unsafe FFmpeg C API wrappers.
451/// Users should use the safe public API on `Output` instead:
452/// - `Output::add_metadata()` for global metadata
453/// - `Output::add_stream_metadata()` for stream metadata
454/// - `Output::map_metadata_from_input()` for metadata mapping
455/// - `Output::disable_auto_copy_metadata()` for controlling auto-copy
456///
457/// # Example
458/// ```rust,ignore
459/// let output = Output::from("output.mp4")
460///     .add_metadata("title", "My Video")
461///     .add_metadata("author", "John Doe")
462///     .add_stream_metadata("v:0", "language", "eng")?;
463/// ```
464pub(crate) mod metadata;
465
466/// The **analysis** module surfaces the results of FFmpeg detector/measurement
467/// filters (`blackdetect`, `silencedetect`, `scdet`, `cropdetect`, `ebur128`)
468/// as typed Rust events and a folded report, instead of only FFmpeg logs.
469pub mod analysis;
470
471/// The **recipes** module provides one-shot helpers for common workflows
472/// (thumbnails/sprite sheets, animated GIF export, HLS ABR ladders) built on
473/// top of the ez-ffmpeg builder. The raw `filter_desc` escape hatch remains
474/// available for anything these do not cover.
475pub mod recipes;
476
477static INIT_FFMPEG: std::sync::Once = std::sync::Once::new();
478
479extern "C" fn cleanup() {
480    let _ = std::panic::catch_unwind(|| {
481        unsafe {
482            hwaccel::hw_device_free_all();
483            ffmpeg_sys_next::avformat_network_deinit();
484        }
485
486        log::debug!("FFmpeg cleaned up");
487    });
488}
489
490// The following type definitions for `VaListType` are inspired by the Rust standard library's
491// implementation of `va_list` (see std::ffi::va_list::VaListImpl). These definitions ensure compatibility
492// with platform-specific ABI requirements when interfacing with C variadic functions.
493
494#[cfg(any(
495    all(
496        not(target_arch = "aarch64"),
497        not(target_arch = "powerpc"),
498        not(target_arch = "s390x"),
499        not(target_arch = "x86_64")
500    ),
501    all(target_arch = "aarch64", target_vendor = "apple"),
502    target_family = "wasm",
503    target_os = "uefi",
504    windows,
505))]
506type VaListType = *mut libc::c_char;
507
508#[cfg(all(target_arch = "x86_64", not(target_os = "uefi"), not(windows)))]
509type VaListType = *mut ffmpeg_sys_next::__va_list_tag;
510
511#[cfg(all(
512    target_arch = "aarch64",
513    not(target_vendor = "apple"),
514    not(target_os = "uefi"),
515    not(windows),
516))]
517type VaListType = *mut libc::c_void;
518
519#[cfg(all(target_arch = "powerpc", not(target_os = "uefi"), not(windows)))]
520type VaListType = *mut ffmpeg_sys_next::__va_list_tag_powerpc;
521
522#[cfg(target_arch = "s390x")]
523type VaListType = *mut ffmpeg_sys_next::__va_list_tag_s390x;
524
525/// Log target used for every message forwarded from FFmpeg, so applications
526/// can tune FFmpeg's verbosity independently of ez-ffmpeg's own logs,
527/// e.g. `RUST_LOG=ez_ffmpeg=info,ez_ffmpeg::ffmpeg=warn`.
528pub const FFMPEG_LOG_TARGET: &str = "ez_ffmpeg::ffmpeg";
529
530/// Highest FFmpeg log level forwarded to the Rust `log` facade.
531/// Defaults to [`FfmpegLogLevel::Info`], matching the historical behavior.
532static FFMPEG_LOG_MAX_LEVEL: std::sync::atomic::AtomicI32 =
533    std::sync::atomic::AtomicI32::new(ffmpeg_sys_next::AV_LOG_INFO);
534
535/// Verbosity levels of the FFmpeg logging system (mirrors `AV_LOG_*`).
536#[derive(Debug, Clone, Copy, PartialEq, Eq)]
537#[non_exhaustive]
538pub enum FfmpegLogLevel {
539    Quiet,
540    Panic,
541    Fatal,
542    Error,
543    Warning,
544    Info,
545    Verbose,
546    Debug,
547    Trace,
548}
549
550impl FfmpegLogLevel {
551    fn to_av_level(self) -> libc::c_int {
552        match self {
553            FfmpegLogLevel::Quiet => ffmpeg_sys_next::AV_LOG_QUIET,
554            FfmpegLogLevel::Panic => ffmpeg_sys_next::AV_LOG_PANIC,
555            FfmpegLogLevel::Fatal => ffmpeg_sys_next::AV_LOG_FATAL,
556            FfmpegLogLevel::Error => ffmpeg_sys_next::AV_LOG_ERROR,
557            FfmpegLogLevel::Warning => ffmpeg_sys_next::AV_LOG_WARNING,
558            FfmpegLogLevel::Info => ffmpeg_sys_next::AV_LOG_INFO,
559            FfmpegLogLevel::Verbose => ffmpeg_sys_next::AV_LOG_VERBOSE,
560            FfmpegLogLevel::Debug => ffmpeg_sys_next::AV_LOG_DEBUG,
561            FfmpegLogLevel::Trace => ffmpeg_sys_next::AV_LOG_TRACE,
562        }
563    }
564}
565
566/// Sets the highest FFmpeg log level forwarded to the `log` facade
567/// (under the [`FFMPEG_LOG_TARGET`] target).
568///
569/// Messages above this level are dropped before any formatting work.
570/// Defaults to [`FfmpegLogLevel::Info`]; raise to [`FfmpegLogLevel::Trace`]
571/// to receive FFmpeg's debug/trace diagnostics (they map to `log::trace!`),
572/// or lower to [`FfmpegLogLevel::Error`] to keep only errors.
573pub fn set_ffmpeg_log_level(level: FfmpegLogLevel) {
574    FFMPEG_LOG_MAX_LEVEL.store(level.to_av_level(), std::sync::atomic::Ordering::Relaxed);
575}
576
577fn av_level_to_rust(level: libc::c_int) -> log::Level {
578    if level <= ffmpeg_sys_next::AV_LOG_ERROR {
579        log::Level::Error
580    } else if level <= ffmpeg_sys_next::AV_LOG_WARNING {
581        log::Level::Warn
582    } else if level <= ffmpeg_sys_next::AV_LOG_INFO {
583        log::Level::Info
584    } else if level <= ffmpeg_sys_next::AV_LOG_VERBOSE {
585        log::Level::Debug
586    } else {
587        log::Level::Trace
588    }
589}
590
591/// Shared formatting state for [`ffmpeg_log_callback`].
592///
593/// `print_prefix` must survive across invocations: FFmpeg emits partial log
594/// lines (not ending in '\n') and uses this flag to decide whether the next
595/// chunk starts a new prefixed line; a per-call flag broke multi-part
596/// messages. The same lock serializes `av_log_format_line` across threads
597/// and carries the duplicate-folding state (`AV_LOG_SKIP_REPEATED`
598/// semantics), mirroring FFmpeg's own default callback (libavutil/log.c).
599struct FfmpegLogState {
600    print_prefix: libc::c_int,
601    last_level: libc::c_int,
602    repeated: u64,
603    last_msg: String,
604}
605
606static FFMPEG_LOG_STATE: std::sync::Mutex<FfmpegLogState> = std::sync::Mutex::new(FfmpegLogState {
607    print_prefix: 1,
608    last_level: ffmpeg_sys_next::AV_LOG_INFO,
609    repeated: 0,
610    last_msg: String::new(),
611});
612
613unsafe extern "C" fn ffmpeg_log_callback(
614    ptr: *mut libc::c_void,
615    level: libc::c_int,
616    fmt: *const libc::c_char,
617    args: VaListType,
618) {
619    // Cheap early exits before any formatting: av_vlog does not filter by
620    // level for custom callbacks, so verbose/debug/trace chatter would
621    // otherwise be vsnprintf-formatted only to be thrown away.
622    if level > FFMPEG_LOG_MAX_LEVEL.load(std::sync::atomic::Ordering::Relaxed) {
623        return;
624    }
625    let rust_level = av_level_to_rust(level);
626    if rust_level > log::max_level() {
627        return;
628    }
629
630    // A panicked holder cannot exist (no panicking code below), but never
631    // propagate poisoning out of an extern "C" callback.
632    let mut state = FFMPEG_LOG_STATE
633        .lock()
634        .unwrap_or_else(|poisoned| poisoned.into_inner());
635
636    let mut buffer = [0u8; 1024];
637    ffmpeg_sys_next::av_log_format_line(
638        ptr,
639        level,
640        fmt,
641        args,
642        buffer.as_mut_ptr() as *mut libc::c_char,
643        buffer.len() as libc::c_int,
644        &mut state.print_prefix,
645    );
646
647    let Ok(msg) = std::ffi::CStr::from_ptr(buffer.as_ptr() as *const libc::c_char).to_str() else {
648        return;
649    };
650    let trimmed_msg = msg.trim_end_matches(['\n', '\r']);
651
652    // Fold consecutive duplicates, like ffmpeg CLI's AV_LOG_SKIP_REPEATED
653    // (e.g. per-frame h264 decode errors after a mid-GOP seek).
654    if level == state.last_level && trimmed_msg == state.last_msg {
655        state.repeated += 1;
656        return;
657    }
658    let flush_repeated = if state.repeated > 0 {
659        Some((av_level_to_rust(state.last_level), state.repeated))
660    } else {
661        None
662    };
663    state.repeated = 0;
664    state.last_level = level;
665    state.last_msg.clear();
666    state.last_msg.push_str(trimmed_msg);
667
668    // Emit outside the lock: a logger backend may itself call into FFmpeg
669    // (re-entering this callback and self-deadlocking the Mutex) or panic
670    // (which must not unwind while the state lock is held).
671    drop(state);
672
673    if let Some((repeated_level, repeated)) = flush_repeated {
674        log::log!(
675            target: FFMPEG_LOG_TARGET,
676            repeated_level,
677            "FFmpeg: last message repeated {} times",
678            repeated
679        );
680    }
681    log::log!(target: FFMPEG_LOG_TARGET, rust_level, "FFmpeg: {}", trimmed_msg);
682}
683
684fn initialize_ffmpeg() {
685    INIT_FFMPEG.call_once(|| {
686        unsafe {
687            libc::atexit(cleanup as extern "C" fn());
688            ffmpeg_sys_next::avdevice_register_all();
689            ffmpeg_sys_next::avformat_network_init();
690            ffmpeg_sys_next::av_log_set_callback(Some(ffmpeg_log_callback));
691        }
692        log::info!("FFmpeg initialized.");
693    });
694}