#![allow(dead_code)]
use crate::Timecode;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum EventKind {
MarkIn,
MarkOut,
Cue,
Custom(String),
}
impl std::fmt::Display for EventKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EventKind::MarkIn => write!(f, "MARK_IN"),
EventKind::MarkOut => write!(f, "MARK_OUT"),
EventKind::Cue => write!(f, "CUE"),
EventKind::Custom(s) => write!(f, "CUSTOM:{}", s),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TimecodeEvent {
pub timecode: Timecode,
pub kind: EventKind,
pub label: String,
pub payload: Option<String>,
}
impl TimecodeEvent {
pub fn new(timecode: Timecode, kind: EventKind) -> Self {
Self {
timecode,
kind,
label: String::new(),
payload: None,
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
pub fn with_payload(mut self, payload: impl Into<String>) -> Self {
self.payload = Some(payload.into());
self
}
}
impl std::fmt::Display for TimecodeEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {} {}", self.kind, self.timecode, self.label)
}
}
#[derive(Debug, Clone)]
pub struct EditRange {
pub mark_in: Timecode,
pub mark_out: Timecode,
}
impl EditRange {
pub fn duration_frames(&self) -> u64 {
let fi = self.mark_in.to_frames();
let fo = self.mark_out.to_frames();
fo.saturating_sub(fi)
}
pub fn duration_seconds(&self) -> f64 {
self.mark_out.to_seconds_f64() - self.mark_in.to_seconds_f64()
}
}
#[derive(Debug, Default)]
pub struct TimecodeEventCapture {
events: Vec<TimecodeEvent>,
pending_mark_in: Option<Timecode>,
}
impl TimecodeEventCapture {
pub fn new() -> Self {
Self::default()
}
pub fn mark_in(&mut self, tc: Timecode) {
self.pending_mark_in = Some(tc);
self.events.push(TimecodeEvent::new(tc, EventKind::MarkIn));
}
pub fn mark_out(&mut self, tc: Timecode) -> Option<EditRange> {
self.events.push(TimecodeEvent::new(tc, EventKind::MarkOut));
self.pending_mark_in.take().map(|mark_in| EditRange {
mark_in,
mark_out: tc,
})
}
pub fn cue(&mut self, tc: Timecode, label: impl Into<String>) {
self.events
.push(TimecodeEvent::new(tc, EventKind::Cue).with_label(label));
}
pub fn custom(
&mut self,
tc: Timecode,
name: impl Into<String>,
label: impl Into<String>,
payload: Option<String>,
) {
let mut ev = TimecodeEvent::new(tc, EventKind::Custom(name.into())).with_label(label);
if let Some(p) = payload {
ev = ev.with_payload(p);
}
self.events.push(ev);
}
pub fn events(&self) -> &[TimecodeEvent] {
&self.events
}
pub fn mark_ins(&self) -> Vec<&TimecodeEvent> {
self.events
.iter()
.filter(|e| e.kind == EventKind::MarkIn)
.collect()
}
pub fn mark_outs(&self) -> Vec<&TimecodeEvent> {
self.events
.iter()
.filter(|e| e.kind == EventKind::MarkOut)
.collect()
}
pub fn edit_ranges(&self) -> Vec<EditRange> {
let mut ranges = Vec::new();
let mut pending: Option<Timecode> = None;
for ev in &self.events {
match &ev.kind {
EventKind::MarkIn => {
pending = Some(ev.timecode);
}
EventKind::MarkOut => {
if let Some(mark_in) = pending.take() {
ranges.push(EditRange {
mark_in,
mark_out: ev.timecode,
});
}
}
_ => {}
}
}
ranges
}
pub fn clear(&mut self) {
self.events.clear();
self.pending_mark_in = None;
}
pub fn len(&self) -> usize {
self.events.len()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FrameRate;
fn tc(h: u8, m: u8, s: u8, f: u8) -> Timecode {
Timecode::new(h, m, s, f, FrameRate::Fps25).expect("valid timecode")
}
#[test]
fn test_mark_in_records_event() {
let mut cap = TimecodeEventCapture::new();
cap.mark_in(tc(0, 0, 1, 0));
assert_eq!(cap.len(), 1);
assert_eq!(cap.events()[0].kind, EventKind::MarkIn);
}
#[test]
fn test_mark_out_returns_range() {
let mut cap = TimecodeEventCapture::new();
cap.mark_in(tc(0, 0, 1, 0));
let range = cap.mark_out(tc(0, 0, 5, 0));
assert!(range.is_some());
let r = range.expect("should have range");
assert_eq!(r.duration_frames(), 4 * 25);
}
#[test]
fn test_mark_out_without_mark_in_returns_none() {
let mut cap = TimecodeEventCapture::new();
let range = cap.mark_out(tc(0, 0, 5, 0));
assert!(range.is_none());
}
#[test]
fn test_cue_event() {
let mut cap = TimecodeEventCapture::new();
cap.cue(tc(0, 1, 0, 0), "Scene 1");
assert_eq!(cap.len(), 1);
assert_eq!(cap.events()[0].kind, EventKind::Cue);
assert_eq!(cap.events()[0].label, "Scene 1");
}
#[test]
fn test_custom_event() {
let mut cap = TimecodeEventCapture::new();
cap.custom(
tc(0, 2, 0, 0),
"FLASH",
"Harding flash detected",
Some("{\"severity\":\"high\"}".into()),
);
assert_eq!(cap.len(), 1);
assert!(matches!(&cap.events()[0].kind, EventKind::Custom(n) if n == "FLASH"));
assert!(cap.events()[0].payload.is_some());
}
#[test]
fn test_edit_ranges_reconstruction() {
let mut cap = TimecodeEventCapture::new();
cap.mark_in(tc(0, 0, 1, 0));
cap.mark_out(tc(0, 0, 5, 0));
cap.mark_in(tc(0, 1, 0, 0));
cap.mark_out(tc(0, 1, 30, 0));
let ranges = cap.edit_ranges();
assert_eq!(ranges.len(), 2);
assert_eq!(ranges[0].duration_frames(), 4 * 25);
assert_eq!(ranges[1].duration_frames(), 30 * 25);
}
#[test]
fn test_mark_ins_filter() {
let mut cap = TimecodeEventCapture::new();
cap.mark_in(tc(0, 0, 1, 0));
cap.cue(tc(0, 0, 2, 0), "cue");
cap.mark_out(tc(0, 0, 5, 0));
assert_eq!(cap.mark_ins().len(), 1);
assert_eq!(cap.mark_outs().len(), 1);
}
#[test]
fn test_clear_resets_state() {
let mut cap = TimecodeEventCapture::new();
cap.mark_in(tc(0, 0, 1, 0));
cap.clear();
assert!(cap.is_empty());
let range = cap.mark_out(tc(0, 0, 5, 0));
assert!(range.is_none());
}
#[test]
fn test_duration_seconds() {
let r = EditRange {
mark_in: tc(0, 0, 0, 0),
mark_out: tc(0, 0, 4, 0),
};
assert!((r.duration_seconds() - 4.0).abs() < 1e-6);
}
#[test]
fn test_event_display() {
let ev = TimecodeEvent::new(tc(1, 2, 3, 4), EventKind::Cue).with_label("test");
let s = ev.to_string();
assert!(s.contains("CUE"));
assert!(s.contains("01:02:03:04"));
}
#[test]
fn test_event_kind_display() {
assert_eq!(EventKind::MarkIn.to_string(), "MARK_IN");
assert_eq!(EventKind::MarkOut.to_string(), "MARK_OUT");
assert_eq!(EventKind::Cue.to_string(), "CUE");
assert_eq!(EventKind::Custom("FOO".into()).to_string(), "CUSTOM:FOO");
}
}