use ffmpeg_common::{CommandBuilder, Duration, MediaPath, PixelFormat, Result, Size, Error};
use std::collections::HashMap;
use std::time::Duration as StdDuration;
#[derive(Debug, Clone)]
pub struct Input {
source: MediaPath,
format: Option<String>,
seek: Option<Duration>,
duration: Option<Duration>,
framerate: Option<f64>,
video_size: Option<(u32, u32)>,
pixel_format: Option<PixelFormat>,
sample_rate: Option<u32>,
channels: Option<u32>,
loop_count: Option<i32>,
realtime: bool,
thread_queue_size: Option<u32>,
options: HashMap<String, String>,
decoder: Option<String>,
hwaccel_device: Option<String>,
buffer_size: Option<Size>,
discard_threshold: Option<StdDuration>,
}
impl Input {
pub fn new(source: impl Into<MediaPath>) -> Self {
Self {
source: source.into(),
format: None,
seek: None,
duration: None,
framerate: None,
video_size: None,
pixel_format: None,
sample_rate: None,
channels: None,
loop_count: None,
realtime: false,
thread_queue_size: None,
options: HashMap::new(),
decoder: None,
hwaccel_device: None,
buffer_size: None,
discard_threshold: None,
}
}
pub fn format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
pub fn seek(mut self, position: Duration) -> Self {
self.seek = Some(position);
self
}
pub fn duration(mut self, duration: Duration) -> Self {
self.duration = Some(duration);
self
}
pub fn framerate(mut self, fps: f64) -> Self {
self.framerate = Some(fps);
self
}
pub fn video_size(mut self, width: u32, height: u32) -> Self {
self.video_size = Some((width, height));
self
}
pub fn pixel_format(mut self, format: PixelFormat) -> Self {
self.pixel_format = Some(format);
self
}
pub fn sample_rate(mut self, rate: u32) -> Self {
self.sample_rate = Some(rate);
self
}
pub fn channels(mut self, channels: u32) -> Self {
self.channels = Some(channels);
self
}
pub fn loop_input(mut self, count: i32) -> Self {
self.loop_count = Some(count);
self
}
pub fn realtime(mut self, enable: bool) -> Self {
self.realtime = enable;
self
}
pub fn thread_queue_size(mut self, size: u32) -> Self {
self.thread_queue_size = Some(size);
self
}
pub fn option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.options.insert(key.into(), value.into());
self
}
pub fn decoder(mut self, decoder: impl Into<String>) -> Self {
self.decoder = Some(decoder.into());
self
}
pub fn hwaccel_device(mut self, device: impl Into<String>) -> Self {
self.hwaccel_device = Some(device.into());
self
}
pub fn buffer_size(mut self, size: Size) -> Self {
self.buffer_size = Some(size);
self
}
pub fn discard_threshold(mut self, threshold: StdDuration) -> Self {
self.discard_threshold = Some(threshold);
self
}
pub fn build_args(&self) -> Vec<String> {
let mut cmd = CommandBuilder::new();
if let Some(ref format) = self.format {
cmd = cmd.option("-f", format);
}
if let Some(seek) = self.seek {
cmd = cmd.option("-ss", seek.to_ffmpeg_format());
}
if let Some(duration) = self.duration {
cmd = cmd.option("-t", duration.to_ffmpeg_format());
}
if let Some(fps) = self.framerate {
cmd = cmd.option("-framerate", fps);
}
if let Some((width, height)) = self.video_size {
cmd = cmd.option("-video_size", format!("{}x{}", width, height));
}
if let Some(ref pix_fmt) = self.pixel_format {
cmd = cmd.option("-pixel_format", pix_fmt.as_str());
}
if let Some(rate) = self.sample_rate {
cmd = cmd.option("-ar", rate);
}
if let Some(channels) = self.channels {
cmd = cmd.option("-ac", channels);
}
if let Some(loop_count) = self.loop_count {
cmd = cmd.option("-stream_loop", loop_count);
}
if self.realtime {
cmd = cmd.flag("-re");
}
if let Some(size) = self.thread_queue_size {
cmd = cmd.option("-thread_queue_size", size);
}
if let Some(ref decoder) = self.decoder {
cmd = cmd.option("-c:v", decoder);
}
if let Some(ref device) = self.hwaccel_device {
cmd = cmd.option("-hwaccel_device", device);
}
if let Some(ref size) = self.buffer_size {
cmd = cmd.option("-bufsize", size.as_bytes());
}
if let Some(ref _threshold) = self.discard_threshold {
cmd = cmd.option("-fflags", "+discardcorrupt");
cmd = cmd.option("-err_detect", "ignore_err");
}
for (key, value) in &self.options {
cmd = cmd.option(format!("-{}", key), value);
}
cmd = cmd.option("-i", self.source.as_str());
cmd.build()
}
}
#[derive(Debug, Clone)]
pub struct DeviceInput {
device_type: String,
device: String,
options: HashMap<String, String>,
}
impl DeviceInput {
pub fn new(device_type: impl Into<String>, device: impl Into<String>) -> Self {
Self {
device_type: device_type.into(),
device: device.into(),
options: HashMap::new(),
}
}
#[cfg(target_os = "linux")]
pub fn webcam(device: impl Into<String>) -> Self {
Self::new("v4l2", device)
}
#[cfg(target_os = "windows")]
pub fn webcam(device: impl Into<String>) -> Self {
Self::new("dshow", format!("video={}", device.into()))
}
#[cfg(target_os = "macos")]
pub fn webcam(device: impl Into<String>) -> Self {
Self::new("avfoundation", device)
}
#[cfg(target_os = "linux")]
pub fn screen_capture() -> Self {
Self::new("x11grab", ":0.0")
}
#[cfg(target_os = "windows")]
pub fn screen_capture() -> Self {
Self::new("gdigrab", "desktop")
}
#[cfg(target_os = "macos")]
pub fn screen_capture() -> Self {
Self::new("avfoundation", "1:none")
}
pub fn option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.options.insert(key.into(), value.into());
self
}
pub fn into_input(self) -> Input {
let mut input = Input::new(self.device).format(self.device_type);
for (key, value) in self.options {
input = input.option(key, value);
}
input
}
}
#[derive(Debug, Clone)]
pub struct StreamInput {
url: String,
options: HashMap<String, String>,
reconnect: bool,
reconnect_delay: Option<StdDuration>,
reconnect_attempts: Option<u32>,
}
impl StreamInput {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
options: HashMap::new(),
reconnect: false,
reconnect_delay: None,
reconnect_attempts: None,
}
}
pub fn rtmp(url: impl Into<String>) -> Self {
Self::new(url)
}
pub fn rtsp(url: impl Into<String>) -> Self {
Self::new(url).option("rtsp_transport", "tcp")
}
pub fn http(url: impl Into<String>) -> Self {
Self::new(url)
}
pub fn reconnect(mut self, enable: bool) -> Self {
self.reconnect = enable;
self
}
pub fn reconnect_delay(mut self, delay: StdDuration) -> Self {
self.reconnect_delay = Some(delay);
self
}
pub fn reconnect_attempts(mut self, attempts: u32) -> Self {
self.reconnect_attempts = Some(attempts);
self
}
pub fn option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.options.insert(key.into(), value.into());
self
}
pub fn user_agent(self, agent: impl Into<String>) -> Self {
self.option("user_agent", agent)
}
pub fn timeout(self, timeout: StdDuration) -> Self {
self.option("timeout", timeout.as_micros().to_string())
}
pub fn into_input(self) -> Input {
let mut input = Input::new(self.url);
if self.reconnect {
input = input.option("reconnect", "1");
if let Some(delay) = self.reconnect_delay {
input = input.option("reconnect_delay_max", delay.as_secs().to_string());
}
if let Some(attempts) = self.reconnect_attempts {
input = input.option("reconnect_streamed", attempts.to_string());
}
}
for (key, value) in self.options {
input = input.option(key, value);
}
input
}
}
#[derive(Debug, Clone)]
pub struct ConcatInput {
inputs: Vec<MediaPath>,
use_demuxer: bool,
}
impl ConcatInput {
pub fn new() -> Self {
Self {
inputs: Vec::new(),
use_demuxer: false,
}
}
pub fn add_input(mut self, path: impl Into<MediaPath>) -> Self {
self.inputs.push(path.into());
self
}
pub fn add_inputs(mut self, paths: impl IntoIterator<Item = impl Into<MediaPath>>) -> Self {
self.inputs.extend(paths.into_iter().map(Into::into));
self
}
pub fn use_demuxer(mut self, enable: bool) -> Self {
self.use_demuxer = enable;
self
}
pub fn into_inputs(self) -> Result<Vec<Input>> {
if self.inputs.is_empty() {
return Err(Error::InvalidArgument(
"No inputs provided for concatenation".to_string(),
));
}
if self.use_demuxer {
let concat_string = self
.inputs
.iter()
.map(|p| p.as_str())
.collect::<Vec<_>>()
.join("|");
Ok(vec![Input::new(format!("concat:{}", concat_string))])
} else {
Ok(self.inputs.into_iter().map(Input::new).collect())
}
}
}
impl Default for ConcatInput {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_input_builder() {
let input = Input::new("input.mp4")
.format("mp4")
.seek(Duration::from_secs(10))
.duration(Duration::from_secs(30))
.option("custom", "value");
let args = input.build_args();
assert!(args.contains(&"-f".to_string()));
assert!(args.contains(&"mp4".to_string()));
assert!(args.contains(&"-ss".to_string()));
assert!(args.contains(&"00:00:10".to_string()));
assert!(args.contains(&"-t".to_string()));
assert!(args.contains(&"00:00:30".to_string()));
assert!(args.contains(&"-custom".to_string()));
assert!(args.contains(&"value".to_string()));
assert!(args.contains(&"-i".to_string()));
assert!(args.contains(&"input.mp4".to_string()));
}
#[test]
fn test_stream_input() {
let input = StreamInput::rtsp("rtsp://example.com/stream")
.reconnect(true)
.timeout(StdDuration::from_secs(10))
.into_input();
let args = input.build_args();
assert!(args.contains(&"-rtsp_transport".to_string()));
assert!(args.contains(&"tcp".to_string()));
assert!(args.contains(&"-reconnect".to_string()));
assert!(args.contains(&"1".to_string()));
assert!(args.contains(&"-timeout".to_string()));
}
#[test]
fn test_concat_input() {
let concat = ConcatInput::new()
.add_input("file1.mp4")
.add_input("file2.mp4")
.add_input("file3.mp4");
let inputs = concat.into_inputs().unwrap();
assert_eq!(inputs.len(), 3);
}
}