feat: add initial ffmpeg implementation

This commit is contained in:
uku 2025-05-13 14:22:03 +02:00
parent 1d93482209
commit 8f3c7e4052
Signed by: uku
SSH key fingerprint: SHA256:4P0aN6M8ajKukNi6aPOaX0LacanGYtlfjmN+m/sHY/o
6 changed files with 237 additions and 32 deletions

126
src/ffmpeg.rs Normal file
View file

@ -0,0 +1,126 @@
use std::{
path::{Path, PathBuf},
process::Stdio,
};
use color_eyre::eyre::{ContextCompat, Result};
use relm4::{
Sender,
tokio::{
self,
io::{AsyncBufReadExt, BufReader},
process::Command,
},
};
use crate::ProgressMessage;
#[derive(serde::Deserialize)]
struct FfprobeOut {
streams: Vec<StreamInfo>,
format: FormatInfo,
}
#[derive(serde::Deserialize)]
struct StreamInfo {
width: usize,
height: usize,
}
#[derive(serde::Deserialize)]
struct FormatInfo {
duration: String,
}
#[derive(Debug)]
pub struct VideoMeta {
pub width: usize,
pub height: usize,
pub duration_us: usize,
}
pub async fn get_video_meta(path: &Path) -> Result<VideoMeta> {
let output = Command::new("ffprobe")
.args([
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height : format=duration",
"-of",
"json",
])
.arg(path)
.stdout(Stdio::piped())
.output()
.await?;
let str = String::from_utf8(output.stdout)?;
let output: FfprobeOut = serde_json::from_str(&str)?;
let stream = output
.streams
.first()
.wrap_err("could not get stream information from ffprobe")?;
let duration_sec = output.format.duration.parse::<f64>()?;
let duration_us = (duration_sec * 1_000_000.0).ceil() as usize;
Ok(VideoMeta {
width: stream.width,
height: stream.height,
duration_us,
})
}
pub async fn convert_video(path: &Path, sender: Sender<ProgressMessage>) -> Result<PathBuf> {
let out_path = PathBuf::from("/tmp/out.webm");
let mut child = Command::new("ffmpeg")
.arg("-i")
.arg(path)
.args([
"-c:a",
"libopus",
"-b:a",
"96k",
"-c:v",
"libsvtav1",
"-loglevel",
"error",
"-progress",
"-",
"-nostats",
])
.arg(&out_path)
.stdout(Stdio::piped())
.spawn()?;
let stdout = child.stdout.take().unwrap();
let mut reader = BufReader::new(stdout).lines();
// make sure the process is actually started and awaited
tokio::spawn(async move {
child
.wait()
.await
.expect("ffmpeg process encountered an error");
});
while let Some(line) = reader.next_line().await? {
if line.starts_with("out_time_us") {
let (_, current_duration) = line
.split_once("=")
.wrap_err_with(|| format!("could not parse ffmpeg output: {line}"))?;
if current_duration != "N/A" {
sender.emit(ProgressMessage::AbsProgress(
current_duration.parse::<usize>()?,
));
}
}
}
Ok(out_path)
}