use crate::event::MediaTime;
use crate::timeline::PTS_WRAP;
use crate::webvtt::Cue;
use alloc::format;
use alloc::string::String;
const TICKS_PER_MS: u64 = crate::PTS_HZ / 1000;
const MS_PER_HOUR: u64 = 3_600_000;
const MS_PER_MIN: u64 = 60_000;
const MS_PER_SEC: u64 = 1_000;
#[must_use]
pub fn format_timestamp(t: MediaTime) -> String {
let total_ms = t.0 / TICKS_PER_MS;
let hours = total_ms / MS_PER_HOUR;
let mins = (total_ms % MS_PER_HOUR) / MS_PER_MIN;
let secs = (total_ms % MS_PER_MIN) / MS_PER_SEC;
let ms = total_ms % MS_PER_SEC;
format!("{hours:02}:{mins:02}:{secs:02}.{ms:03}")
}
#[must_use]
pub fn escape_payload(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for c in text.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
_ => out.push(c),
}
}
out
}
#[must_use]
pub fn cue_block(cue: &Cue) -> String {
let mut out = format!(
"{} --> {}\n",
format_timestamp(cue.start),
format_timestamp(cue.end)
);
for (i, line) in cue.text.lines().enumerate() {
if i > 0 {
out.push('\n');
}
out.push_str(&escape_payload(line));
}
out.push('\n');
out
}
#[must_use]
pub fn write_document(cues: &[Cue]) -> String {
let mut out = String::from("WEBVTT\n\n");
for cue in cues {
out.push_str(&cue_block(cue));
out.push('\n');
}
out
}
#[must_use]
pub fn write_segment(cues: &[Cue], segment_start: MediaTime) -> String {
let mpegts = segment_start.0 % PTS_WRAP;
let mut out = format!("WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:{mpegts},LOCAL:00:00:00.000\n\n");
for cue in cues {
let local = Cue {
start: MediaTime(cue.start.0.saturating_sub(segment_start.0)),
end: MediaTime(cue.end.0.saturating_sub(segment_start.0)),
text: cue.text.clone(),
};
out.push_str(&cue_block(&local));
out.push('\n');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
fn cue(start: u64, end: u64, text: &str) -> Cue {
Cue {
start: MediaTime(start),
end: MediaTime(end),
text: text.to_string(),
}
}
#[test]
fn timestamp_formatting() {
assert_eq!(format_timestamp(MediaTime(0)), "00:00:00.000");
assert_eq!(format_timestamp(MediaTime(90_000)), "00:00:01.000");
assert_eq!(format_timestamp(MediaTime(60 * 90_000)), "00:01:00.000");
assert_eq!(format_timestamp(MediaTime(3_600 * 90_000)), "01:00:00.000");
assert_eq!(format_timestamp(MediaTime(90)), "00:00:00.001");
}
#[test]
fn escape_order_is_safe() {
assert_eq!(escape_payload("<"), "&lt;");
assert_eq!(escape_payload("a < b & c > d"), "a < b & c > d");
}
#[test]
fn cue_block_multiline() {
let c = cue(90_000, 180_000, "line one\nline two");
let block = cue_block(&c);
assert_eq!(block, "00:00:01.000 --> 00:00:02.000\nline one\nline two\n");
}
#[test]
fn write_document_signature_and_blank_separation() {
let cues = [cue(0, 1_000, "a"), cue(2_000, 3_000, "b")];
let doc = write_document(&cues);
assert!(doc.starts_with("WEBVTT\n\n"));
assert!(doc.contains("a\n\n00:00:00"));
}
#[test]
fn write_segment_timestamp_map_and_local_times() {
let cues = [cue(9_090_000, 9_180_000, "hi")]; let seg = write_segment(&cues, MediaTime(9_000_000)); let mut lines = seg.lines();
assert_eq!(lines.next(), Some("WEBVTT"));
assert_eq!(
lines.next(),
Some("X-TIMESTAMP-MAP=MPEGTS:9000000,LOCAL:00:00:00.000")
);
assert_eq!(lines.next(), Some(""));
assert_eq!(lines.next(), Some("00:00:01.000 --> 00:00:02.000"));
assert_eq!(lines.next(), Some("hi"));
}
#[test]
fn write_segment_mpegts_wraps_at_33_bits() {
let wrap = PTS_WRAP; let seg = write_segment(&[], MediaTime(wrap + 5));
assert!(seg.contains("X-TIMESTAMP-MAP=MPEGTS:5,LOCAL:00:00:00.000"));
}
}