use ffmpeg_common::{
CommandBuilder, Duration, Error, LogLevel, MediaPath, Process, ProcessConfig, Result,
StreamSpecifier,
};
use std::path::PathBuf;
use std::time::Duration as StdDuration;
use tracing::info;
use crate::display::DisplayOptions;
use crate::playback::{PlaybackOptions, SyncType};
use crate::types::ShowMode;
#[derive(Debug, Clone)]
pub struct FFplayBuilder {
executable: PathBuf,
input: Option<MediaPath>,
display: DisplayOptions,
playback: PlaybackOptions,
log_level: Option<LogLevel>,
raw_args: Vec<String>,
timeout: Option<StdDuration>,
}
impl FFplayBuilder {
pub fn new() -> Result<Self> {
let executable = ffmpeg_common::process::find_executable("ffplay")?;
Ok(Self {
executable,
input: None,
display: DisplayOptions::default(),
playback: PlaybackOptions::default(),
log_level: None,
raw_args: Vec::new(),
timeout: None,
})
}
pub fn with_executable(path: impl Into<PathBuf>) -> Self {
Self {
executable: path.into(),
input: None,
display: DisplayOptions::default(),
playback: PlaybackOptions::default(),
log_level: None,
raw_args: Vec::new(),
timeout: None,
}
}
pub fn input(mut self, input: impl Into<MediaPath>) -> Self {
self.input = Some(input.into());
self
}
pub fn width(mut self, width: u32) -> Self {
self.display = self.display.width(width);
self
}
pub fn height(mut self, height: u32) -> Self {
self.display = self.display.height(height);
self
}
pub fn size(mut self, width: u32, height: u32) -> Self {
self.display = self.display.size(width, height);
self
}
pub fn fullscreen(mut self, enable: bool) -> Self {
self.display = self.display.fullscreen(enable);
self
}
pub fn window_title(mut self, title: impl Into<String>) -> Self {
self.display = self.display.window_title(title);
self
}
pub fn window_position(mut self, x: i32, y: i32) -> Self {
self.display = self.display.position(x, y);
self
}
pub fn borderless(mut self, enable: bool) -> Self {
self.display = self.display.borderless(enable);
self
}
pub fn always_on_top(mut self, enable: bool) -> Self {
self.display = self.display.always_on_top(enable);
self
}
pub fn no_display(mut self, enable: bool) -> Self {
self.display = self.display.no_display(enable);
self
}
pub fn show_mode(mut self, mode: ShowMode) -> Self {
self.display = self.display.show_mode(mode);
self
}
pub fn no_audio(mut self, enable: bool) -> Self {
self.playback = self.playback.no_audio(enable);
self
}
pub fn no_video(mut self, enable: bool) -> Self {
self.playback = self.playback.no_video(enable);
self
}
pub fn no_subtitles(mut self, enable: bool) -> Self {
self.playback = self.playback.no_subtitles(enable);
self
}
pub fn seek(mut self, position: Duration) -> Self {
self.playback = self.playback.seek(position);
self
}
pub fn duration(mut self, duration: Duration) -> Self {
self.playback = self.playback.duration(duration);
self
}
pub fn loop_count(mut self, count: i32) -> Self {
self.playback = self.playback.loop_count(count);
self
}
pub fn volume(mut self, volume: u8) -> Self {
self.playback = self.playback.volume(volume);
self
}
pub fn fast(mut self, enable: bool) -> Self {
self.playback = self.playback.fast(enable);
self
}
pub fn sync(mut self, sync_type: SyncType) -> Self {
self.playback = self.playback.sync(sync_type);
self
}
pub fn autoexit(mut self, enable: bool) -> Self {
self.playback = self.playback.autoexit(enable);
self
}
pub fn exitonkeydown(mut self, enable: bool) -> Self {
self.playback = self.playback.exitonkeydown(enable);
self
}
pub fn exitonmousedown(mut self, enable: bool) -> Self {
self.playback = self.playback.exitonmousedown(enable);
self
}
pub fn audio_stream(mut self, spec: StreamSpecifier) -> Self {
self.playback = self.playback.audio_stream(spec);
self
}
pub fn video_stream(mut self, spec: StreamSpecifier) -> Self {
self.playback = self.playback.video_stream(spec);
self
}
pub fn subtitle_stream(mut self, spec: StreamSpecifier) -> Self {
self.playback = self.playback.subtitle_stream(spec);
self
}
pub fn video_filter(mut self, filter: impl Into<String>) -> Self {
self.playback = self.playback.video_filter(filter);
self
}
pub fn audio_filter(mut self, filter: impl Into<String>) -> Self {
self.playback = self.playback.audio_filter(filter);
self
}
pub fn framedrop(mut self, enable: bool) -> Self {
self.playback = self.playback.framedrop(enable);
self
}
pub fn infbuf(mut self, enable: bool) -> Self {
self.playback = self.playback.infbuf(enable);
self
}
pub fn log_level(mut self, level: LogLevel) -> Self {
self.log_level = Some(level);
self
}
pub fn raw_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.raw_args.extend(args.into_iter().map(Into::into));
self
}
pub fn timeout(mut self, duration: StdDuration) -> Self {
self.timeout = Some(duration);
self
}
fn validate(&self) -> Result<()> {
if self.input.is_none() {
return Err(Error::InvalidArgument("No input specified".to_string()));
}
Ok(())
}
pub fn build_args(&self) -> Result<Vec<String>> {
self.validate()?;
let mut cmd = CommandBuilder::new();
if let Some(level) = self.log_level {
cmd = cmd.option("-loglevel", level.as_str());
}
cmd = cmd.args(self.display.build_args());
cmd = cmd.args(self.playback.build_args());
cmd = cmd.args(&self.raw_args);
if let Some(ref input) = self.input {
cmd = cmd.option("-i", input.as_str());
}
Ok(cmd.build())
}
pub async fn spawn(self) -> Result<FFplayProcess> {
let args = self.build_args()?;
info!("Spawning FFplay with args: {:?}", args);
let mut config = ProcessConfig::new(&self.executable)
.capture_stdout(false)
.capture_stderr(true);
if let Some(timeout) = self.timeout {
config = config.timeout(timeout);
}
let process = Process::spawn(config, args).await?;
Ok(FFplayProcess { process })
}
pub fn command(&self) -> Result<String> {
let args = self.build_args()?;
Ok(format!(
"{} {}",
self.executable.display(),
args.join(" ")
))
}
}
impl Default for FFplayBuilder {
fn default() -> Self {
Self::new().expect("FFplay executable not found")
}
}
pub struct FFplayProcess {
process: Process,
}
impl FFplayProcess {
pub async fn wait(self) -> Result<std::process::ExitStatus> {
let output = self.process.wait().await?;
Ok(output.status)
}
pub async fn kill(&mut self) -> Result<()> {
self.process.kill().await
}
pub fn id(&self) -> Option<u32> {
self.process.id()
}
pub fn try_wait(&mut self) -> Result<Option<std::process::ExitStatus>> {
self.process.try_wait()
}
}
impl FFplayBuilder {
pub fn play(input: impl Into<MediaPath>) -> Self {
Self::new().unwrap().input(input)
}
pub fn play_fullscreen(input: impl Into<MediaPath>) -> Self {
Self::play(input).fullscreen(true)
}
pub fn play_audio(input: impl Into<MediaPath>) -> Self {
Self::play(input).no_video(true).no_display(true)
}
pub fn play_video_only(input: impl Into<MediaPath>) -> Self {
Self::play(input).no_audio(true)
}
pub fn play_minimal(input: impl Into<MediaPath>) -> Self {
Self::play(input)
.borderless(true)
.exitonkeydown(true)
.exitonmousedown(true)
}
pub fn preview(input: impl Into<MediaPath>) -> Self {
Self::play(input)
.duration(Duration::from_secs(10))
.autoexit(true)
}
pub fn slideshow(pattern: impl Into<MediaPath>) -> Self {
Self::play(pattern)
.loop_count(-1)
.raw_args(["-framerate", "1"])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_playback() {
let builder = FFplayBuilder::play("video.mp4");
let args = builder.build_args().unwrap();
assert!(args.contains(&"-i".to_string()));
assert!(args.contains(&"video.mp4".to_string()));
}
#[test]
fn test_display_options() {
let builder = FFplayBuilder::play("video.mp4")
.size(1280, 720)
.fullscreen(true)
.window_title("My Video");
let args = builder.build_args().unwrap();
assert!(args.contains(&"-x".to_string()));
assert!(args.contains(&"1280".to_string()));
assert!(args.contains(&"-y".to_string()));
assert!(args.contains(&"720".to_string()));
assert!(args.contains(&"-fs".to_string()));
assert!(args.contains(&"-window_title".to_string()));
assert!(args.contains(&"My Video".to_string()));
}
#[test]
fn test_playback_options() {
let builder = FFplayBuilder::play("video.mp4")
.seek(Duration::from_secs(30))
.duration(Duration::from_secs(60))
.volume(50)
.loop_count(3);
let args = builder.build_args().unwrap();
assert!(args.contains(&"-ss".to_string()));
assert!(args.contains(&"00:00:30".to_string()));
assert!(args.contains(&"-t".to_string()));
assert!(args.contains(&"00:01:00".to_string()));
assert!(args.contains(&"-volume".to_string()));
assert!(args.contains(&"50".to_string()));
assert!(args.contains(&"-loop".to_string()));
assert!(args.contains(&"3".to_string()));
}
#[test]
fn test_convenience_functions() {
let fullscreen = FFplayBuilder::play_fullscreen("video.mp4");
let args = fullscreen.build_args().unwrap();
assert!(args.contains(&"-fs".to_string()));
let audio_only = FFplayBuilder::play_audio("audio.mp3");
let args = audio_only.build_args().unwrap();
assert!(args.contains(&"-vn".to_string()));
assert!(args.contains(&"-nodisp".to_string()));
let preview = FFplayBuilder::preview("video.mp4");
let args = preview.build_args().unwrap();
assert!(args.contains(&"-t".to_string()));
assert!(args.contains(&"-autoexit".to_string()));
}
}