#![warn(missing_docs)]
#![allow(
clippy::module_name_repetitions,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
dead_code,
clippy::pedantic
)]
pub mod aaf;
pub mod ale;
pub mod audio;
pub mod batch_export;
pub mod cmx3600;
pub mod conform_report;
pub mod consolidate;
pub mod converter;
pub mod diff;
pub mod edl_ascii_timeline;
pub mod edl_changelist;
pub mod edl_comments;
pub mod edl_compare;
pub mod edl_duration;
pub mod edl_event;
pub mod edl_filter;
pub mod edl_merge;
pub mod edl_sanitize;
pub mod edl_statistics;
pub mod edl_timeline;
pub mod edl_validator;
pub mod error;
pub mod event;
pub mod event_list;
pub mod fcpxml;
pub mod filter;
pub mod frame_count;
pub mod fuzz_tests;
pub mod generator;
pub mod lazy_parser;
pub mod metadata;
pub mod motion;
pub mod multicam;
pub mod optimizer;
pub mod otio;
pub mod parser;
pub mod parser_opt;
pub mod reel;
pub mod reel_map;
pub mod reel_registry;
pub mod roundtrip;
pub mod subframe;
pub mod tc_cache;
pub mod tc_list;
pub mod timecode;
pub mod to_timeline;
pub mod transition_events;
pub mod validator;
pub mod version_history;
pub use ale::{parse_ale_records, AleRecord};
pub use batch_export::BatchEdlExporter;
pub use error::{EdlError, EdlResult};
pub use generator::EdlGenerator;
pub use parser::{parse_edl, EdlParser};
pub use validator::EdlValidator;
use crate::event::EdlEvent;
use crate::reel::ReelTable;
use crate::timecode::EdlFrameRate;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum EdlFormat {
Cmx3600,
Cmx3400,
Cmx340,
Gvg,
SonyBve9000,
}
impl EdlFormat {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Cmx3600 => "CMX 3600",
Self::Cmx3400 => "CMX 3400",
Self::Cmx340 => "CMX 340",
Self::Gvg => "GVG",
Self::SonyBve9000 => "Sony BVE-9000",
}
}
}
impl std::fmt::Display for EdlFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct Edl {
pub format: EdlFormat,
pub title: Option<String>,
pub frame_rate: EdlFrameRate,
pub events: Vec<EdlEvent>,
pub reel_table: ReelTable,
pub source_file: Option<PathBuf>,
pub global_comments: Vec<String>,
}
impl Edl {
#[must_use]
pub fn new(format: EdlFormat) -> Self {
Self {
format,
title: None,
frame_rate: EdlFrameRate::Fps2997NDF,
events: Vec::new(),
reel_table: ReelTable::new(),
source_file: None,
global_comments: Vec::new(),
}
}
#[must_use]
pub fn cmx3600() -> Self {
Self::new(EdlFormat::Cmx3600)
}
pub fn set_title(&mut self, title: String) {
self.title = Some(title);
}
pub fn set_frame_rate(&mut self, frame_rate: EdlFrameRate) {
self.frame_rate = frame_rate;
}
pub fn set_source_file(&mut self, path: PathBuf) {
self.source_file = Some(path);
}
pub fn add_event(&mut self, event: EdlEvent) -> EdlResult<()> {
event.validate()?;
self.events.push(event);
Ok(())
}
pub fn add_global_comment(&mut self, comment: String) {
self.global_comments.push(comment);
}
#[must_use]
pub fn get_event(&self, number: u32) -> Option<&EdlEvent> {
self.events.iter().find(|e| e.number == number)
}
pub fn get_event_mut(&mut self, number: u32) -> Option<&mut EdlEvent> {
self.events.iter_mut().find(|e| e.number == number)
}
pub fn remove_event(&mut self, number: u32) -> Option<EdlEvent> {
if let Some(index) = self.events.iter().position(|e| e.number == number) {
Some(self.events.remove(index))
} else {
None
}
}
#[must_use]
pub fn event_count(&self) -> usize {
self.events.len()
}
#[must_use]
pub fn total_duration_frames(&self) -> u64 {
self.events.iter().map(|e| e.duration_frames()).sum()
}
#[must_use]
pub fn total_duration_seconds(&self) -> f64 {
self.total_duration_frames() as f64 / self.frame_rate.fps() as f64
}
pub fn sort_events(&mut self) {
self.events.sort_by_key(|e| e.record_in.to_frames());
}
pub fn renumber_events(&mut self) {
for (i, event) in self.events.iter_mut().enumerate() {
event.number = (i + 1) as u32;
}
}
pub fn validate(&self) -> EdlResult<()> {
let validator = EdlValidator::default();
validator.validate(self)?;
Ok(())
}
pub fn to_string_format(&self) -> EdlResult<String> {
let generator = EdlGenerator::new();
generator.generate(self)
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(input: &str) -> EdlResult<Self> {
parse_edl(input)
}
pub fn from_file(path: &std::path::Path) -> EdlResult<Self> {
let content = std::fs::read_to_string(path)?;
let mut edl = parse_edl(&content)?;
edl.set_source_file(path.to_path_buf());
Ok(edl)
}
pub fn to_file(&self, path: &std::path::Path) -> EdlResult<()> {
let generator = EdlGenerator::new();
generator.generate_to_file(self, path)
}
}
impl Default for Edl {
fn default() -> Self {
Self::cmx3600()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{EditType, TrackType};
use crate::timecode::EdlTimecode;
#[test]
fn test_create_edl() {
let edl = Edl::new(EdlFormat::Cmx3600);
assert_eq!(edl.format, EdlFormat::Cmx3600);
assert_eq!(edl.events.len(), 0);
}
#[test]
fn test_add_event() {
let mut edl = Edl::new(EdlFormat::Cmx3600);
edl.set_frame_rate(EdlFrameRate::Fps25);
let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
let event = EdlEvent::new(
1,
"A001".to_string(),
TrackType::Video,
EditType::Cut,
tc1,
tc2,
tc1,
tc2,
);
edl.add_event(event).expect("add_event should succeed");
assert_eq!(edl.events.len(), 1);
}
#[test]
fn test_get_event() {
let mut edl = Edl::new(EdlFormat::Cmx3600);
edl.set_frame_rate(EdlFrameRate::Fps25);
let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
let event = EdlEvent::new(
1,
"A001".to_string(),
TrackType::Video,
EditType::Cut,
tc1,
tc2,
tc1,
tc2,
);
edl.add_event(event).expect("add_event should succeed");
let retrieved = edl.get_event(1).expect("get_event should succeed");
assert_eq!(retrieved.number, 1);
}
#[test]
fn test_remove_event() {
let mut edl = Edl::new(EdlFormat::Cmx3600);
edl.set_frame_rate(EdlFrameRate::Fps25);
let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
let event = EdlEvent::new(
1,
"A001".to_string(),
TrackType::Video,
EditType::Cut,
tc1,
tc2,
tc1,
tc2,
);
edl.add_event(event).expect("add_event should succeed");
assert_eq!(edl.events.len(), 1);
edl.remove_event(1);
assert_eq!(edl.events.len(), 0);
}
#[test]
fn test_renumber_events() {
let mut edl = Edl::new(EdlFormat::Cmx3600);
edl.set_frame_rate(EdlFrameRate::Fps25);
let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
let event1 = EdlEvent::new(
10,
"A001".to_string(),
TrackType::Video,
EditType::Cut,
tc1,
tc2,
tc1,
tc2,
);
let event2 = EdlEvent::new(
20,
"A002".to_string(),
TrackType::Video,
EditType::Cut,
tc1,
tc2,
tc1,
tc2,
);
edl.add_event(event1).expect("add_event should succeed");
edl.add_event(event2).expect("add_event should succeed");
edl.renumber_events();
assert_eq!(edl.events[0].number, 1);
assert_eq!(edl.events[1].number, 2);
}
#[test]
fn test_total_duration() {
let mut edl = Edl::new(EdlFormat::Cmx3600);
edl.set_frame_rate(EdlFrameRate::Fps25);
let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
let event = EdlEvent::new(
1,
"A001".to_string(),
TrackType::Video,
EditType::Cut,
tc1,
tc2,
tc1,
tc2,
);
edl.add_event(event).expect("add_event should succeed");
let duration_frames = edl.total_duration_frames();
assert_eq!(duration_frames, 125);
let duration_seconds = edl.total_duration_seconds();
assert!((duration_seconds - 5.0).abs() < f64::EPSILON);
}
#[test]
fn test_edl_format_display() {
assert_eq!(EdlFormat::Cmx3600.to_string(), "CMX 3600");
assert_eq!(EdlFormat::Cmx3400.to_string(), "CMX 3400");
assert_eq!(EdlFormat::Gvg.to_string(), "GVG");
}
#[test]
fn test_parse_and_generate_roundtrip() {
let edl_text = r#"TITLE: Test EDL
FCM: NON-DROP FRAME
001 AX V C 01:00:00:00 01:00:05:00 01:00:00:00 01:00:05:00
"#;
let edl = parse_edl(edl_text).expect("operation should succeed");
let generated = edl.to_string_format().expect("formatting should succeed");
let edl2 = parse_edl(&generated).expect("operation should succeed");
assert_eq!(edl.title, edl2.title);
assert_eq!(edl.events.len(), edl2.events.len());
assert_eq!(edl.events[0].number, edl2.events[0].number);
}
#[test]
fn test_cmx3600_roundtrip() {
let mut edl = Edl::new(EdlFormat::Cmx3600);
edl.set_title("Roundtrip Test EDL".to_string());
edl.set_frame_rate(EdlFrameRate::Fps25);
let reels = ["ALPHA", "BETA", "GAMMA"];
for (i, reel) in reels.iter().enumerate() {
let n = i as u8;
let tc_in = EdlTimecode::new(1, 0, n * 5, 0, EdlFrameRate::Fps25).expect("valid tc_in");
let tc_out =
EdlTimecode::new(1, 0, n * 5 + 5, 0, EdlFrameRate::Fps25).expect("valid tc_out");
let event = EdlEvent::new(
(i + 1) as u32,
reel.to_string(),
TrackType::Video,
EditType::Cut,
tc_in,
tc_out,
tc_in,
tc_out,
);
edl.add_event(event).expect("add_event should succeed");
}
assert_eq!(
edl.events.len(),
3,
"EDL should have 3 events before serialisation"
);
let cmx_string = edl
.to_string_format()
.expect("serialisation should succeed");
assert!(
cmx_string.contains("TITLE: Roundtrip Test EDL"),
"generated text should contain the title"
);
let edl2 = parse_edl(&cmx_string).expect("re-parse should succeed");
assert_eq!(
edl2.events.len(),
3,
"re-parsed EDL should have 3 events; generated text:\n{cmx_string}"
);
for (orig_event, reparsed_event) in edl.events.iter().zip(edl2.events.iter()) {
assert_eq!(
orig_event.reel, reparsed_event.reel,
"reel name should survive the roundtrip"
);
}
}
}