#[macro_use]
extern crate lazy_static;
pub mod common;
use fs_err as fs;
use std::env;
use std::process::Command;
use std::path::{Path, PathBuf};
use ffprobe::ffprobe;
use file_format::FileFormat;
use test_log::test;
use common::check_file_size_approx;
lazy_static! {
static ref TMP: PathBuf = tempfile::TempDir::new().unwrap().keep();
}
fn container_run(args: Vec<&str>) {
let vspec = format!("{}:/content", TMP.display());
let mut cargs = vec!["run", "--rm",
"--pull=newer",
"--volume", &vspec,
"ghcr.io/emarsden/dash-mpd-cli"];
for a in &args {
cargs.push(a);
}
println!("CTR> {:?}", cargs);
let mut docker_exe = String::from("podman");
if let Ok(docker) = env::var("DOCKER") {
docker_exe = docker;
}
let cli = Command::new(docker_exe)
.args(cargs)
.output()
.expect("failed spawning podman");
if !cli.status.success() {
let stdout = String::from_utf8_lossy(&cli.stdout);
if !stdout.is_empty() {
println!("Podman stdout> {stdout}");
}
let stderr = String::from_utf8_lossy(&cli.stderr);
if !stderr.is_empty() {
println!("Podman stderr> {stderr}");
}
}
assert!(cli.status.success());
}
fn container_metadata_encoder(p: &Path) -> Option<String> {
if let Ok(meta) = ffprobe(p) {
if let Some(tags) = meta.format.tags {
if let Some(enc) = tags.encoder {
return Some(enc);
}
}
}
if let Ok(minf) = Command::new("mediainfo")
.args(["--Output=JSON", &p.to_string_lossy()])
.output()
{
let json = String::from_utf8_lossy(&minf.stdout);
if let Ok(j) = jzon::parse(&json) {
if let Some(ea) = j["media"]["track"][0]["Encoded_Application"].as_str() {
return Some(String::from(ea));
}
}
}
None
}
#[test]
#[cfg(not(target_os = "macos"))]
fn test_container_mp4 () {
let mpd = "https://cloudflarestream.com/31c9291ab41fac05471db4e73aa11717/manifest/video.mpd";
let out = Path::new("cf.mp4");
let outpath = TMP.join(out);
container_run(vec!["-o", &out.to_string_lossy(), "--quality", "worst", mpd]);
check_file_size_approx(&outpath, 410_218);
let format = FileFormat::from_file(&outpath).unwrap();
assert_eq!(format, FileFormat::Mpeg4Part14Video);
let _ = fs::remove_file(outpath);
}
#[test]
fn test_container_mp4a () {
if env::var("CI").is_ok() {
return;
}
let mpd = "https://dash.akamaized.net/dash264/TestCases/3a/fraunhofer/aac-lc_stereo_without_video/Sintel/sintel_audio_only_aaclc_stereo_sidx.mpd";
let out = Path::new("sintel-audio.mp4");
let outpath = TMP.join(out);
if outpath.exists() {
let _ = fs::remove_file(&outpath);
}
container_run(vec!["-o", &out.to_string_lossy(), "--quality", "worst", mpd]);
check_file_size_approx(&outpath, 7_456_334);
let format = FileFormat::from_file(&outpath).unwrap();
assert_eq!(format, FileFormat::Mpeg4Part14Audio);
let meta = ffprobe(&outpath).unwrap();
assert_eq!(meta.streams.len(), 1);
let audio = &meta.streams[0];
assert_eq!(audio.codec_type, Some(String::from("audio")));
assert_eq!(audio.codec_name, Some(String::from("aac")));
assert!(audio.width.is_none());
let _ = fs::remove_file(outpath);
}
#[test]
fn test_container_audio_flac () {
if env::var("CI").is_ok() {
return;
}
let mpd = "http://rdmedia.bbc.co.uk/testcard/vod/manifests/radio-flac-en.mpd";
let out = Path::new("bbcradio-flac.mp4");
let outpath = TMP.join(out);
if outpath.exists() {
let _ = fs::remove_file(&outpath);
}
container_run(vec!["-o", &out.to_string_lossy(), "--quality", "worst", mpd]);
check_file_size_approx(&outpath, 81_603_640);
let format = FileFormat::from_file(&outpath).unwrap();
assert_eq!(format, FileFormat::Mpeg4Part14Audio);
let meta = ffprobe(&outpath).unwrap();
assert_eq!(meta.streams.len(), 1);
let audio = &meta.streams[0];
assert_eq!(audio.codec_type, Some(String::from("audio")));
assert_eq!(audio.codec_name, Some(String::from("flac")));
assert!(audio.width.is_none());
let _ = fs::remove_file(outpath);
}
#[test]
fn test_container_subtitles_wvtt () {
if env::var("CI").is_ok() {
return;
}
let mpd = "https://storage.googleapis.com/shaka-demo-assets/sintel-mp4-wvtt/dash.mpd";
let out = Path::new("sintel-wvtt.mp4");
let outpath = TMP.join(out);
if outpath.exists() {
let _ = fs::remove_file(&outpath);
}
let mut subpath = outpath.clone();
subpath.set_extension("srt");
let subpath = Path::new(&subpath);
container_run(vec!["-v", "-v",
"-o", &out.to_string_lossy(),
"--quality", "worst",
"--write-subs", mpd]);
check_file_size_approx(&outpath, 25_950_458);
assert!(fs::metadata(subpath).is_ok());
let srt = fs::read_to_string(subpath).unwrap();
assert!(srt.contains("land van de poortwachters"));
let _ = fs::remove_file(&outpath);
container_run(vec!["-o", &out.to_string_lossy(),
"--quality", "worst",
"--write-subs", "--prefer-language", "eng", mpd]);
let srt = fs::read_to_string(subpath).unwrap();
assert!(srt.contains("land of the gatekeepers"));
let _ = fs::remove_file(outpath);
}
#[test]
fn test_container_subtitles_ttml () {
if env::var("CI").is_ok() {
return;
}
let mpd = "https://dash.akamaized.net/dash264/TestCases/4b/qualcomm/2/TearsOfSteel_onDem5secSegSubTitles.mpd";
let out = Path::new("tears-of-steel-ttml.mp4");
let outpath = TMP.join(out);
if outpath.exists() {
let _ = fs::remove_file(&outpath);
}
let mut subpath = outpath.clone();
subpath.set_extension("ttml");
let subpath = Path::new(&subpath);
container_run(vec!["-v",
"-o", &out.to_string_lossy(),
"--quality", "worst",
"--write-subs",
mpd]);
check_file_size_approx(&outpath, 46_299_053);
assert!(fs::metadata(subpath).is_ok());
let ttml = fs::read_to_string(subpath).unwrap();
assert!(ttml.contains("You're a jerk"));
let _ = fs::remove_file(&outpath);
let _ = fs::remove_file(subpath);
container_run(vec!["-o", &out.to_string_lossy(),
"--quality", "worst",
"--write-subs",
"--prefer-language", "de", mpd]);
let ttml = fs::read_to_string(subpath).unwrap();
assert!(ttml.contains("Du bist ein Vollidiot"));
let _ = fs::remove_file(outpath);
let _ = fs::remove_file(subpath);
}
#[test]
fn test_container_subtitles_vtt () {
if env::var("CI").is_ok() {
return;
}
let mpd = "http://dash.edgesuite.net/akamai/test/caption_test/ElephantsDream/elephants_dream_480p_heaac5_1.mpd";
let out = Path::new("elephants-dream.mp4");
let outpath = TMP.join(out);
if outpath.exists() {
let _ = fs::remove_file(&outpath);
}
let mut subpath = outpath.clone();
subpath.set_extension("vtt");
let subpath = Path::new(&subpath);
container_run(vec!["-o", &out.to_string_lossy(), "--quality", "worst", "--write-subs", mpd]);
check_file_size_approx(&outpath, 128_768_482);
assert!(fs::metadata(subpath).is_ok());
let vtt = fs::read_to_string(subpath).unwrap();
assert!(vtt.contains("Hurry Emo!"));
let _ = fs::remove_file(outpath);
}
#[test]
fn test_container_decryption_playready_cenc () {
if env::var("CI").is_ok() {
return;
}
let mpd = "https://refapp.hbbtv.org/videos/00_llama_h264_v8_8s/cenc/manifest_prcenc.mpd";
let out = Path::new("llama.mp4");
let outpath = TMP.join(out);
if outpath.exists() {
let _ = fs::remove_file(&outpath);
}
container_run(vec!["-v", "-o", &out.to_string_lossy(),
"--quality", "worst",
"--key", "43215678123412341234123412341236:12341234123412341234123412341236",
mpd]);
check_file_size_approx(&outpath, 26_420_624);
let _ = fs::remove_file(outpath);
}
#[test]
fn test_container_decryption_marlin_cenc () {
if env::var("CI").is_ok() {
return;
}
let mpd = "https://refapp.hbbtv.org/videos/agent327_h264_v8/cenc/manifest_mlcenc.mpd";
let out = Path::new("llama-cenc.mp4");
let outpath = TMP.join(out);
if outpath.exists() {
let _ = fs::remove_file(&outpath);
}
container_run(vec!["-v", "-o", &out.to_string_lossy(),
"--quality", "worst",
"--key", "43215678123412341234123412341234:12341234123412341234123412341234",
mpd]);
check_file_size_approx(&outpath, 14_357_917);
let _ = fs::remove_file(outpath);
}
#[test]
#[cfg(not(target_os = "macos"))]
fn test_container_decryption_webm() {
let mpd = "https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/dash.mpd";
let out = Path::new("angel.webm");
let outpath = TMP.join(out);
if outpath.exists() {
let _ = fs::remove_file(&outpath);
}
container_run(vec!["-v", "-o", &out.to_string_lossy(),
"--quality", "worst",
"--decryption-application", "shaka",
"--key", "4d97930a3d7b55fa81d0028653f5e499:429ec76475e7a952d224d8ef867f12b6",
"--key", "d21373c0b8ab5ba9954742bcdfb5f48b:150a6c7d7dee6a91b74dccfce5b31928",
"--key", "6f1729072b4a5cd288c916e11846b89e:a84b4bd66901874556093454c075e2c6",
"--key", "800aacaa522958ae888062b5695db6bf:775dbf7289c4cc5847becd571f536ff2",
"--key", "67b30c86756f57c5a0a38a23ac8c9178:efa2878c2ccf6dd47ab349fcf90e6259",
mpd]);
check_file_size_approx(&outpath, 1_331_284);
let meta = ffprobe(&outpath).unwrap();
assert_eq!(meta.streams.len(), 2);
let audio = meta.streams.iter()
.find(|s| s.codec_type.eq(&Some(String::from("audio"))))
.expect("finding audio stream");
assert_eq!(audio.codec_name, Some(String::from("opus")));
let video = meta.streams.iter()
.find(|s| s.codec_type.eq(&Some(String::from("video"))))
.expect("finding video stream");
assert_eq!(video.codec_name, Some(String::from("vp9")));
assert!(video.width.is_some());
let ffmpeg = Command::new("ffmpeg")
.args(["-nostdin",
"-v", "error",
"-i", &outpath.to_string_lossy(),
"-f", "null", "-"])
.output()
.expect("spawning ffmpeg");
let msg = String::from_utf8_lossy(&ffmpeg.stderr);
if !msg.is_empty() {
eprintln!("FFMPEG stderr {msg}");
}
let _ = fs::remove_file(outpath);
}
#[test]
#[cfg(not(target_os = "macos"))]
fn test_container_decryption_small_shaka () {
let mpd = "https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/dash.mpd";
let out = Path::new("angel.mp4");
let outpath = TMP.join(out);
if outpath.exists() {
let _ = fs::remove_file(&outpath);
}
container_run(vec!["-v", "-o", &out.to_string_lossy(),
"--quality", "worst",
"--decryption-application", "shaka",
"--key", "4d97930a3d7b55fa81d0028653f5e499:429ec76475e7a952d224d8ef867f12b6",
"--key", "d21373c0b8ab5ba9954742bcdfb5f48b:150a6c7d7dee6a91b74dccfce5b31928",
"--key", "6f1729072b4a5cd288c916e11846b89e:a84b4bd66901874556093454c075e2c6",
"--key", "800aacaa522958ae888062b5695db6bf:775dbf7289c4cc5847becd571f536ff2",
"--key", "67b30c86756f57c5a0a38a23ac8c9178:efa2878c2ccf6dd47ab349fcf90e6259",
mpd]);
check_file_size_approx(&outpath, 1_316_391);
let _ = fs::remove_file(outpath);
}
#[test]
fn test_container_muxers_mkv () {
if env::var("CI").is_ok() {
return;
}
let mpd = "http://refapp.hbbtv.org/videos/01_llama_drama_2160p_25f75g6sv3/manifest.mpd";
let out = Path::new("llama.mkv");
let outpath = TMP.join(out);
if outpath.exists() {
let _ = fs::remove_file(&outpath);
}
container_run(vec!["-v", "-o", &out.to_string_lossy(),
"--quality", "worst",
"--muxer-preference", "mkv:ffmpeg",
mpd]);
check_file_size_approx(&outpath, 6_652_846);
let format = FileFormat::from_file(&outpath).unwrap();
assert_eq!(format, FileFormat::MatroskaVideo);
if let Some(enc) = container_metadata_encoder(&outpath) {
assert!(enc.starts_with("Lavf"), "Unexpected encoder {enc} in mkv metadata");
}
let _ = fs::remove_file(outpath);
let out = Path::new("angel-mkvmerge.mkv");
let outpath = TMP.join(out);
container_run(vec!["-v", "-o", &out.to_string_lossy(),
"--quality", "worst",
"--muxer-preference", "mkv:mkvmerge",
mpd]);
check_file_size_approx(&outpath, 6_652_846);
let format = FileFormat::from_file(&outpath).unwrap();
assert_eq!(format, FileFormat::MatroskaVideo);
if let Some(enc) = container_metadata_encoder(&outpath) {
assert!(enc.contains("libmatroska"), "Unexpected encoder {enc} in mkv metadata");
}
let _ = fs::remove_file(outpath);
}
#[test]
fn test_container_xslt_multiple_stylesheets() {
if env::var("CI").is_ok() {
return;
}
let mpd = "http://dash.edgesuite.net/envivio/dashpr/clear/Manifest.mpd";
let out = Path::new("ricked-cleaned.mp4");
let outpath = TMP.join(out);
if outpath.exists() {
let _ = fs::remove_file(&outpath);
}
let mut xslt_rick = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
xslt_rick.push("tests");
xslt_rick.push("fixtures");
xslt_rick.push("rewrite-rickroll");
xslt_rick.set_extension("xslt");
let mut xslt_clean = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
xslt_clean.push("tests");
xslt_clean.push("fixtures");
xslt_clean.push("rewrite-drop-dai");
xslt_clean.set_extension("xslt");
let s1 = TMP.join("s1.xslt");
let s2 = TMP.join("s2.xslt");
fs::copy(xslt_rick, &s1).unwrap();
fs::copy(xslt_clean, &s2).unwrap();
container_run(vec!["-v", "-o", &out.to_string_lossy(),
"--quality", "worst",
"--xslt-stylesheet", "s1.xslt",
"--xslt-stylesheet", "s2.xslt",
mpd]);
check_file_size_approx(&outpath, 12_975_377);
let format = FileFormat::from_file(&outpath).unwrap();
assert_eq!(format, FileFormat::Mpeg4Part14Video);
let meta = ffprobe(&outpath).unwrap();
assert_eq!(meta.streams.len(), 2);
let video = &meta.streams[0];
assert_eq!(video.codec_type, Some(String::from("video")));
assert_eq!(video.codec_name, Some(String::from("h264")));
assert_eq!(video.width, Some(320));
let _ = fs::remove_file(outpath);
}