#![cfg(feature = "teletext")]
use dvb_vbi::TeletextDataField;
use std::fs;
use std::path::Path;
use timed_metadata::event::MediaTime;
use timed_metadata::webvtt::{Cue, TeletextCueExtractor, write_document};
fn fixture_path() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("fixtures")
.join("teletext")
.join("teletext_subtitle_synthetic.txt")
}
fn load_frames() -> Vec<(u64, Vec<u8>)> {
let text =
fs::read_to_string(fixture_path()).expect("read teletext_subtitle_synthetic.txt fixture");
let mut frames = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut parts = line.split_whitespace();
let pts: u64 = parts
.next()
.expect("pts field")
.parse()
.expect("pts is a u64");
let hex = parts.next().expect("hex field");
let bytes = (0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).expect("valid hex byte"))
.collect();
frames.push((pts, bytes));
}
frames
}
fn extract_cues() -> Vec<Cue> {
let frames = load_frames();
assert_eq!(frames.len(), 4, "fixture frame count changed unexpectedly");
let mut ex = TeletextCueExtractor::new(8, 0x88);
for (pts, bytes) in &frames {
let field = TeletextDataField::parse(bytes).expect("valid TeletextDataField wire bytes");
ex.push_frame(*pts, std::slice::from_ref(&field));
}
ex.finalize(33_000);
ex.into_cues()
}
#[test]
fn decode_to_expected_cues() {
let cues = extract_cues();
let expected = [
(3_000u64, 6_000u64, "HELLO WORLD"),
(6_000, 30_000, "HELLO WORLD\nTHIS IS A TEST"),
];
assert_eq!(cues.len(), expected.len(), "cues: {cues:?}");
for (cue, (start, end, text)) in cues.iter().zip(expected.iter()) {
assert_eq!(cue.start, MediaTime(*start), "cue {text:?} start");
assert_eq!(cue.end, MediaTime(*end), "cue {text:?} end");
assert_eq!(cue.text, *text);
}
}
#[test]
fn webvtt_output_contains_expected_text() {
let cues = extract_cues();
let doc = write_document(&cues);
assert!(doc.starts_with("WEBVTT\n\n"));
assert!(doc.contains("HELLO WORLD"));
assert!(doc.contains("THIS IS A TEST"));
assert!(doc.contains("00:00:00.066 --> 00:00:00.333"));
}
#[test]
fn mutation_bite_parity_corruption_yields_replacement_char() {
let mut frames = load_frames();
frames[1].1[4] ^= 0x01;
let mut ex = TeletextCueExtractor::new(8, 0x88);
for (pts, bytes) in &frames {
let field = TeletextDataField::parse(bytes).expect("valid TeletextDataField wire bytes");
ex.push_frame(*pts, std::slice::from_ref(&field));
}
ex.finalize(33_000);
let cues = ex.into_cues();
assert_eq!(cues.len(), 2, "cues: {cues:?}");
assert!(
cues[0].text.starts_with("\u{FFFD}ELLO WORLD"),
"corrupted parity byte must decode as the replacement character, not 'H' or anything else: {:?}",
cues[0].text
);
assert!(
cues[1]
.text
.starts_with("\u{FFFD}ELLO WORLD\nTHIS IS A TEST")
);
}
#[test]
fn mutation_bite_hamming_single_bit_error_is_corrected() {
let mut frames = load_frames();
frames[0].1[4] ^= 0x02;
let mut ex = TeletextCueExtractor::new(8, 0x88);
for (pts, bytes) in &frames {
let field = TeletextDataField::parse(bytes).expect("valid TeletextDataField wire bytes");
ex.push_frame(*pts, std::slice::from_ref(&field));
}
ex.finalize(33_000);
let cues = ex.into_cues();
assert_eq!(
cues.len(),
2,
"a single-bit Hamming error in the page header must still be corrected \
and the page recognised, producing the same cues as the uncorrupted decode: {cues:?}"
);
assert_eq!(cues[0].text, "HELLO WORLD");
assert_eq!(cues[1].text, "HELLO WORLD\nTHIS IS A TEST");
}