1#![allow(clippy::uninlined_format_args)]
2
3mod progress_event;
5
6mod builder;
8
9mod 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#[derive(Debug, thiserror::Error)]
27pub enum Error {
28 #[error("failed to spawn a process")]
30 ProcessSpawn(#[source] std::io::Error),
31
32 #[error("missing input file")]
34 MissingInput,
35
36 #[error("missing output file")]
38 MissingOutput,
39
40 #[error("io error")]
42 Io(#[source] std::io::Error),
43
44 #[error("output file already exists")]
46 OutputAlreadyExists,
47
48 #[error("invalid progress event")]
50 InvalidProgressEvent(#[from] crate::progress_event::LineBuilderError),
51
52 #[error("invalid exit status '{0}'")]
54 InvalidExitStatus(ExitStatus),
55
56 #[error(transparent)]
58 InvalidUtf8Str(std::str::Utf8Error),
59
60 #[error("failed to parse encoder line")]
62 InvalidEncoderLine(#[from] EncoderFromLineError),
63}
64
65#[derive(Debug)]
67pub enum Event {
68 Progress(ProgressEvent),
70
71 ExitStatus(ExitStatus),
73
74 Unknown(String),
76}
77
78pub 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 const SAMPLE_M3U8: &str =
109 "http://devimages.apple.com.edgekey.net/iphone/samples/bipbop/bipbopall.m3u8";
110
111 #[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 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 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}