timed_metadata/webvtt/
writer.rs1use crate::event::MediaTime;
9use crate::timeline::PTS_WRAP;
10use crate::webvtt::Cue;
11use alloc::format;
12use alloc::string::String;
13
14const TICKS_PER_MS: u64 = crate::PTS_HZ / 1000;
17const MS_PER_HOUR: u64 = 3_600_000;
19const MS_PER_MIN: u64 = 60_000;
20const MS_PER_SEC: u64 = 1_000;
21
22#[must_use]
27pub fn format_timestamp(t: MediaTime) -> String {
28 let total_ms = t.0 / TICKS_PER_MS;
29 let hours = total_ms / MS_PER_HOUR;
30 let mins = (total_ms % MS_PER_HOUR) / MS_PER_MIN;
31 let secs = (total_ms % MS_PER_MIN) / MS_PER_SEC;
32 let ms = total_ms % MS_PER_SEC;
33 format!("{hours:02}:{mins:02}:{secs:02}.{ms:03}")
34}
35
36#[must_use]
42pub fn escape_payload(text: &str) -> String {
43 let mut out = String::with_capacity(text.len());
44 for c in text.chars() {
45 match c {
46 '&' => out.push_str("&"),
47 '<' => out.push_str("<"),
48 '>' => out.push_str(">"),
49 _ => out.push(c),
50 }
51 }
52 out
53}
54
55#[must_use]
59pub fn cue_block(cue: &Cue) -> String {
60 let mut out = format!(
61 "{} --> {}\n",
62 format_timestamp(cue.start),
63 format_timestamp(cue.end)
64 );
65 for (i, line) in cue.text.lines().enumerate() {
66 if i > 0 {
67 out.push('\n');
68 }
69 out.push_str(&escape_payload(line));
70 }
71 out.push('\n');
72 out
73}
74
75#[must_use]
78pub fn write_document(cues: &[Cue]) -> String {
79 let mut out = String::from("WEBVTT\n\n");
80 for cue in cues {
81 out.push_str(&cue_block(cue));
82 out.push('\n');
83 }
84 out
85}
86
87#[must_use]
101pub fn write_segment(cues: &[Cue], segment_start: MediaTime) -> String {
102 let mpegts = segment_start.0 % PTS_WRAP;
103 let mut out = format!("WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:{mpegts},LOCAL:00:00:00.000\n\n");
104 for cue in cues {
105 let local = Cue {
106 start: MediaTime(cue.start.0.saturating_sub(segment_start.0)),
107 end: MediaTime(cue.end.0.saturating_sub(segment_start.0)),
108 text: cue.text.clone(),
109 };
110 out.push_str(&cue_block(&local));
111 out.push('\n');
112 }
113 out
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use alloc::string::ToString;
120
121 fn cue(start: u64, end: u64, text: &str) -> Cue {
122 Cue {
123 start: MediaTime(start),
124 end: MediaTime(end),
125 text: text.to_string(),
126 }
127 }
128
129 #[test]
130 fn timestamp_formatting() {
131 assert_eq!(format_timestamp(MediaTime(0)), "00:00:00.000");
132 assert_eq!(format_timestamp(MediaTime(90_000)), "00:00:01.000");
134 assert_eq!(format_timestamp(MediaTime(60 * 90_000)), "00:01:00.000");
136 assert_eq!(format_timestamp(MediaTime(3_600 * 90_000)), "01:00:00.000");
138 assert_eq!(format_timestamp(MediaTime(90)), "00:00:00.001");
140 }
141
142 #[test]
143 fn escape_order_is_safe() {
144 assert_eq!(escape_payload("<"), "&lt;");
147 assert_eq!(escape_payload("a < b & c > d"), "a < b & c > d");
148 }
149
150 #[test]
151 fn cue_block_multiline() {
152 let c = cue(90_000, 180_000, "line one\nline two");
153 let block = cue_block(&c);
154 assert_eq!(block, "00:00:01.000 --> 00:00:02.000\nline one\nline two\n");
155 }
156
157 #[test]
158 fn write_document_signature_and_blank_separation() {
159 let cues = [cue(0, 1_000, "a"), cue(2_000, 3_000, "b")];
160 let doc = write_document(&cues);
161 assert!(doc.starts_with("WEBVTT\n\n"));
162 assert!(doc.contains("a\n\n00:00:00"));
164 }
165
166 #[test]
167 fn write_segment_timestamp_map_and_local_times() {
168 let cues = [cue(9_090_000, 9_180_000, "hi")]; let seg = write_segment(&cues, MediaTime(9_000_000)); let mut lines = seg.lines();
171 assert_eq!(lines.next(), Some("WEBVTT"));
172 assert_eq!(
173 lines.next(),
174 Some("X-TIMESTAMP-MAP=MPEGTS:9000000,LOCAL:00:00:00.000")
175 );
176 assert_eq!(lines.next(), Some(""));
177 assert_eq!(lines.next(), Some("00:00:01.000 --> 00:00:02.000"));
179 assert_eq!(lines.next(), Some("hi"));
180 }
181
182 #[test]
183 fn write_segment_mpegts_wraps_at_33_bits() {
184 let wrap = PTS_WRAP; let seg = write_segment(&[], MediaTime(wrap + 5));
186 assert!(seg.contains("X-TIMESTAMP-MAP=MPEGTS:5,LOCAL:00:00:00.000"));
187 }
188}