Skip to main content

oximedia_edit/edl/
mod.rs

1//! Edit Decision List (EDL) parsing and generation.
2//!
3//! This module provides comprehensive support for various EDL formats used in
4//! professional video editing:
5//!
6//! - **CMX 3600**: Industry-standard EDL format for linear editing systems
7//! - **ALE (Avid Log Exchange)**: Metadata-rich format for Avid systems
8//! - **FCPXML**: Final Cut Pro X XML project format
9//! - **AAF-EDL**: Advanced Authoring Format style EDL representation
10//! - **OpenTimelineIO**: Modern interchange format for editorial data
11//!
12//! # Format Conversion
13//!
14//! The module supports bidirectional conversion between formats, with automatic
15//! handling of feature mapping and lossy conversion warnings:
16//!
17//! ```no_run
18//! use oximedia_edit::edl::{Edl, EdlFormat};
19//!
20//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
21//! // Parse CMX 3600 EDL
22//! let cmx_content = std::fs::read_to_string("timeline.edl")?;
23//! let edl = Edl::parse(&cmx_content, EdlFormat::Cmx3600)?;
24//!
25//! // Convert to FCPXML
26//! let fcpxml = edl.to_format(EdlFormat::FcpXml)?;
27//! std::fs::write("timeline.fcpxml", fcpxml)?;
28//! # Ok(())
29//! # }
30//! ```
31//!
32//! # Validation
33//!
34//! Built-in validation ensures EDL integrity:
35//!
36//! ```no_run
37//! use oximedia_edit::edl::{Edl, validator::EdlValidator};
38//!
39//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
40//! # let edl_content = "";
41//! let edl = Edl::parse(&edl_content, oximedia_edit::edl::EdlFormat::Cmx3600)?;
42//!
43//! let validator = EdlValidator::new();
44//! let report = validator.validate(&edl)?;
45//!
46//! if !report.is_valid() {
47//!     for error in &report.errors {
48//!         eprintln!("Error: {}", error);
49//!     }
50//! }
51//! # Ok(())
52//! # }
53//! ```
54
55pub mod aaf_edl;
56pub mod ale;
57pub mod cmx3600;
58pub mod converter;
59pub mod fcpxml;
60pub mod otio;
61pub mod validator;
62
63use oximedia_core::Rational;
64use std::collections::HashMap;
65use thiserror::Error;
66
67/// EDL error types.
68#[derive(Debug, Error)]
69pub enum EdlError {
70    /// Parse error with context.
71    #[error("Parse error at line {line}: {message}")]
72    ParseError {
73        /// Line number where error occurred.
74        line: usize,
75        /// Error message.
76        message: String,
77    },
78
79    /// Invalid timecode format.
80    #[error("Invalid timecode: {0}")]
81    InvalidTimecode(String),
82
83    /// Invalid edit number.
84    #[error("Invalid edit number: {0}")]
85    InvalidEditNumber(u32),
86
87    /// Unsupported feature.
88    #[error("Unsupported feature: {0}")]
89    UnsupportedFeature(String),
90
91    /// Conversion error.
92    #[error("Conversion error: {0}")]
93    ConversionError(String),
94
95    /// Validation error.
96    #[error("Validation error: {0}")]
97    ValidationError(String),
98
99    /// IO error.
100    #[error("IO error: {0}")]
101    IoError(#[from] std::io::Error),
102
103    /// XML error.
104    #[error("XML error: {0}")]
105    XmlError(String),
106
107    /// JSON error.
108    #[error("JSON error: {0}")]
109    JsonError(String),
110
111    /// Missing required field.
112    #[error("Missing required field: {0}")]
113    MissingField(String),
114
115    /// Invalid format.
116    #[error("Invalid format: {0}")]
117    InvalidFormat(String),
118}
119
120/// Result type for EDL operations.
121pub type EdlResult<T> = Result<T, EdlError>;
122
123/// Supported EDL formats.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125pub enum EdlFormat {
126    /// CMX 3600 format.
127    Cmx3600,
128    /// ALE (Avid Log Exchange) format.
129    Ale,
130    /// FCPXML (Final Cut Pro XML) format.
131    FcpXml,
132    /// AAF-style EDL format.
133    AafEdl,
134    /// OpenTimelineIO JSON format.
135    Otio,
136}
137
138/// Timecode representation.
139#[derive(Debug, Clone, PartialEq)]
140pub struct Timecode {
141    /// Hours (0-23).
142    pub hours: u8,
143    /// Minutes (0-59).
144    pub minutes: u8,
145    /// Seconds (0-59).
146    pub seconds: u8,
147    /// Frames (0-fps).
148    pub frames: u8,
149    /// Drop-frame flag.
150    pub drop_frame: bool,
151    /// Frame rate.
152    pub frame_rate: Rational,
153}
154
155impl Timecode {
156    /// Create a new timecode.
157    #[must_use]
158    pub fn new(
159        hours: u8,
160        minutes: u8,
161        seconds: u8,
162        frames: u8,
163        drop_frame: bool,
164        frame_rate: Rational,
165    ) -> Self {
166        Self {
167            hours,
168            minutes,
169            seconds,
170            frames,
171            drop_frame,
172            frame_rate,
173        }
174    }
175
176    /// Create timecode from frame count.
177    #[must_use]
178    pub fn from_frames(frames: i64, frame_rate: Rational, drop_frame: bool) -> Self {
179        let fps = frame_rate.to_f64() as i64;
180        let frames_per_minute = fps * 60;
181        let frames_per_hour = frames_per_minute * 60;
182
183        let hours = (frames / frames_per_hour) as u8;
184        let remaining = frames % frames_per_hour;
185        let minutes = (remaining / frames_per_minute) as u8;
186        let remaining = remaining % frames_per_minute;
187        let seconds = (remaining / fps) as u8;
188        let frames = (remaining % fps) as u8;
189
190        Self::new(hours, minutes, seconds, frames, drop_frame, frame_rate)
191    }
192
193    /// Convert timecode to frame count.
194    #[must_use]
195    pub fn to_frames(&self) -> i64 {
196        let fps = self.frame_rate.to_f64() as i64;
197        let total_seconds =
198            i64::from(self.hours) * 3600 + i64::from(self.minutes) * 60 + i64::from(self.seconds);
199        total_seconds * fps + i64::from(self.frames)
200    }
201
202    /// Parse timecode from string (HH:MM:SS:FF or HH:MM:SS;FF for drop-frame).
203    pub fn parse(s: &str, frame_rate: Rational) -> EdlResult<Self> {
204        let parts: Vec<&str> = s.split(&[':', ';'][..]).collect();
205        if parts.len() != 4 {
206            return Err(EdlError::InvalidTimecode(s.to_string()));
207        }
208
209        let hours = parts[0]
210            .parse()
211            .map_err(|_| EdlError::InvalidTimecode(s.to_string()))?;
212        let minutes = parts[1]
213            .parse()
214            .map_err(|_| EdlError::InvalidTimecode(s.to_string()))?;
215        let seconds = parts[2]
216            .parse()
217            .map_err(|_| EdlError::InvalidTimecode(s.to_string()))?;
218        let frames = parts[3]
219            .parse()
220            .map_err(|_| EdlError::InvalidTimecode(s.to_string()))?;
221        let drop_frame = s.contains(';');
222
223        Ok(Self::new(
224            hours, minutes, seconds, frames, drop_frame, frame_rate,
225        ))
226    }
227
228    /// Format timecode as string.
229    #[must_use]
230    pub fn format(&self) -> String {
231        let separator = if self.drop_frame { ';' } else { ':' };
232        format!(
233            "{:02}:{:02}:{:02}{}{:02}",
234            self.hours, self.minutes, self.seconds, separator, self.frames
235        )
236    }
237}
238
239/// Edit type (transition type).
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub enum EditType {
242    /// Cut (instant transition).
243    Cut,
244    /// Dissolve (cross-fade).
245    Dissolve,
246    /// Wipe transition.
247    Wipe,
248    /// Key (overlay/composite).
249    Key,
250}
251
252/// Edit event in an EDL.
253#[derive(Debug, Clone)]
254pub struct EdlEvent {
255    /// Event number.
256    pub number: u32,
257    /// Source reel/tape name.
258    pub reel: String,
259    /// Track type (V for video, A for audio, etc.).
260    pub track: String,
261    /// Edit type.
262    pub edit_type: EditType,
263    /// Source in timecode.
264    pub source_in: Timecode,
265    /// Source out timecode.
266    pub source_out: Timecode,
267    /// Record in timecode.
268    pub record_in: Timecode,
269    /// Record out timecode.
270    pub record_out: Timecode,
271    /// Transition duration (in frames, for dissolves/wipes).
272    pub transition_duration: Option<u32>,
273    /// Motion effects (speed changes, freeze frames, etc.).
274    pub motion_effect: Option<MotionEffect>,
275    /// Comments associated with this event.
276    pub comments: Vec<String>,
277    /// Additional metadata.
278    pub metadata: HashMap<String, String>,
279}
280
281/// Motion effect specification.
282#[derive(Debug, Clone)]
283pub struct MotionEffect {
284    /// Speed multiplier (1.0 = normal, 0.5 = half speed, 2.0 = double speed).
285    pub speed: f64,
286    /// Freeze frame flag.
287    pub freeze: bool,
288    /// Reverse motion flag.
289    pub reverse: bool,
290    /// Entry point (timecode).
291    pub entry: Option<Timecode>,
292}
293
294/// Complete EDL structure.
295#[derive(Debug, Clone)]
296pub struct Edl {
297    /// Title of the EDL.
298    pub title: String,
299    /// Frame rate.
300    pub frame_rate: Rational,
301    /// Drop-frame flag.
302    pub drop_frame: bool,
303    /// List of events.
304    pub events: Vec<EdlEvent>,
305    /// Global comments.
306    pub comments: Vec<String>,
307    /// Additional metadata.
308    pub metadata: HashMap<String, String>,
309}
310
311impl Edl {
312    /// Create a new empty EDL.
313    #[must_use]
314    pub fn new(title: String, frame_rate: Rational, drop_frame: bool) -> Self {
315        Self {
316            title,
317            frame_rate,
318            drop_frame,
319            events: Vec::new(),
320            comments: Vec::new(),
321            metadata: HashMap::new(),
322        }
323    }
324
325    /// Parse EDL from string with specified format.
326    pub fn parse(content: &str, format: EdlFormat) -> EdlResult<Self> {
327        match format {
328            EdlFormat::Cmx3600 => cmx3600::parse(content),
329            EdlFormat::Ale => ale::parse(content),
330            EdlFormat::FcpXml => fcpxml::parse(content),
331            EdlFormat::AafEdl => aaf_edl::parse(content),
332            EdlFormat::Otio => otio::parse(content),
333        }
334    }
335
336    /// Convert EDL to specified format.
337    pub fn to_format(&self, format: EdlFormat) -> EdlResult<String> {
338        match format {
339            EdlFormat::Cmx3600 => cmx3600::write(self),
340            EdlFormat::Ale => ale::write(self),
341            EdlFormat::FcpXml => fcpxml::write(self),
342            EdlFormat::AafEdl => aaf_edl::write(self),
343            EdlFormat::Otio => otio::write(self),
344        }
345    }
346
347    /// Add an event to the EDL.
348    pub fn add_event(&mut self, event: EdlEvent) {
349        self.events.push(event);
350    }
351
352    /// Get event by number.
353    #[must_use]
354    pub fn get_event(&self, number: u32) -> Option<&EdlEvent> {
355        self.events.iter().find(|e| e.number == number)
356    }
357
358    /// Sort events by record in timecode.
359    pub fn sort_events(&mut self) {
360        self.events.sort_by_key(|e| e.record_in.to_frames());
361    }
362
363    /// Validate the EDL.
364    pub fn validate(&self) -> EdlResult<validator::ValidationReport> {
365        validator::EdlValidator::new().validate(self)
366    }
367}