#![cfg(feature = "ffmpeg-cli-tests")]
use assert_cmd::prelude::*;
use predicates::str;
use std::ffi::CString;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
use rsmpeg::avformat::AVFormatContextInput;
use rsmpeg::ffi;
fn ensure_ffmpeg_present() {
let out = Command::new("ffmpeg").arg("-version").output();
match out {
Ok(o) if o.status.success() => (),
_ => panic!("ffmpeg CLI not found. Install ffmpeg and ensure it is on PATH."),
}
}
fn mk_subs_file(path: &Path) {
let mut f = File::create(path).expect("create srt");
writeln!(
f,
"1\n00:00:00,000 --> 00:00:00,800\nvobsub line\n\n2\n00:00:01,000 --> 00:00:01,600\nsecond line\n"
)
.unwrap();
}
fn gen_problem_input_with_vobsub(tmp: &TempDir) -> (PathBuf, u64) {
let dir = tmp.path();
let video = dir.join("v.mkv");
let audio = dir.join("a.mp2");
let subs = dir.join("subs.srt");
let input = dir.join("input_vobsub.mkv");
mk_subs_file(&subs);
let status_v = Command::new("ffmpeg")
.args([
"-y",
"-f",
"lavfi",
"-i",
"testsrc=size=160x120:rate=25:duration=2",
"-pix_fmt",
"yuv420p",
"-c:v",
"mpeg4",
&video.to_string_lossy(),
])
.status()
.expect("run ffmpeg video");
assert!(status_v.success(), "ffmpeg video generation failed");
let status_a = Command::new("ffmpeg")
.args([
"-y",
"-f",
"lavfi",
"-i",
"sine=frequency=1000:sample_rate=44100:duration=2",
"-c:a",
"mp2",
&audio.to_string_lossy(),
])
.status()
.expect("run ffmpeg audio");
assert!(status_a.success(), "ffmpeg audio generation failed");
let candidates = ["dvd_subtitle", "dvb_subtitle", "hdmv_pgs_subtitle"];
let mut ok = false;
for codec in candidates {
let status_mux = Command::new("ffmpeg")
.args([
"-y",
"-i",
&video.to_string_lossy(),
"-i",
&audio.to_string_lossy(),
"-i",
&subs.to_string_lossy(),
"-c:v",
"copy",
"-c:a",
"copy",
"-c:s",
codec,
"-map",
"0:v:0",
"-map",
"1:a:0",
"-map",
"2:0",
&input.to_string_lossy(),
])
.status()
.expect("run ffmpeg mux vobsub");
if status_mux.success() {
ok = true;
break;
}
}
if !ok {
let status_text = Command::new("ffmpeg")
.args([
"-y",
"-i",
&video.to_string_lossy(),
"-i",
&audio.to_string_lossy(),
"-i",
&subs.to_string_lossy(),
"-c:v",
"copy",
"-c:a",
"copy",
"-c:s",
"ass",
"-map",
"0:v:0",
"-map",
"1:a:0",
"-map",
"2:0",
&input.to_string_lossy(),
])
.status()
.expect("run ffmpeg mux ass");
assert!(
status_text.success(),
"ffmpeg mux with VobSub/PGS and ASS fallback failed"
);
}
let input_cstr = CString::new(input.to_string_lossy().to_string()).unwrap();
let ictx = AVFormatContextInput::open(input_cstr.as_c_str()).unwrap();
let dur_ms = (ictx.duration / 1000).max(0) as u64;
(input, dur_ms)
}
fn probe_duration_ms(path: &Path) -> u64 {
let path_cstr = CString::new(path.to_string_lossy().to_string()).unwrap();
let ictx = AVFormatContextInput::open(path_cstr.as_c_str()).unwrap();
(ictx.duration / 1000).max(0) as u64
}
#[test]
fn cli_converts_vobsub_to_mov_text_and_direct_play() -> Result<(), Box<dyn std::error::Error>> {
ensure_ffmpeg_present();
let tmp = TempDir::new()?;
let (input, in_dur_ms) = gen_problem_input_with_vobsub(&tmp);
let output = tmp.path().join("out_vobsub.mp4");
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("direct_play_nice"));
cmd.arg("-s")
.arg("chromecast_1st_gen,chromecast_2nd_gen,chromecast_ultra")
.arg(&input)
.arg(&output);
cmd.assert().success().stdout(str::is_empty());
assert!(output.exists(), "output file was not created");
let output_cstr = CString::new(output.to_string_lossy().to_string()).unwrap();
let octx = AVFormatContextInput::open(output_cstr.as_c_str())?;
let mut saw_v = false;
let mut saw_a = false;
let mut saw_s = false;
let mut width = 0i32;
let mut height = 0i32;
let mut fps_num = 0i32;
let mut fps_den = 1i32;
let mut level = 0i32;
let mut pix_fmt = -1i32;
for st in octx.streams() {
let par = st.codecpar();
match par.codec_type {
t if t == ffi::AVMEDIA_TYPE_VIDEO => {
saw_v = true;
assert_eq!(par.codec_id, ffi::AV_CODEC_ID_H264, "video must be H.264");
width = par.width;
height = par.height;
level = par.level;
pix_fmt = par.format;
let rate = st.avg_frame_rate;
fps_num = rate.num;
fps_den = rate.den;
}
t if t == ffi::AVMEDIA_TYPE_AUDIO => {
saw_a = true;
assert_eq!(par.codec_id, ffi::AV_CODEC_ID_AAC, "audio must be AAC");
}
t if t == ffi::AVMEDIA_TYPE_SUBTITLE => {
saw_s = true;
assert_eq!(
par.codec_id,
ffi::AV_CODEC_ID_MOV_TEXT,
"subs must be MOV_TEXT"
);
}
_ => {}
}
}
assert!(
saw_v && saw_a && saw_s,
"missing one or more required streams"
);
assert!(
width as u32 <= 1920 && height as u32 <= 1080,
"resolution too high"
);
assert!(level <= 41, "H.264 level too high: {}", level);
assert_eq!(pix_fmt, ffi::AV_PIX_FMT_YUV420P, "pix fmt must be yuv420p");
if fps_den != 0 {
let fps = (fps_num as f64) / (fps_den as f64);
assert!(fps <= 30.01, "fps too high: {}", fps);
}
let out_dur_ms = probe_duration_ms(&output);
let diff = out_dur_ms.abs_diff(in_dur_ms);
assert!(
diff <= 200,
"duration drift too large: in={}ms out={}ms",
in_dur_ms,
out_dur_ms
);
Ok(())
}