#![allow(clippy::uninlined_format_args)]
mod progress_event;
mod builder;
mod encoder;
pub use self::{
builder::Builder,
encoder::{
Encoder,
FromLineError as EncoderFromLineError,
},
progress_event::{
LineBuilderError,
ProgressEvent,
},
};
use std::process::ExitStatus;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("failed to spawn a process")]
ProcessSpawn(#[source] std::io::Error),
#[error("missing input file")]
MissingInput,
#[error("missing output file")]
MissingOutput,
#[error("io error")]
Io(#[source] std::io::Error),
#[error("output file already exists")]
OutputAlreadyExists,
#[error("invalid progress event")]
InvalidProgressEvent(#[from] crate::progress_event::LineBuilderError),
#[error("invalid exit status '{0}'")]
InvalidExitStatus(ExitStatus),
#[error(transparent)]
InvalidUtf8Str(std::str::Utf8Error),
#[error("failed to parse encoder line")]
InvalidEncoderLine(#[from] EncoderFromLineError),
}
#[derive(Debug)]
pub enum Event {
Progress(ProgressEvent),
ExitStatus(ExitStatus),
Unknown(String),
}
pub async fn get_encoders() -> Result<Vec<Encoder>, Error> {
let output = tokio::process::Command::new("ffmpeg")
.arg("-hide_banner")
.arg("-encoders")
.output()
.await
.map_err(Error::Io)?;
if !output.status.success() {
return Err(Error::InvalidExitStatus(output.status));
}
let stdout_str = std::str::from_utf8(&output.stdout).map_err(Error::InvalidUtf8Str)?;
Ok(stdout_str
.lines()
.map(|line| line.trim())
.skip_while(|line| *line != "------")
.skip(1)
.map(Encoder::from_line)
.collect::<Result<_, _>>()?)
}
#[cfg(test)]
mod tests {
use super::*;
use tokio_stream::StreamExt;
const SAMPLE_M3U8: &str =
"https://devimages.apple.com.edgekey.net/iphone/samples/bipbop/bipbopall.m3u8";
#[tokio::test]
async fn transcode_m3u8() {
let mut stream = Builder::new()
.audio_codec("copy")
.video_codec("copy")
.input(SAMPLE_M3U8)
.output("transcode_m3u8.mp4")
.overwrite(true)
.spawn()
.expect("failed to spawn ffmpeg");
while let Some(maybe_event) = stream.next().await {
match maybe_event {
Ok(Event::Progress(event)) => {
println!("Progress Event: {:#?}", event);
}
Ok(Event::ExitStatus(exit_status)) => {
println!("FFMpeg exited: {:?}", exit_status);
}
Ok(Event::Unknown(line)) => {
dbg!(line);
}
Err(e) => {
panic!("Error: {}", e);
}
}
}
}
#[tokio::test]
#[ignore]
async fn reencode_m3u8() {
let mut stream = Builder::new()
.audio_codec("libopus")
.video_codec("vp9")
.input(SAMPLE_M3U8)
.output("reencode_m3u8.webm")
.overwrite(true)
.spawn()
.expect("failed to spawn ffmpeg");
while let Some(maybe_event) = stream.next().await {
match maybe_event {
Ok(Event::Progress(event)) => {
println!("Progress Event: {:#?}", event);
}
Ok(Event::ExitStatus(exit_status)) => {
println!("FFMpeg exited: {:?}", exit_status);
}
Ok(Event::Unknown(line)) => {
dbg!(line);
}
Err(e) => {
panic!("Error: {}", e);
}
}
}
}
#[tokio::test]
async fn ffmpeg_get_encoders() {
let encoders = get_encoders().await.expect("failed to get encoders");
dbg!(encoders);
}
}