lesavka/client/src/bin/lesavka-sync-analyze.rs

126 lines
3.9 KiB
Rust
Raw Normal View History

#[cfg(any(not(coverage), test))]
use anyhow::{Context, Result, bail};
#[cfg(not(coverage))]
use serde::Serialize;
#[cfg(not(coverage))]
use lesavka_client::sync_probe::analyze::{
SyncAnalysisOptions, SyncAnalysisReport, SyncCalibrationRecommendation, analyze_capture,
};
#[cfg(not(coverage))]
#[derive(Serialize)]
struct SyncAnalyzeOutput<'a> {
#[serde(flatten)]
report: &'a SyncAnalysisReport,
calibration: SyncCalibrationRecommendation,
}
#[cfg(not(coverage))]
fn main() -> Result<()> {
let (capture_path, emit_json) = parse_args(std::env::args().skip(1))?;
let report = analyze_capture(&capture_path, &SyncAnalysisOptions::default())
.with_context(|| format!("analyzing sync capture {}", capture_path.display()))?;
let calibration = report.calibration_recommendation();
if emit_json {
let output = SyncAnalyzeOutput {
report: &report,
calibration,
};
println!(
"{}",
serde_json::to_string_pretty(&output).context("serializing JSON report")?
);
} else {
println!("A/V sync report for {}", capture_path.display());
println!("- video onsets: {}", report.video_event_count);
println!("- audio onsets: {}", report.audio_event_count);
println!("- paired pulses: {}", report.paired_event_count);
println!(
"- first skew: {:+.1} ms (audio after video is positive)",
report.first_skew_ms
);
println!("- last skew: {:+.1} ms", report.last_skew_ms);
println!("- mean skew: {:+.1} ms", report.mean_skew_ms);
println!("- median skew: {:+.1} ms", report.median_skew_ms);
println!("- max abs skew: {:.1} ms", report.max_abs_skew_ms);
println!("- drift: {:+.1} ms", report.drift_ms);
println!("- calibration ready: {}", calibration.ready);
println!(
"- recommended audio offset adjust: {:+} us",
calibration.recommended_audio_offset_adjust_us
);
println!(
"- alternative video offset adjust: {:+} us",
calibration.recommended_video_offset_adjust_us
);
println!("- calibration note: {}", calibration.note);
}
Ok(())
}
#[cfg(any(not(coverage), test))]
fn parse_args<I, S>(args: I) -> Result<(std::path::PathBuf, bool)>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") {
println!("Usage: lesavka-sync-analyze <capture.mkv> [--json]");
std::process::exit(0);
}
let mut emit_json = false;
let mut capture_path = None::<std::path::PathBuf>;
for arg in args {
if arg == "--json" {
emit_json = true;
continue;
}
if capture_path.is_some() {
bail!("unexpected extra argument `{arg}`");
}
capture_path = Some(std::path::PathBuf::from(arg));
}
let capture_path = capture_path.context("capture path is required")?;
Ok((capture_path, emit_json))
}
#[cfg(coverage)]
fn main() {}
#[cfg(test)]
mod tests {
use super::parse_args;
#[test]
fn parse_args_accepts_capture_path_and_json_flag() {
let (path, json) = parse_args(["capture.mkv", "--json"]).expect("args");
assert_eq!(path, std::path::PathBuf::from("capture.mkv"));
assert!(json);
}
#[test]
fn parse_args_rejects_extra_positional_arguments() {
assert!(parse_args(["one.mkv", "two.mkv"]).is_err());
}
#[test]
fn parse_args_requires_a_capture_path() {
let error = parse_args(["--json"]).expect_err("missing capture path should fail");
assert!(
error.to_string().contains("capture path is required"),
"unexpected error: {error:#}"
);
}
#[test]
fn coverage_main_stub_is_non_panicking() {
let _ = super::main();
}
}