direct_play_nice 0.1.0-beta.3

CLI program that converts video files to direct-play-compatible formats.
Documentation
#![cfg(feature = "ffmpeg-cli-tests")]

#[path = "common/mod.rs"]
mod common;

use assert_cmd::prelude::*;
use predicates::str;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;

fn mk_subs_file(path: &Path) {
    let mut f = fs::File::create(path).expect("create srt");
    use std::io::Write;
    writeln!(
        f,
        "1\n00:00:00,200 --> 00:00:00,800\nhello bitmap\n\n2\n00:00:01,200 --> 00:00:01,900\nsecond line\n"
    )
    .unwrap();
}

fn gen_bitmap_input(tmp: &TempDir) -> Option<PathBuf> {
    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_bitmap.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"];
    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 bitmap subs");
        if status_mux.success() {
            return Some(input);
        }
    }

    None
}

#[test]
fn test_ass_formatting() {
    let text = "Fully Featured!";
    let x = 960;
    let y = 1000;
    let formatted = format!("{{\\pos({},{})}}{}", x, y, text);
    assert_eq!(formatted, "{\\pos(960,1000)}Fully Featured!");
}

#[test]
#[ignore]
fn test_e2e_pgs_to_ass_conversion() -> Result<(), Box<dyn std::error::Error>> {
    common::ensure_ffmpeg_present();

    let tmp = TempDir::new()?;
    let Some(input) = gen_bitmap_input(&tmp) else {
        eprintln!("No bitmap subtitle encoder available; skipping e2e OCR test.");
        return Ok(());
    };

    let output = tmp.path().join("out_ocr.mkv");
    let ass_out = tmp.path().join("out_ocr.ass");

    let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("direct_play_nice"));
    cmd.arg("--sub-mode")
        .arg("force")
        .arg("--ocr-engine")
        .arg("external")
        .arg("--ocr-format")
        .arg("ass")
        .arg("--ocr-external-command")
        .arg("echo hello bitmap")
        .arg(&input)
        .arg(&output);
    cmd.assert().success().stdout(str::is_empty());

    assert!(output.exists(), "output file was not created");

    let output_probe = Command::new("ffprobe")
        .args([
            "-v",
            "error",
            "-select_streams",
            "s",
            "-show_entries",
            "stream=codec_name",
            "-of",
            "csv=p=0",
            &output.to_string_lossy(),
        ])
        .output()?;
    assert!(
        output_probe.status.success(),
        "ffprobe failed: {}",
        String::from_utf8_lossy(&output_probe.stderr)
    );
    let codec_name = String::from_utf8(output_probe.stdout)?
        .lines()
        .next()
        .unwrap_or("")
        .trim()
        .to_string();
    assert_eq!(codec_name, "ass");

    let status_extract = Command::new("ffmpeg")
        .args([
            "-y",
            "-i",
            &output.to_string_lossy(),
            "-map",
            "0:s:0",
            &ass_out.to_string_lossy(),
        ])
        .status()
        .expect("run ffmpeg extract ass");
    assert!(status_extract.success(), "ffmpeg subtitle extract failed");

    let ass_text = fs::read_to_string(&ass_out)?;
    assert!(ass_text.contains("hello bitmap"));

    Ok(())
}