diff --git a/Cargo.toml b/Cargo.toml index d3b8f179b..e514fb6f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,6 +121,7 @@ members = [ "write-debian-changelog", "zip-or-dir", "zip-or-dir/dir2zip", + "media-utils/video2srt", ] exclude = ["led-box-firmware", "led-box-firmware-pico"] diff --git a/media-utils/video2srt/Cargo.toml b/media-utils/video2srt/Cargo.toml new file mode 100644 index 000000000..06d5365da --- /dev/null +++ b/media-utils/video2srt/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "video2srt" +description = "Create a subtitle .srt file from video with Strand Cam timestamps" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.3.4", features = ["derive"] } +color-eyre = "0.6.2" + +tracing = "0.1.40" +chrono = { version = "0.4.38", features = [ + "libc", + "serde", + "std", +], default-features = false } + +env-tracing-logger = { path = "../../env-tracing-logger" } +frame-source = { path = "../frame-source" } diff --git a/media-utils/video2srt/src/main.rs b/media-utils/video2srt/src/main.rs new file mode 100644 index 000000000..c3b7da366 --- /dev/null +++ b/media-utils/video2srt/src/main.rs @@ -0,0 +1,115 @@ +use clap::Parser; +use color_eyre::eyre::{self, WrapErr}; +use std::{io::Write, path::PathBuf}; + +use frame_source::Timestamp; + +#[derive(Debug, Parser)] +#[command(version, about)] +struct Opt { + /// Input video filename. + #[arg(short, long)] + input: PathBuf, + + /// Output srt filename. Defaults to ".srt" + #[arg(short, long)] + output: Option, +} + +trait Srt { + fn srt(&self) -> String; +} + +impl Srt for std::time::Duration { + fn srt(&self) -> String { + // from https://en.wikipedia.org/wiki/SubRip : + // "hours:minutes:seconds,milliseconds with time units fixed to two + // zero-padded digits and fractions fixed to three zero-padded digits + // (00:00:00,000). The fractional separator used is the comma, since the + // program was written in France." + let total_secs = self.as_secs(); + let hours = total_secs / (60 * 60); + let minutes = (total_secs % (60 * 60)) / 60; + let seconds = total_secs % 60; + dbg!(total_secs); + dbg!(hours); + dbg!(minutes); + dbg!(seconds); + debug_assert_eq!(total_secs, hours * 60 * 60 + minutes * 60 + seconds); + let millis = self.subsec_millis(); + format!("{hours:02}:{minutes:02}:{seconds:02},{millis:03}") + } +} + +fn main() -> eyre::Result<()> { + if std::env::var_os("RUST_LOG").is_none() { + std::env::set_var("RUST_LOG", "info"); + } + env_tracing_logger::init(); + let opt = Opt::parse(); + + let output = opt.output; + + let output = output.unwrap_or_else(|| { + let mut output = opt.input.as_os_str().to_owned(); + output.push(".srt"); + output.into() + }); + + let do_decode_h264 = false; + let mut src = frame_source::from_path(&opt.input, do_decode_h264) + .with_context(|| format!("while opening path {}", opt.input.display()))?; + + let start_time = src + .frame0_time() + .ok_or_else(|| eyre::eyre!("no start time found"))?; + + let mut out_fd = std::fs::File::create(&output)?; + + let mut prev_data: Option<(std::time::Duration, _)> = None; + + let mut count = 1; + for (_fno, frame) in src.iter().enumerate() { + let frame = frame?; + let pts = match frame.timestamp() { + Timestamp::Duration(pts) => pts, + _ => { + eyre::bail!( + "video has no PTS timestamps and framerate was not \ + specified on the command line." + ); + } + }; + let frame_stamp = start_time + pts; + if let Some((prev_pts, prev_stamp)) = prev_data.take() { + out_fd.write_all( + format!( + "{}\n{} --> {}\n{}\n\n", + count, + prev_pts.srt(), + pts.srt(), + prev_stamp + ) + .as_bytes(), + )?; + count += 1; + } + prev_data = Some((pts, frame_stamp)); + } + + if let Some((prev_pts, prev_stamp)) = prev_data.take() { + let pts = prev_pts + std::time::Duration::from_secs(1); + out_fd.write_all( + format!( + "{}\n{} --> {}\n{}\n\n", + count, + prev_pts.srt(), + pts.srt(), + prev_stamp + ) + .as_bytes(), + )?; + } + + Ok(()) +}