rust_ffplay/
lib.rs

1//! Safe and idiomatic Rust wrapper for FFplay
2//!
3//! This crate provides a high-level, safe interface to FFplay functionality,
4//! allowing you to play multimedia files with various options.
5//!
6//! # Examples
7//!
8//! ## Basic playback
9//! ```no_run
10//! use ffplay_rs::FFplayBuilder;
11//!
12//! # async fn example() -> ffmpeg_common::Result<()> {
13//! // Play a video file
14//! let mut player = FFplayBuilder::play("video.mp4")
15//!     .spawn()
16//!     .await?;
17//!
18//! // Wait for playback to complete
19//! player.wait().await?;
20//! # Ok(())
21//! # }
22//! ```
23//!
24//! ## Advanced usage
25//! ```no_run
26//! use ffplay_rs::{FFplayBuilder, ShowMode};
27//! use ffplay_rs::playback::SyncType;
28//! use ffmpeg_common::{Duration, StreamSpecifier};
29//!
30//! # async fn example() -> ffmpeg_common::Result<()> {
31//! // Play with custom options
32//! let mut player = FFplayBuilder::new()?
33//!     .input("https://example.com/stream.m3u8")
34//!     .size(1280, 720)
35//!     .fullscreen(false)
36//!     .window_title("My Stream")
37//!     .seek(Duration::from_secs(30))
38//!     .duration(Duration::from_secs(120))
39//!     .volume(75)
40//!     .audio_stream(StreamSpecifier::Index(1))
41//!     .sync(SyncType::Audio)
42//!     .autoexit(true)
43//!     .spawn()
44//!     .await?;
45//!
46//! // Kill the player after some time
47//! tokio::time::sleep(std::time::Duration::from_secs(10)).await;
48//! player.kill().await?;
49//! # Ok(())
50//! # }
51//! ```
52//!
53//! ## Audio visualization
54//! ```no_run
55//! use ffplay_rs::{FFplayBuilder, ShowMode};
56//!
57//! # async fn example() -> ffmpeg_common::Result<()> {
58//! // Play audio with waveform visualization
59//! let mut player = FFplayBuilder::play("audio.mp3")
60//!     .show_mode(ShowMode::Waves)
61//!     .window_title("Audio Player")
62//!     .spawn()
63//!     .await?;
64//!
65//! player.wait().await?;
66//! # Ok(())
67//! # }
68//! ```
69
70#![warn(missing_docs)]
71#![warn(clippy::all)]
72#![warn(clippy::pedantic)]
73#![allow(clippy::module_name_repetitions)]
74#![allow(clippy::must_use_candidate)]
75
76pub mod builder;
77pub mod display;
78pub mod playback;
79pub mod types;
80
81// Re-export main types
82pub use builder::{FFplayBuilder, FFplayProcess};
83pub use display::DisplayOptions;
84pub use playback::{PlaybackOptions, SyncType};
85pub use types::{
86    HwAccelOptions, KeyBinding, MouseAction, PlaybackState, ShowMode, VisualizationType,
87    VulkanOptions, WindowState,
88};
89
90// Re-export from common
91pub use ffmpeg_common::{
92    get_version, Capabilities, Duration, Error, LogLevel, MediaPath, Result, StreamSpecifier,
93    StreamType, Version,
94};
95
96/// Prelude module for convenient imports
97pub mod prelude {
98    pub use crate::{
99        FFplayBuilder, ShowMode, SyncType,
100        display::presets as display_presets,
101        playback::presets as playback_presets,
102    };
103    pub use ffmpeg_common::{Duration, MediaPath, Result, StreamSpecifier};
104}
105
106/// Play a media file with default settings
107pub async fn play(path: impl Into<MediaPath>) -> Result<FFplayProcess> {
108    FFplayBuilder::play(path).spawn().await
109}
110
111/// Play in fullscreen
112pub async fn play_fullscreen(path: impl Into<MediaPath>) -> Result<FFplayProcess> {
113    FFplayBuilder::play_fullscreen(path).spawn().await
114}
115
116/// Play audio only
117pub async fn play_audio(path: impl Into<MediaPath>) -> Result<FFplayProcess> {
118    FFplayBuilder::play_audio(path).spawn().await
119}
120
121/// Get FFplay capabilities
122pub async fn capabilities() -> Result<Capabilities> {
123    Capabilities::detect("ffplay").await
124}
125
126/// Check if FFplay is available
127pub async fn is_available() -> bool {
128    ffmpeg_common::process::find_executable("ffplay").is_ok()
129}
130
131/// Get FFplay version
132pub async fn version() -> Result<Version> {
133    get_version("ffplay").await
134}
135
136/// Common playback scenarios
137pub mod scenarios {
138    use super::*;
139    use crate::prelude::*;
140
141    /// Play a video stream with low latency
142    pub fn stream_low_latency(url: impl Into<MediaPath>) -> FFplayBuilder {
143        FFplayBuilder::play(url)
144            .framedrop(true)
145            .infbuf(true)
146            .sync(SyncType::External)
147            .fast(true)
148    }
149
150    /// Play with hardware acceleration
151    pub fn with_hw_accel(path: impl Into<MediaPath>) -> FFplayBuilder {
152        let mut builder = FFplayBuilder::play(path);
153
154        #[cfg(target_os = "linux")]
155        {
156            builder = builder.raw_args(["-hwaccel", "vaapi"]);
157        }
158        #[cfg(target_os = "macos")]
159        {
160            builder = builder.raw_args(["-hwaccel", "videotoolbox"]);
161        }
162        #[cfg(target_os = "windows")]
163        {
164            builder = builder.raw_args(["-hwaccel", "d3d11va"]);
165        }
166
167        builder
168    }
169
170    /// Create a video wall (multiple instances)
171    pub async fn video_wall(
172        paths: Vec<impl Into<MediaPath>>,
173        grid_width: u32,
174        grid_height: u32,
175        window_width: u32,
176        window_height: u32,
177    ) -> Result<Vec<FFplayProcess>> {
178        let mut players = Vec::new();
179
180        for (i, path) in paths.into_iter().enumerate() {
181            let row = i as u32 / grid_width;
182            let col = i as u32 % grid_width;
183
184            let x = col * window_width;
185            let y = row * window_height;
186
187            let player = FFplayBuilder::play(path)
188                .size(window_width, window_height)
189                .window_position(x as i32, y as i32)
190                .borderless(true)
191                .no_audio(i > 0) // Only first instance plays audio
192                .spawn()
193                .await?;
194
195            players.push(player);
196        }
197
198        Ok(players)
199    }
200
201    /// Play with subtitle overlay
202    pub fn with_subtitles(
203        video: impl Into<MediaPath>,
204        subtitle_file: impl Into<String>,
205    ) -> FFplayBuilder {
206        FFplayBuilder::play(video)
207            .video_filter(format!("subtitles={}", subtitle_file.into()))
208    }
209
210    /// Play with custom aspect ratio
211    pub fn with_aspect_ratio(path: impl Into<MediaPath>, ratio: &str) -> FFplayBuilder {
212        FFplayBuilder::play(path)
213            .video_filter(format!("setdar={}", ratio))
214    }
215
216    /// Play with deinterlacing
217    pub fn deinterlaced(path: impl Into<MediaPath>) -> FFplayBuilder {
218        FFplayBuilder::play(path)
219            .video_filter("yadif")
220    }
221
222    /// Benchmark decoder performance
223    pub fn benchmark(path: impl Into<MediaPath>) -> FFplayBuilder {
224        FFplayBuilder::play(path)
225            .no_display(true)
226            .autoexit(true)
227            .raw_args(["-benchmark"])
228    }
229
230    /// Loop a short video as animated wallpaper
231    pub fn animated_wallpaper(path: impl Into<MediaPath>) -> FFplayBuilder {
232        FFplayBuilder::play(path)
233            .fullscreen(true)
234            .loop_count(-1)
235            .no_audio(true)
236            .exitonkeydown(false)
237            .exitonmousedown(false)
238    }
239}
240
241/// Helper utilities
242pub mod utils {
243    use super::*;
244
245    /// Get default window size based on video aspect ratio
246    pub fn calculate_window_size(
247        video_width: u32,
248        video_height: u32,
249        max_width: u32,
250        max_height: u32,
251    ) -> (u32, u32) {
252        let video_aspect = video_width as f64 / video_height as f64;
253        let max_aspect = max_width as f64 / max_height as f64;
254
255        if video_aspect > max_aspect {
256            // Video is wider than max area
257            let height = (max_width as f64 / video_aspect) as u32;
258            (max_width, height)
259        } else {
260            // Video is taller than max area
261            let width = (max_height as f64 * video_aspect) as u32;
262            (width, max_height)
263        }
264    }
265
266    /// Create filter string for picture-in-picture
267    pub fn pip_filter(
268        main_video: &str,
269        pip_video: &str,
270        pip_scale: f32,
271        pip_position: &str,
272    ) -> String {
273        format!(
274            "[0:v][1:v]scale=iw*{}:ih*{}[pip];[0:v][pip]overlay={}",
275            pip_scale, pip_scale, pip_position
276        )
277    }
278
279    /// Create filter for side-by-side comparison
280    pub fn side_by_side_filter() -> &'static str {
281        "[0:v]pad=iw*2:ih[bg];[bg][1:v]overlay=W/2:0"
282    }
283
284    /// Get key bindings help text
285    pub fn get_help_text() -> String {
286        let bindings = types::get_key_bindings();
287        let mut help = String::from("FFplay Key Bindings:\n\n");
288
289        for (key, desc) in bindings {
290            help.push_str(&format!("{:<20} {}\n", key, desc));
291        }
292
293        help
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use crate::prelude::*;
301
302    #[test]
303    fn test_basic_play() {
304        let builder = FFplayBuilder::play("test.mp4");
305        let command = builder.command().unwrap();
306        assert!(command.contains("ffplay"));
307        assert!(command.contains("test.mp4"));
308    }
309
310    #[test]
311    fn test_scenarios() {
312        let low_latency = scenarios::stream_low_latency("rtmp://example.com/live");
313        let args = low_latency.build_args().unwrap();
314        assert!(args.contains(&"-framedrop".to_string()));
315        assert!(args.contains(&"-infbuf".to_string()));
316        assert!(args.contains(&"-sync".to_string()));
317        assert!(args.contains(&"ext".to_string()));
318
319        let deinterlaced = scenarios::deinterlaced("interlaced.mp4");
320        let args = deinterlaced.build_args().unwrap();
321        assert!(args.contains(&"-vf".to_string()));
322        assert!(args.contains(&"yadif".to_string()));
323    }
324
325    #[test]
326    fn test_utils() {
327        // Test window size calculation
328        let (w, h) = utils::calculate_window_size(1920, 1080, 1280, 720);
329        assert_eq!(w, 1280);
330        assert_eq!(h, 720);
331
332        let (w, h) = utils::calculate_window_size(1080, 1920, 1280, 720);
333        assert_eq!(w, 405);
334        assert_eq!(h, 720);
335
336        // Test help text
337        let help = utils::get_help_text();
338        assert!(help.contains("FFplay Key Bindings"));
339        assert!(help.contains("Quit"));
340    }
341}