use std::fs;
use fs::File;
use std::env;
use std::path::Path;
use std::process::Command;
use std::io::Cursor;
use ffprobe::ffprobe;
use anyhow::{Context, Result};
use lazy_static::lazy_static;
use std::sync::Once;
lazy_static! {
static ref TRACING_INIT: Once = Once::new();
}
pub fn setup_logging() {
use tracing_subscriber::{EnvFilter, fmt, fmt::time::LocalTime, prelude::*};
use time::macros::format_description;
TRACING_INIT.call_once(|| {
let timer = LocalTime::new(format_description!("[hour]:[minute]:[second]"));
let fmt_layer = fmt::layer()
.compact()
.with_timer(timer)
.with_target(false);
let filter_layer = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info,reqwest=warn,hyper=warn,h2=warn,sqlx=warn"))
.expect("initializing logging");
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.init();
});
}
pub fn check_file_size_approx(p: &Path, expected: u64) {
let meta = fs::metadata(p).unwrap();
let ratio = meta.len() as f64 / expected as f64;
assert!(0.9 < ratio && ratio < 1.1, "File sizes: expected {expected}, got {}", meta.len());
}
pub fn check_media_duration(p: &Path, expected: f64) {
use serde_json::Value;
if let Ok(meta) = ffprobe(p) {
if let Some(video) = meta.streams.iter()
.find(|s| s.codec_type.eq(&Some(String::from("video"))))
{
if let Some(duration_str) = video.duration.as_ref() {
if let Ok(duration) = duration_str.parse::<f64>() {
let ratio = duration / expected;
assert!(0.9 < ratio && ratio < 1.1,
"Media duration: expected {expected}, got {duration}");
return;
}
}
}
}
let minfo = Command::new("mediainfo")
.arg("--output=JSON")
.arg(p)
.output()
.expect("failed to run mediainfo utility");
let out = String::from_utf8_lossy(&minfo.stdout);
let json: Value = serde_json::from_str(&out).expect("parsing mediainfo JSON");
let track0 = &json["media"]["track"][0];
if let Some(duration_str) = track0["Duration"].as_str() {
if let Ok(duration) = duration_str.parse::<f64>() {
let ratio = duration / expected;
assert!(0.9 < ratio && ratio < 1.1,
"Media duration: expected {expected}, got {duration}");
}
}
}
pub fn ffmpeg_approval(name: &Path) -> bool {
let ffmpeg = Command::new("ffmpeg")
.args(["-nostdin",
"-v", "error",
"-i", &name.to_string_lossy(),
"-f", "null", "-"])
.output()
.expect("spawning ffmpeg");
let msg = String::from_utf8_lossy(&ffmpeg.stderr);
if !msg.is_empty() {
println!("ffmpeg stderr: {msg}");
false
} else {
true
}
}
pub fn generate_minimal_mp4 () -> Vec<u8> {
let tmp = env::temp_dir().join("fragment.mp4");
let ffmpeg = Command::new("ffmpeg")
.args(["-f", "lavfi",
"-y", "-nostdin",
"-i", "testsrc=size=10x10:rate=1",
"-t", "4",
"-c:v", "libx264",
"-vf", "hue=s=0",
"-g", "52",
"-f", "mp4",
"-movflags", "frag_keyframe+empty_moov",
tmp.to_str().unwrap()])
.output()
.expect("spawning ffmpeg");
assert!(ffmpeg.status.success());
fs::read(tmp).unwrap()
}
pub fn generate_minimal_mp4_rust () -> Vec<u8> {
let config = mp4::Mp4Config {
major_brand: str::parse("isom").unwrap(),
minor_version: 512,
compatible_brands: vec![
str::parse("isom").unwrap(),
str::parse("iso2").unwrap(),
str::parse("avc1").unwrap(),
str::parse("mp41").unwrap(),
],
timescale: 60,
};
let data = Cursor::new(Vec::<u8>::new());
let mut writer = mp4::Mp4Writer::write_start(data, &config).unwrap();
let media_conf = mp4::MediaConfig::AvcConfig(mp4::AvcConfig {
width: 10,
height: 10,
seq_param_set: vec![
0, 0, 0, 0, ],
pic_param_set: vec![],
});
let track_conf = mp4::TrackConfig {
track_type: mp4::TrackType::Video,
timescale: 60,
language: "und".to_string(),
media_conf,
};
writer.add_track(&track_conf).unwrap();
let mut now = 0;
let sample1 = mp4::Mp4Sample {
start_time: now,
duration: 512,
rendering_offset: 0,
is_sync: true,
bytes: mp4::Bytes::from(vec![0x0u8; 751]),
};
now += 512;
writer.write_sample(1, &sample1).unwrap();
let sample2 = mp4::Mp4Sample {
start_time: now,
duration: 512,
rendering_offset: 0,
is_sync: true,
bytes: mp4::Bytes::from(vec![0x0u8; 179]),
};
now += 512;
writer.write_sample(1, &sample2).unwrap();
let sample3 = mp4::Mp4Sample {
start_time: now,
duration: 512,
rendering_offset: 0,
is_sync: true,
bytes: mp4::Bytes::from(vec![0x0u8; 180]),
};
now += 512;
writer.write_sample(1, &sample3).unwrap();
let sample4 = mp4::Mp4Sample {
start_time: now,
duration: 512,
rendering_offset: 0,
is_sync: true,
bytes: mp4::Bytes::from(vec![0x0u8; 160]),
};
writer.write_sample(1, &sample4).unwrap();
writer.write_end().unwrap();
writer.into_writer().into_inner()
}
pub fn generate_minimal_mp4_ffmpeg(metadata: &str) -> Vec<u8> {
let tmp = env::temp_dir().join("segment.mp4");
let ffmpeg = Command::new("ffmpeg")
.args(["-f", "lavfi",
"-y", "-nostdin",
"-i", "testsrc=size=10x10:rate=1",
"-c:v", "libx264",
"-vf", "hue=s=0",
"-t", "1",
"-metadata", metadata,
tmp.to_str().unwrap()])
.output()
.expect("spawning ffmpeg");
assert!(ffmpeg.status.success());
fs::read(tmp).unwrap()
}
pub fn ffprobe_metadata_title(mp4: &Path) -> Result<u8> {
let ffprobe = Command::new("ffprobe")
.args(["-loglevel", "error",
"-show_entries", "format_tags",
"-of", "json",
mp4.to_str().unwrap()])
.output()
.expect("spawning ffmpeg");
assert!(ffprobe.status.success());
let parsed = jzon::parse(&String::from_utf8_lossy(&ffprobe.stdout)).unwrap();
let title = parsed["format"]["tags"]["title"].as_str().unwrap();
title.parse().context("parsing title metadata")
}
pub fn curl(url: &str, output: &Path) -> Result<()> {
let mut response = reqwest::blocking::get(url)?;
let mut out = File::create(output)
.context("failed to create file")?;
std::io::copy(&mut response, &mut out)
.context("copying reqwest data to file")?;
Ok(())
}