1#![allow(dead_code)]
2use crate::Timecode;
25
26#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub enum EventKind {
33 MarkIn,
35 MarkOut,
37 Cue,
39 Custom(String),
41}
42
43impl std::fmt::Display for EventKind {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 EventKind::MarkIn => write!(f, "MARK_IN"),
47 EventKind::MarkOut => write!(f, "MARK_OUT"),
48 EventKind::Cue => write!(f, "CUE"),
49 EventKind::Custom(s) => write!(f, "CUSTOM:{}", s),
50 }
51 }
52}
53
54#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
60pub struct TimecodeEvent {
61 pub timecode: Timecode,
63 pub kind: EventKind,
65 pub label: String,
67 pub payload: Option<String>,
69}
70
71impl TimecodeEvent {
72 pub fn new(timecode: Timecode, kind: EventKind) -> Self {
74 Self {
75 timecode,
76 kind,
77 label: String::new(),
78 payload: None,
79 }
80 }
81
82 pub fn with_label(mut self, label: impl Into<String>) -> Self {
84 self.label = label.into();
85 self
86 }
87
88 pub fn with_payload(mut self, payload: impl Into<String>) -> Self {
90 self.payload = Some(payload.into());
91 self
92 }
93}
94
95impl std::fmt::Display for TimecodeEvent {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 write!(f, "[{}] {} {}", self.kind, self.timecode, self.label)
98 }
99}
100
101#[derive(Debug, Clone)]
107pub struct EditRange {
108 pub mark_in: Timecode,
110 pub mark_out: Timecode,
112}
113
114impl EditRange {
115 pub fn duration_frames(&self) -> u64 {
119 let fi = self.mark_in.to_frames();
120 let fo = self.mark_out.to_frames();
121 fo.saturating_sub(fi)
122 }
123
124 pub fn duration_seconds(&self) -> f64 {
126 self.mark_out.to_seconds_f64() - self.mark_in.to_seconds_f64()
127 }
128}
129
130#[derive(Debug, Default)]
139pub struct TimecodeEventCapture {
140 events: Vec<TimecodeEvent>,
142 pending_mark_in: Option<Timecode>,
144}
145
146impl TimecodeEventCapture {
147 pub fn new() -> Self {
149 Self::default()
150 }
151
152 pub fn mark_in(&mut self, tc: Timecode) {
157 self.pending_mark_in = Some(tc);
158 self.events.push(TimecodeEvent::new(tc, EventKind::MarkIn));
159 }
160
161 pub fn mark_out(&mut self, tc: Timecode) -> Option<EditRange> {
165 self.events.push(TimecodeEvent::new(tc, EventKind::MarkOut));
166 self.pending_mark_in.take().map(|mark_in| EditRange {
167 mark_in,
168 mark_out: tc,
169 })
170 }
171
172 pub fn cue(&mut self, tc: Timecode, label: impl Into<String>) {
174 self.events
175 .push(TimecodeEvent::new(tc, EventKind::Cue).with_label(label));
176 }
177
178 pub fn custom(
180 &mut self,
181 tc: Timecode,
182 name: impl Into<String>,
183 label: impl Into<String>,
184 payload: Option<String>,
185 ) {
186 let mut ev = TimecodeEvent::new(tc, EventKind::Custom(name.into())).with_label(label);
187 if let Some(p) = payload {
188 ev = ev.with_payload(p);
189 }
190 self.events.push(ev);
191 }
192
193 pub fn events(&self) -> &[TimecodeEvent] {
195 &self.events
196 }
197
198 pub fn mark_ins(&self) -> Vec<&TimecodeEvent> {
200 self.events
201 .iter()
202 .filter(|e| e.kind == EventKind::MarkIn)
203 .collect()
204 }
205
206 pub fn mark_outs(&self) -> Vec<&TimecodeEvent> {
208 self.events
209 .iter()
210 .filter(|e| e.kind == EventKind::MarkOut)
211 .collect()
212 }
213
214 pub fn edit_ranges(&self) -> Vec<EditRange> {
218 let mut ranges = Vec::new();
219 let mut pending: Option<Timecode> = None;
220
221 for ev in &self.events {
222 match &ev.kind {
223 EventKind::MarkIn => {
224 pending = Some(ev.timecode);
225 }
226 EventKind::MarkOut => {
227 if let Some(mark_in) = pending.take() {
228 ranges.push(EditRange {
229 mark_in,
230 mark_out: ev.timecode,
231 });
232 }
233 }
234 _ => {}
235 }
236 }
237
238 ranges
239 }
240
241 pub fn clear(&mut self) {
243 self.events.clear();
244 self.pending_mark_in = None;
245 }
246
247 pub fn len(&self) -> usize {
249 self.events.len()
250 }
251
252 pub fn is_empty(&self) -> bool {
254 self.events.is_empty()
255 }
256}
257
258#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::FrameRate;
266
267 fn tc(h: u8, m: u8, s: u8, f: u8) -> Timecode {
268 Timecode::new(h, m, s, f, FrameRate::Fps25).expect("valid timecode")
269 }
270
271 #[test]
272 fn test_mark_in_records_event() {
273 let mut cap = TimecodeEventCapture::new();
274 cap.mark_in(tc(0, 0, 1, 0));
275 assert_eq!(cap.len(), 1);
276 assert_eq!(cap.events()[0].kind, EventKind::MarkIn);
277 }
278
279 #[test]
280 fn test_mark_out_returns_range() {
281 let mut cap = TimecodeEventCapture::new();
282 cap.mark_in(tc(0, 0, 1, 0));
283 let range = cap.mark_out(tc(0, 0, 5, 0));
284 assert!(range.is_some());
285 let r = range.expect("should have range");
286 assert_eq!(r.duration_frames(), 4 * 25);
287 }
288
289 #[test]
290 fn test_mark_out_without_mark_in_returns_none() {
291 let mut cap = TimecodeEventCapture::new();
292 let range = cap.mark_out(tc(0, 0, 5, 0));
293 assert!(range.is_none());
294 }
295
296 #[test]
297 fn test_cue_event() {
298 let mut cap = TimecodeEventCapture::new();
299 cap.cue(tc(0, 1, 0, 0), "Scene 1");
300 assert_eq!(cap.len(), 1);
301 assert_eq!(cap.events()[0].kind, EventKind::Cue);
302 assert_eq!(cap.events()[0].label, "Scene 1");
303 }
304
305 #[test]
306 fn test_custom_event() {
307 let mut cap = TimecodeEventCapture::new();
308 cap.custom(
309 tc(0, 2, 0, 0),
310 "FLASH",
311 "Harding flash detected",
312 Some("{\"severity\":\"high\"}".into()),
313 );
314 assert_eq!(cap.len(), 1);
315 assert!(matches!(&cap.events()[0].kind, EventKind::Custom(n) if n == "FLASH"));
316 assert!(cap.events()[0].payload.is_some());
317 }
318
319 #[test]
320 fn test_edit_ranges_reconstruction() {
321 let mut cap = TimecodeEventCapture::new();
322 cap.mark_in(tc(0, 0, 1, 0));
323 cap.mark_out(tc(0, 0, 5, 0));
324 cap.mark_in(tc(0, 1, 0, 0));
325 cap.mark_out(tc(0, 1, 30, 0));
326
327 let ranges = cap.edit_ranges();
328 assert_eq!(ranges.len(), 2);
329 assert_eq!(ranges[0].duration_frames(), 4 * 25);
330 assert_eq!(ranges[1].duration_frames(), 30 * 25);
331 }
332
333 #[test]
334 fn test_mark_ins_filter() {
335 let mut cap = TimecodeEventCapture::new();
336 cap.mark_in(tc(0, 0, 1, 0));
337 cap.cue(tc(0, 0, 2, 0), "cue");
338 cap.mark_out(tc(0, 0, 5, 0));
339 assert_eq!(cap.mark_ins().len(), 1);
340 assert_eq!(cap.mark_outs().len(), 1);
341 }
342
343 #[test]
344 fn test_clear_resets_state() {
345 let mut cap = TimecodeEventCapture::new();
346 cap.mark_in(tc(0, 0, 1, 0));
347 cap.clear();
348 assert!(cap.is_empty());
349 let range = cap.mark_out(tc(0, 0, 5, 0));
351 assert!(range.is_none());
352 }
353
354 #[test]
355 fn test_duration_seconds() {
356 let r = EditRange {
357 mark_in: tc(0, 0, 0, 0),
358 mark_out: tc(0, 0, 4, 0),
359 };
360 assert!((r.duration_seconds() - 4.0).abs() < 1e-6);
361 }
362
363 #[test]
364 fn test_event_display() {
365 let ev = TimecodeEvent::new(tc(1, 2, 3, 4), EventKind::Cue).with_label("test");
366 let s = ev.to_string();
367 assert!(s.contains("CUE"));
368 assert!(s.contains("01:02:03:04"));
369 }
370
371 #[test]
372 fn test_event_kind_display() {
373 assert_eq!(EventKind::MarkIn.to_string(), "MARK_IN");
374 assert_eq!(EventKind::MarkOut.to_string(), "MARK_OUT");
375 assert_eq!(EventKind::Cue.to_string(), "CUE");
376 assert_eq!(EventKind::Custom("FOO".into()).to_string(), "CUSTOM:FOO");
377 }
378}