diff --git a/crates/captions/src/scc.rs b/crates/captions/src/scc.rs index 4200b95f..e0000cff 100644 --- a/crates/captions/src/scc.rs +++ b/crates/captions/src/scc.rs @@ -446,14 +446,32 @@ pub fn schedule(doc: &Document) -> BTreeMap { } let load = load_pairs(&c.text); let n = load.len() as i64; - // Doubled codes must stay adjacent, so the load is one contiguous run of free frames: - // the latest run ending by `s`, else the first run after `cursor` (delaying the caption). - let run_free = |occ: &BTreeMap, b: i64| (b..b + n).all(|f| !occ.contains_key(&f)); - let b = (cursor..=s - n).rev().find(|&b| run_free(&occ, b)).unwrap_or_else(|| (cursor..).find(|&b| run_free(&occ, b)).unwrap_or(cursor)); - for (k, p) in load.into_iter().enumerate() { - occ.insert(b + k as i64, p); + // Erasing displayed memory can interrupt loading non-displayed memory. Keep each + // doubled control/character code adjacent, but allow pauses between those groups. + let plan_load = |mut f: i64| { + let mut plan = Vec::with_capacity(load.len()); + let mut k = 0; + while let Some(&p) = load.get(k) { + let doubled = (0x10..=0x1f).contains(&p.0) && load.get(k + 1) == Some(&p); + let width = if doubled { 2 } else { 1 }; + while (f..f + width).any(|at| occ.contains_key(&at)) { + f += 1; + } + for offset in 0..width { + plan.push((f + offset, p)); + } + f += width; + k += width as usize; + } + (plan, f) + }; + // Prefer the latest load that finishes by the requested start; if it cannot fit, + // use the earliest available frames rather than waiting until the prior cue ends. + let (plan, end) = (cursor..=s - n).rev().map(plan_load).find(|(_, end)| *end <= s).unwrap_or_else(|| plan_load(cursor)); + for (f, p) in plan { + occ.insert(f, p); } - let mut d = s.max(b + n); + let mut d = s.max(end); while occ.contains_key(&d) { d += 1; } diff --git a/crates/captions/tests/it/roundtrip.rs b/crates/captions/tests/it/roundtrip.rs index cbb7bdcd..23714416 100644 --- a/crates/captions/tests/it/roundtrip.rs +++ b/crates/captions/tests/it/roundtrip.rs @@ -260,3 +260,49 @@ fn srt_to_vtt_to_scc_keeps_text_and_frames() { assert_eq!(scc::frame_of(doc3.cues[0].start), 60); assert_eq!(scc::frame_of(doc3.cues[1].end), 210); } + +#[test] +fn tight_pop_on_load_can_cross_the_previous_caption_end() { + let text = "A second caption that needs more time to load"; + let doc = Document { + cues: vec![ + Cue { start: scc::RATE.tick_of(30), end: scc::RATE.tick_of(60), text: "First".into(), ..Default::default() }, + Cue { start: scc::RATE.tick_of(67), end: scc::RATE.tick_of(150), text: text.into(), ..Default::default() }, + ], + ..Default::default() + }; + for format in [Format::Scc, Format::Mcc] { + let back = parse(&write(&doc, format, WriteOptions::default()), format).unwrap(); + assert_eq!(back.cues.len(), 2); + assert_eq!(scc::frame_of(back.cues[0].end), 60); + assert_eq!(scc::frame_of(back.cues[1].start), 67, "{format:?}"); + assert_eq!(scc::frame_of(back.cues[1].end), 150); + } +} + +#[test] +fn interrupted_load_keeps_extended_characters_and_repeated_basic_pairs() { + for text in ["A second caption that needs more time to load", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "ÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀ\nÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀ"] + { + let doc = Document { + cues: vec![ + Cue { start: scc::RATE.tick_of(30), end: scc::RATE.tick_of(60), text: "First".into(), ..Default::default() }, + Cue { start: scc::RATE.tick_of(67), end: scc::RATE.tick_of(180), text: text.into(), ..Default::default() }, + ], + ..Default::default() + }; + for format in [Format::Scc, Format::Mcc] { + let back = parse(&write(&doc, format, WriteOptions::default()), format).unwrap(); + assert_eq!(back.cues.len(), 2); + assert_eq!(back.cues[1].text.replace(['\n', ' '], ""), text.replace(['\n', ' '], ""), "{format:?}"); + assert_eq!(scc::frame_of(back.cues[0].end), 60); + assert_eq!(scc::frame_of(back.cues[1].end), 180); + } + let pairs = scc::schedule(&doc); + for (&frame, &pair) in &pairs { + if (0x10..=0x1f).contains(&pair.0) { + assert!(pairs.get(&(frame - 1)) == Some(&pair) || pairs.get(&(frame + 1)) == Some(&pair), "split doubled code at {frame}"); + } + } + } +}