From 518df2fb9232a667e5fc198eacc5ae5a3ef4c874 Mon Sep 17 00:00:00 2001 From: webdevtodayjason <115739170+webdevtodayjason@users.noreply.github.com> Date: Mon, 12 Oct 2026 03:04:27 +0000 Subject: [PATCH] Captions: preserve fractional TTML tick offsets --- crates/captions/src/ttml.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/captions/src/ttml.rs b/crates/captions/src/ttml.rs index c16d6976..bdb851b5 100644 --- a/crates/captions/src/ttml.rs +++ b/crates/captions/src/ttml.rs @@ -294,10 +294,7 @@ fn parse_time(s: &str, tm: Timing) -> Option { } whole.0 as i128 + decimal_ticks("0", vf, fd)? } - "t" => { - let n: i128 = vi.parse().ok()?; - n * tps / tm.tick_rate.max(1) as i128 - } + "t" => decimal_ticks(vi, vf, tps)? / tm.tick_rate.max(1) as i128, _ => return None, }; i64::try_from(t).ok().map(Tick) @@ -631,6 +628,23 @@ mod tests { assert!(!doc.warnings.is_empty()); } + #[test] + fn fractional_tick_offsets_keep_exact_caption_times() { + for format in [crate::Format::Ttml, crate::Format::Dfxp] { + let xml = r#"

Hello

"#; + let doc = crate::parse(xml.as_bytes(), format).unwrap(); + assert_eq!(doc.cues.len(), 1); + assert_eq!(doc.cues[0].start, Tick(TICKS_PER_SECOND * 3 / 20)); + assert_eq!(doc.cues[0].end, Tick(TICKS_PER_SECOND * 5 / 4)); + assert!(doc.warnings.is_empty()); + } + let timing = Timing { tick_rate: 3, ..tm(FrameRate::FPS_25) }; + assert_eq!(parse_time("0.5t", timing), Some(Tick(TICKS_PER_SECOND / 6))); + for expr in ["1.xt", "1.1234567890123t", "999999999999999.5t"] { + assert_eq!(parse_time(expr, timing), None, "{expr}"); + } + } + #[test] fn reads_styles_spans_and_nesting() { let x = r#"