use crate::error::RecordError;
use std::io::Write;
use std::os::windows::process::CommandExt;
use std::path::Path;
use std::process::{Child, ChildStdin, Command, Stdio};
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
fn ffmpeg_command() -> Command {
let mut cmd = Command::new("ffmpeg");
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP);
cmd
}
pub(crate) fn encode_args(width: u32, height: u32, fps: u32, out: &str) -> Vec<String> {
vec![
"-hide_banner".into(),
"-loglevel".into(),
"warning".into(),
"-y".into(),
"-f".into(),
"rawvideo".into(),
"-pixel_format".into(),
"bgra".into(),
"-video_size".into(),
format!("{width}x{height}"),
"-framerate".into(),
fps.to_string(),
"-i".into(),
"pipe:0".into(),
"-an".into(),
"-c:v".into(),
"libx264".into(),
"-preset".into(),
"veryfast".into(),
"-pix_fmt".into(),
"yuv420p".into(),
"-movflags".into(),
"+faststart".into(),
out.into(),
]
}
pub(crate) fn mux_args(audio: &str, video: &str, audio_offset_s: f64, out: &str) -> Vec<String> {
vec![
"-hide_banner".into(),
"-loglevel".into(),
"warning".into(),
"-y".into(),
"-itsoffset".into(),
format!("{audio_offset_s:.3}"),
"-i".into(),
audio.into(),
"-i".into(),
video.into(),
"-map".into(),
"1:v:0".into(),
"-map".into(),
"0:a:0".into(),
"-c:v".into(),
"copy".into(),
"-c:a".into(),
"aac".into(),
"-b:a".into(),
"128k".into(),
"-movflags".into(),
"+faststart".into(),
"-shortest".into(),
out.into(),
]
}
pub(crate) fn ffmpeg_available() -> bool {
Command::new("ffmpeg")
.arg("-version")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn spawn_err(e: std::io::Error) -> RecordError {
if e.kind() == std::io::ErrorKind::NotFound {
RecordError::FfmpegNotFound
} else {
RecordError::Io(e)
}
}
pub(crate) struct VideoEncoder {
child: Child,
stdin: Option<ChildStdin>,
}
impl VideoEncoder {
pub(crate) fn spawn(
width: u32,
height: u32,
fps: u32,
out: &Path,
) -> Result<Self, RecordError> {
let out = out.to_string_lossy().into_owned();
let mut child = ffmpeg_command()
.args(encode_args(width, height, fps, &out))
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.map_err(spawn_err)?;
let stdin = child.stdin.take();
Ok(Self { child, stdin })
}
pub(crate) fn write_frame(&mut self, bytes: &[u8]) -> std::io::Result<()> {
match self.stdin.as_mut() {
Some(w) => w.write_all(bytes),
None => Ok(()),
}
}
pub(crate) fn finish(mut self) -> Result<(), RecordError> {
drop(self.stdin.take());
let status = self.child.wait()?;
if status.success() {
Ok(())
} else {
Err(RecordError::FfmpegFailed(status))
}
}
}
pub(crate) fn run_mux(
audio: &Path,
video: &Path,
audio_offset_s: f64,
out: &Path,
) -> Result<(), RecordError> {
let status = ffmpeg_command()
.args(mux_args(
&audio.to_string_lossy(),
&video.to_string_lossy(),
audio_offset_s,
&out.to_string_lossy(),
))
.stdin(Stdio::null())
.stderr(Stdio::inherit())
.status()
.map_err(spawn_err)?;
if status.success() {
Ok(())
} else {
Err(RecordError::FfmpegFailed(status))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_args_declare_cfr_bgra_input() {
let a = encode_args(1920, 1080, 30, "out.mp4");
let i = a.iter().position(|x| x == "-i").unwrap();
assert!(a[..i].contains(&"rawvideo".to_string()));
assert!(a[..i].contains(&"bgra".to_string()));
assert!(a[..i].contains(&"1920x1080".to_string()));
assert!(a[..i].contains(&"30".to_string()));
assert!(a.contains(&"libx264".to_string()));
assert!(a.contains(&"yuv420p".to_string()));
assert!(a.contains(&"+faststart".to_string()));
assert_eq!(a.last().unwrap(), "out.mp4");
}
#[test]
fn mux_args_apply_offset_and_copy_video() {
let a = mux_args("a.wav", "v.mp4", 0.25, "final.mp4");
let off = a.iter().position(|x| x == "-itsoffset").unwrap();
assert_eq!(a[off + 1], "0.250");
assert!(a.windows(2).any(|w| w == ["-map", "1:v:0"]));
assert!(a.windows(2).any(|w| w == ["-map", "0:a:0"]));
assert!(a.windows(2).any(|w| w == ["-c:v", "copy"]));
assert!(a.contains(&"-shortest".to_string()));
assert_eq!(a.last().unwrap(), "final.mp4");
}
#[test]
fn mux_args_format_negative_offset() {
let a = mux_args("a.wav", "v.mp4", -0.02, "final.mp4");
let off = a.iter().position(|x| x == "-itsoffset").unwrap();
assert_eq!(a[off + 1], "-0.020");
}
}