tokio_ffmpeg_cli/
lib.rs

1#![allow(clippy::uninlined_format_args)]
2
3/// Progress event
4mod progress_event;
5
6/// The command builder
7mod builder;
8
9/// Encoder info
10mod encoder;
11
12pub use self::{
13    builder::Builder,
14    encoder::{
15        Encoder,
16        FromLineError as EncoderFromLineError,
17    },
18    progress_event::{
19        LineBuilderError,
20        ProgressEvent,
21    },
22};
23use std::process::ExitStatus;
24
25/// The error type
26#[derive(Debug, thiserror::Error)]
27pub enum Error {
28    /// Failed to spawn a process
29    #[error("failed to spawn a process")]
30    ProcessSpawn(#[source] std::io::Error),
31
32    /// The input file was not specified
33    #[error("missing input file")]
34    MissingInput,
35
36    /// The output file was not specified
37    #[error("missing output file")]
38    MissingOutput,
39
40    /// An IO error occured
41    #[error("io error")]
42    Io(#[source] std::io::Error),
43
44    /// The output file already exists
45    #[error("output file already exists")]
46    OutputAlreadyExists,
47
48    /// Failed to construct a progress event
49    #[error("invalid progress event")]
50    InvalidProgressEvent(#[from] crate::progress_event::LineBuilderError),
51
52    /// An exit status was invalid
53    #[error("invalid exit status '{0}'")]
54    InvalidExitStatus(ExitStatus),
55
56    /// Failed to convert bytes to a str
57    #[error(transparent)]
58    InvalidUtf8Str(std::str::Utf8Error),
59
60    /// Invalid encoder
61    #[error("failed to parse encoder line")]
62    InvalidEncoderLine(#[from] EncoderFromLineError),
63}
64
65/// An Event
66#[derive(Debug)]
67pub enum Event {
68    /// A progress event
69    Progress(ProgressEvent),
70
71    /// The process exit status
72    ExitStatus(ExitStatus),
73
74    /// An unknown line
75    Unknown(String),
76}
77
78/// Get encoders that this ffmpeg supports
79pub async fn get_encoders() -> Result<Vec<Encoder>, Error> {
80    let output = tokio::process::Command::new("ffmpeg")
81        .arg("-hide_banner")
82        .arg("-encoders")
83        .output()
84        .await
85        .map_err(Error::Io)?;
86
87    if !output.status.success() {
88        return Err(Error::InvalidExitStatus(output.status));
89    }
90
91    let stdout_str = std::str::from_utf8(&output.stdout).map_err(Error::InvalidUtf8Str)?;
92    Ok(stdout_str
93        .lines()
94        .map(|line| line.trim())
95        .skip_while(|line| *line != "------")
96        .skip(1)
97        .map(Encoder::from_line)
98        .collect::<Result<_, _>>()?)
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use anyhow::Context;
105    use tokio_stream::StreamExt;
106
107    // https://ottverse.com/free-hls-m3u8-test-urls/
108    const SAMPLE_M3U8: &str =
109        "http://devimages.apple.com.edgekey.net/iphone/samples/bipbop/bipbopall.m3u8";
110
111    // TODO: Unignore
112    // This works on my dev machine.
113    // This works on my test machine.
114    // This works on my deployment machine.
115    // However, this fails on CI.
116    // The issue is parsing a progress event with out a frame key.
117    // I have no idea why this would ever happen.
118    #[tokio::test]
119    #[ignore]
120    async fn transcode_m3u8() -> anyhow::Result<()> {
121        let mut stream = Builder::new()
122            .audio_codec("copy")
123            .video_codec("copy")
124            .input(SAMPLE_M3U8)
125            .output("transcode_m3u8.mp4")
126            .overwrite(true)
127            .spawn()
128            .context("failed to spawn ffmpeg")?;
129
130        while let Some(maybe_event) = stream.next().await {
131            match maybe_event {
132                Ok(Event::Progress(event)) => {
133                    println!("Progress Event: {:#?}", event);
134                }
135                Ok(Event::ExitStatus(exit_status)) => {
136                    println!("FFMpeg exited: {:?}", exit_status);
137                }
138                Ok(Event::Unknown(line)) => {
139                    //  panic!("{:?}", event);
140                    dbg!(line);
141                }
142                Err(error) => {
143                    Err(error).context("stream error")?;
144                }
145            }
146        }
147
148        Ok(())
149    }
150
151    #[tokio::test]
152    #[ignore]
153    async fn reencode_m3u8() -> anyhow::Result<()> {
154        let mut stream = Builder::new()
155            .audio_codec("libopus")
156            .video_codec("vp9")
157            .input(SAMPLE_M3U8)
158            .output("reencode_m3u8.webm")
159            .overwrite(true)
160            .spawn()
161            .context("failed to spawn ffmpeg")?;
162
163        while let Some(maybe_event) = stream.next().await {
164            match maybe_event {
165                Ok(Event::Progress(event)) => {
166                    println!("Progress Event: {:#?}", event);
167                }
168                Ok(Event::ExitStatus(exit_status)) => {
169                    println!("FFMpeg exited: {:?}", exit_status);
170                }
171                Ok(Event::Unknown(line)) => {
172                    //  panic!("{:?}", event);
173                    dbg!(line);
174                }
175                Err(error) => {
176                    Err(error).context("stream error")?;
177                }
178            }
179        }
180
181        Ok(())
182    }
183
184    #[tokio::test]
185    async fn ffmpeg_get_encoders() -> anyhow::Result<()> {
186        let encoders = get_encoders().await.context("failed to get encoders")?;
187        dbg!(encoders);
188
189        Ok(())
190    }
191}