Skip to main content

ff_probe/
lib.rs

1//! # ff-probe
2//!
3//! Media file metadata extraction - the Rust way.
4//!
5//! This crate provides functionality for extracting metadata from media files,
6//! including video streams, audio streams, and container information. It serves
7//! as the Rust equivalent of ffprobe with a clean, idiomatic API.
8//!
9//! ## Module Structure
10//!
11//! - `error` - Error types ([`ProbeError`])
12//! - `info` - Media info extraction ([`open`])
13//!
14//! ## Quick Start
15//!
16//! ```no_run
17//! use ff_probe::open;
18//!
19//! fn main() -> Result<(), Box<dyn std::error::Error>> {
20//!     let info = open("video.mp4")?;
21//!
22//!     println!("Format: {}", info.format());
23//!     println!("Duration: {:?}", info.duration());
24//!
25//!     // Check for video stream
26//!     if let Some(video) = info.primary_video() {
27//!         println!("Video: {}x{} @ {:.2} fps",
28//!             video.width(),
29//!             video.height(),
30//!             video.fps()
31//!         );
32//!     }
33//!
34//!     // Check for audio stream
35//!     if let Some(audio) = info.primary_audio() {
36//!         println!("Audio: {} Hz, {} channels",
37//!             audio.sample_rate(),
38//!             audio.channels()
39//!         );
40//!     }
41//!
42//!     Ok(())
43//! }
44//! ```
45//!
46//! ## Extracting Detailed Information
47//!
48//! ```no_run
49//! use ff_probe::{open, ColorSpace, ColorPrimaries};
50//!
51//! fn main() -> Result<(), Box<dyn std::error::Error>> {
52//!     let info = open("hdr_video.mp4")?;
53//!
54//!     // Enumerate all video streams
55//!     for (i, stream) in info.video_streams().iter().enumerate() {
56//!         println!("Video stream {}: {} {}x{}",
57//!             i, stream.codec_name(), stream.width(), stream.height());
58//!         println!("  Color space: {:?}", stream.color_space());
59//!         println!("  Color range: {:?}", stream.color_range());
60//!
61//!         // Check for HDR content
62//!         if stream.color_primaries() == ColorPrimaries::Bt2020 {
63//!             println!("  HDR content detected!");
64//!         }
65//!
66//!         if let Some(bitrate) = stream.bitrate() {
67//!             println!("  Bitrate: {} kbps", bitrate / 1000);
68//!         }
69//!     }
70//!
71//!     // Enumerate all audio streams
72//!     for (i, stream) in info.audio_streams().iter().enumerate() {
73//!         println!("Audio stream {}: {} {} Hz, {} ch",
74//!             i, stream.codec_name(), stream.sample_rate(), stream.channels());
75//!         if let Some(lang) = stream.language() {
76//!             println!("  Language: {}", lang);
77//!         }
78//!     }
79//!
80//!     // Access container metadata
81//!     if let Some(title) = info.title() {
82//!         println!("Title: {}", title);
83//!     }
84//!     if let Some(artist) = info.artist() {
85//!         println!("Artist: {}", artist);
86//!     }
87//!
88//!     Ok(())
89//! }
90//! ```
91//!
92//! ## Error Handling
93//!
94//! The crate provides detailed error types through [`ProbeError`]:
95//!
96//! ```
97//! use ff_probe::{open, ProbeError};
98//!
99//! let result = open("/nonexistent/path.mp4");
100//!
101//! match result {
102//!     Err(ProbeError::FileNotFound { path }) => {
103//!         println!("File not found: {}", path.display());
104//!     }
105//!     Err(ProbeError::CannotOpen { path, reason }) => {
106//!         println!("Cannot open {}: {}", path.display(), reason);
107//!     }
108//!     Err(ProbeError::InvalidMedia { path, reason }) => {
109//!         println!("Invalid media {}: {}", path.display(), reason);
110//!     }
111//!     Err(e) => println!("Other error: {}", e),
112//!     Ok(info) => println!("Opened: {}", info.format()),
113//! }
114//! ```
115//!
116//! ## Features
117//!
118//! - Extract container format information (MP4, MKV, AVI, etc.)
119//! - List all video and audio streams with detailed properties
120//! - Get codec parameters (codec type, pixel format, sample format)
121//! - Read container and stream metadata (title, artist, etc.)
122//! - Color space and HDR information (BT.709, BT.2020, etc.)
123//! - Bitrate extraction and calculation
124//! - Duration and frame count information
125
126#![warn(missing_docs)]
127#![warn(clippy::all)]
128#![warn(clippy::pedantic)]
129
130mod error;
131mod info;
132
133// Re-export types from ff-format for convenience
134pub use ff_format::{
135    AudioCodec, AudioStreamInfo, ChannelLayout, ChapterInfo, ChapterInfoBuilder, ColorPrimaries,
136    ColorRange, ColorSpace, MediaInfo, PixelFormat, Rational, SampleFormat, SubtitleCodec,
137    SubtitleStreamInfo, SubtitleStreamInfoBuilder, Timestamp, VideoCodec, VideoStreamInfo,
138};
139
140pub use error::ProbeError;
141
142// Re-export the open function at the crate level
143pub use info::open;
144
145/// Prelude module for convenient imports.
146///
147/// This module re-exports all commonly used types for easy access:
148///
149/// ```no_run
150/// use ff_probe::prelude::*;
151///
152/// fn main() -> Result<(), Box<dyn std::error::Error>> {
153///     let info = open("video.mp4")?;
154///
155///     // Access stream information
156///     if let Some(video) = info.primary_video() {
157///         let _codec: VideoCodec = video.codec();
158///         let _color: ColorSpace = video.color_space();
159///     }
160///
161///     Ok(())
162/// }
163/// ```
164pub mod prelude {
165    pub use crate::{
166        AudioCodec, AudioStreamInfo, ChannelLayout, ChapterInfo, ChapterInfoBuilder,
167        ColorPrimaries, ColorRange, ColorSpace, MediaInfo, PixelFormat, ProbeError, Rational,
168        SampleFormat, SubtitleCodec, SubtitleStreamInfo, SubtitleStreamInfoBuilder, Timestamp,
169        VideoCodec, VideoStreamInfo, open,
170    };
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_prelude_exports() {
179        // Verify prelude exports all expected types
180        let _rational: Rational = Rational::default();
181        let _timestamp: Timestamp = Timestamp::default();
182        let _color_space: ColorSpace = ColorSpace::default();
183        let _color_range: ColorRange = ColorRange::default();
184        let _color_primaries: ColorPrimaries = ColorPrimaries::default();
185        let _video_codec: VideoCodec = VideoCodec::default();
186        let _audio_codec: AudioCodec = AudioCodec::default();
187        let _channel_layout: ChannelLayout = ChannelLayout::default();
188        let _video_stream: VideoStreamInfo = VideoStreamInfo::default();
189        let _audio_stream: AudioStreamInfo = AudioStreamInfo::default();
190        let _media_info: MediaInfo = MediaInfo::default();
191        let _pixel_format: PixelFormat = PixelFormat::default();
192        let _sample_format: SampleFormat = SampleFormat::default();
193
194        // Verify open function is accessible (implicitly via the imports)
195        // The function signature uses impl AsRef<Path>, not &str
196        let _: Result<MediaInfo, ProbeError> = Err(ProbeError::FileNotFound {
197            path: std::path::PathBuf::new(),
198        });
199        // Can't easily verify function pointer due to impl trait, but open is re-exported
200    }
201
202    #[test]
203    fn test_probe_error_display() {
204        use std::path::PathBuf;
205
206        let err = ProbeError::FileNotFound {
207            path: PathBuf::from("test.mp4"),
208        };
209        assert!(err.to_string().contains("test.mp4"));
210    }
211
212    #[test]
213    fn test_open_nonexistent_file() {
214        let result = open("/nonexistent/path/to/video.mp4");
215        assert!(matches!(result, Err(ProbeError::FileNotFound { .. })));
216    }
217}