use crate::audio::AudioChannel;
use crate::error::{EdlError, EdlResult};
use crate::motion::MotionEffect;
use crate::timecode::EdlTimecode;
use smallvec::SmallVec;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq)]
pub struct EdlEvent {
pub number: u32,
pub reel: String,
pub track: TrackType,
pub edit_type: EditType,
pub source_in: EdlTimecode,
pub source_out: EdlTimecode,
pub record_in: EdlTimecode,
pub record_out: EdlTimecode,
pub transition_duration: Option<u32>,
pub motion_effect: Option<MotionEffect>,
pub clip_name: Option<String>,
pub comments: SmallVec<[String; 2]>,
pub wipe_pattern: Option<WipePattern>,
pub key_type: Option<KeyType>,
}
impl EdlEvent {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
number: u32,
reel: String,
track: TrackType,
edit_type: EditType,
source_in: EdlTimecode,
source_out: EdlTimecode,
record_in: EdlTimecode,
record_out: EdlTimecode,
) -> Self {
Self {
number,
reel,
track,
edit_type,
source_in,
source_out,
record_in,
record_out,
transition_duration: None,
motion_effect: None,
clip_name: None,
comments: SmallVec::new(),
wipe_pattern: None,
key_type: None,
}
}
pub fn validate(&self) -> EdlResult<()> {
if self.source_in >= self.source_out {
return Err(EdlError::InvalidSourceRange);
}
if self.record_in >= self.record_out {
return Err(EdlError::InvalidRecordRange);
}
if matches!(self.edit_type, EditType::Dissolve | EditType::Wipe)
&& self.transition_duration.is_none()
{
return Err(EdlError::MissingField("transition_duration".to_string()));
}
if self.edit_type == EditType::Wipe && self.wipe_pattern.is_none() {
return Err(EdlError::MissingField("wipe_pattern".to_string()));
}
if self.edit_type == EditType::Key && self.key_type.is_none() {
return Err(EdlError::MissingField("key_type".to_string()));
}
Ok(())
}
#[must_use]
pub fn duration_frames(&self) -> u64 {
self.record_out.to_frames() - self.record_in.to_frames()
}
#[must_use]
pub fn overlaps_with(&self, other: &Self) -> bool {
if !self.track.overlaps_with(&other.track) {
return false;
}
!(self.record_out <= other.record_in || self.record_in >= other.record_out)
}
pub fn add_comment(&mut self, comment: String) {
self.comments.push(comment);
}
pub fn set_clip_name(&mut self, name: String) {
self.clip_name = Some(name);
}
pub fn set_motion_effect(&mut self, effect: MotionEffect) {
self.motion_effect = Some(effect);
}
pub fn set_transition_duration(&mut self, duration: u32) {
self.transition_duration = Some(duration);
}
pub fn set_wipe_pattern(&mut self, pattern: WipePattern) {
self.wipe_pattern = Some(pattern);
}
pub fn set_key_type(&mut self, key_type: KeyType) {
self.key_type = Some(key_type);
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum TrackType {
Video,
Audio(AudioChannel),
AudioPair,
AudioWithVideo,
AudioPairWithVideo,
AudioMulti(Vec<AudioChannel>),
VideoWithAudioMulti(Vec<AudioChannel>),
}
impl TrackType {
#[must_use]
pub const fn has_video(&self) -> bool {
matches!(
self,
Self::Video
| Self::AudioWithVideo
| Self::AudioPairWithVideo
| Self::VideoWithAudioMulti(_)
)
}
#[must_use]
pub const fn has_audio(&self) -> bool {
matches!(
self,
Self::Audio(_)
| Self::AudioPair
| Self::AudioWithVideo
| Self::AudioPairWithVideo
| Self::AudioMulti(_)
| Self::VideoWithAudioMulti(_)
)
}
#[must_use]
pub fn overlaps_with(&self, other: &Self) -> bool {
match (self, other) {
(Self::Video, Self::Video) => true,
(Self::Audio(ch1), Self::Audio(ch2)) => ch1 == ch2,
(Self::AudioPair, Self::AudioPair) => true,
(Self::AudioWithVideo, Self::Video) | (Self::Video, Self::AudioWithVideo) => true,
(Self::AudioWithVideo, Self::Audio(_)) | (Self::Audio(_), Self::AudioWithVideo) => true,
(Self::AudioPairWithVideo, Self::Video) | (Self::Video, Self::AudioPairWithVideo) => {
true
}
(Self::AudioPairWithVideo, Self::AudioPair)
| (Self::AudioPair, Self::AudioPairWithVideo) => true,
_ => false,
}
}
}
impl FromStr for TrackType {
type Err = EdlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_uppercase().as_str() {
"V" => Ok(Self::Video),
"A" => Ok(Self::Audio(AudioChannel::A1)),
"A2" => Ok(Self::Audio(AudioChannel::A2)),
"A3" => Ok(Self::Audio(AudioChannel::A3)),
"A4" => Ok(Self::Audio(AudioChannel::A4)),
"AA" => Ok(Self::AudioPair),
"A/V" => Ok(Self::AudioWithVideo),
"AA/V" => Ok(Self::AudioPairWithVideo),
_ => Err(EdlError::InvalidTrackType(s.to_string())),
}
}
}
impl fmt::Display for TrackType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Video => "V",
Self::Audio(AudioChannel::A1) => "A",
Self::Audio(AudioChannel::A2) => "A2",
Self::Audio(AudioChannel::A3) => "A3",
Self::Audio(AudioChannel::A4) => "A4",
Self::Audio(_) => "A",
Self::AudioPair => "AA",
Self::AudioWithVideo => "A/V",
Self::AudioPairWithVideo => "AA/V",
Self::AudioMulti(_) => "A",
Self::VideoWithAudioMulti(_) => "V",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum EditType {
Cut,
Dissolve,
Wipe,
Key,
}
impl FromStr for EditType {
type Err = EdlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_uppercase().as_str() {
"C" => Ok(Self::Cut),
"D" => Ok(Self::Dissolve),
"W" => Ok(Self::Wipe),
"K" => Ok(Self::Key),
_ => Err(EdlError::InvalidEditType(s.to_string())),
}
}
}
impl fmt::Display for EditType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Cut => "C",
Self::Dissolve => "D",
Self::Wipe => "W",
Self::Key => "K",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum WipePattern {
Horizontal,
Vertical,
Diagonal,
Circle,
Custom(u16),
}
impl FromStr for WipePattern {
type Err = EdlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_uppercase().as_str() {
"HORIZONTAL" | "H" | "001" => Ok(Self::Horizontal),
"VERTICAL" | "V" | "002" => Ok(Self::Vertical),
"DIAGONAL" | "D" | "003" => Ok(Self::Diagonal),
"CIRCLE" | "C" | "004" => Ok(Self::Circle),
_ => {
if let Ok(code) = s.trim().parse::<u16>() {
Ok(Self::Custom(code))
} else {
Err(EdlError::InvalidWipePattern(s.to_string()))
}
}
}
}
}
impl fmt::Display for WipePattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Horizontal => "001",
Self::Vertical => "002",
Self::Diagonal => "003",
Self::Circle => "004",
Self::Custom(code) => return write!(f, "{code:03}"),
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum KeyType {
Luminance,
Chroma,
Alpha,
Custom(u16),
}
impl FromStr for KeyType {
type Err = EdlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_uppercase().as_str() {
"LUMINANCE" | "L" => Ok(Self::Luminance),
"CHROMA" | "C" => Ok(Self::Chroma),
"ALPHA" | "A" => Ok(Self::Alpha),
_ => {
if let Ok(code) = s.trim().parse::<u16>() {
Ok(Self::Custom(code))
} else {
Err(EdlError::InvalidKeyType(s.to_string()))
}
}
}
}
}
impl fmt::Display for KeyType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Luminance => "Luminance",
Self::Chroma => "Chroma",
Self::Alpha => "Alpha",
Self::Custom(code) => return write!(f, "{code}"),
};
write!(f, "{s}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::timecode::EdlFrameRate;
#[test]
fn test_track_type_parsing() {
assert_eq!(
"V".parse::<TrackType>().expect("operation should succeed"),
TrackType::Video
);
assert_eq!(
"A".parse::<TrackType>().expect("operation should succeed"),
TrackType::Audio(AudioChannel::A1)
);
assert_eq!(
"AA".parse::<TrackType>().expect("operation should succeed"),
TrackType::AudioPair
);
assert_eq!(
"A/V"
.parse::<TrackType>()
.expect("operation should succeed"),
TrackType::AudioWithVideo
);
}
#[test]
fn test_edit_type_parsing() {
assert_eq!(
"C".parse::<EditType>().expect("operation should succeed"),
EditType::Cut
);
assert_eq!(
"D".parse::<EditType>().expect("operation should succeed"),
EditType::Dissolve
);
assert_eq!(
"W".parse::<EditType>().expect("operation should succeed"),
EditType::Wipe
);
assert_eq!(
"K".parse::<EditType>().expect("operation should succeed"),
EditType::Key
);
}
#[test]
fn test_wipe_pattern_parsing() {
assert_eq!(
"001"
.parse::<WipePattern>()
.expect("operation should succeed"),
WipePattern::Horizontal
);
assert_eq!(
"100"
.parse::<WipePattern>()
.expect("operation should succeed"),
WipePattern::Custom(100)
);
}
#[test]
fn test_event_validation() {
let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc2 = EdlTimecode::new(1, 0, 10, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc3 = EdlTimecode::new(1, 0, 20, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc4 = EdlTimecode::new(1, 0, 30, 0, EdlFrameRate::Fps25).expect("failed to create");
let event = EdlEvent::new(
1,
"A001".to_string(),
TrackType::Video,
EditType::Cut,
tc1,
tc2,
tc3,
tc4,
);
assert!(event.validate().is_ok());
}
#[test]
fn test_event_overlap_detection() {
let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc2 = EdlTimecode::new(1, 0, 10, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc3 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc4 = EdlTimecode::new(1, 0, 15, 0, EdlFrameRate::Fps25).expect("failed to create");
let event1 = EdlEvent::new(
1,
"A001".to_string(),
TrackType::Video,
EditType::Cut,
tc1,
tc2,
tc1,
tc2,
);
let event2 = EdlEvent::new(
2,
"A002".to_string(),
TrackType::Video,
EditType::Cut,
tc3,
tc4,
tc3,
tc4,
);
assert!(event1.overlaps_with(&event2));
}
#[test]
fn test_track_has_video() {
assert!(TrackType::Video.has_video());
assert!(!TrackType::Audio(AudioChannel::A1).has_video());
assert!(TrackType::AudioWithVideo.has_video());
}
#[test]
fn test_track_has_audio() {
assert!(!TrackType::Video.has_audio());
assert!(TrackType::Audio(AudioChannel::A1).has_audio());
assert!(TrackType::AudioWithVideo.has_audio());
}
fn make_test_event() -> EdlEvent {
let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc2 = EdlTimecode::new(1, 0, 10, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc3 = EdlTimecode::new(1, 0, 20, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc4 = EdlTimecode::new(1, 0, 30, 0, EdlFrameRate::Fps25).expect("failed to create");
EdlEvent::new(
1,
"R001".to_string(),
TrackType::Video,
EditType::Cut,
tc1,
tc2,
tc3,
tc4,
)
}
#[test]
fn test_smallvec_inline_comments() {
let ev0 = make_test_event();
assert_eq!(ev0.comments.len(), 0);
assert!(!ev0.comments.spilled());
let mut ev1 = make_test_event();
ev1.add_comment("first".to_string());
assert_eq!(ev1.comments.len(), 1);
assert!(!ev1.comments.spilled());
let mut ev2 = make_test_event();
ev2.add_comment("first".to_string());
ev2.add_comment("second".to_string());
assert_eq!(ev2.comments.len(), 2);
assert!(!ev2.comments.spilled());
}
#[test]
fn test_smallvec_spill_comments() {
let mut ev = make_test_event();
for i in 0..5 {
ev.add_comment(format!("comment {i}"));
}
assert_eq!(ev.comments.len(), 5);
assert!(ev.comments.spilled());
for i in 0..5 {
assert_eq!(ev.comments[i], format!("comment {i}"));
}
}
}