#![allow(dead_code)]
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExportTarget {
Cmx3600Edl,
OpenTimelineIo,
AvidInterchange,
FcpXml,
DaVinciResolve,
RawAaf,
}
impl ExportTarget {
#[must_use]
pub fn supports_markers(self) -> bool {
matches!(
self,
Self::OpenTimelineIo | Self::FcpXml | Self::DaVinciResolve | Self::RawAaf
)
}
#[must_use]
pub fn is_text_format(self) -> bool {
matches!(self, Self::Cmx3600Edl | Self::OpenTimelineIo | Self::FcpXml)
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Cmx3600Edl => "CMX 3600 EDL",
Self::OpenTimelineIo => "OpenTimelineIO",
Self::AvidInterchange => "Avid Interchange",
Self::FcpXml => "FCP XML",
Self::DaVinciResolve => "DaVinci Resolve",
Self::RawAaf => "Raw AAF",
}
}
}
#[derive(Debug, Clone)]
pub struct ExportEvent {
pub number: u32,
pub reel_name: String,
pub source_in: i64,
pub source_out: i64,
pub record_in: i64,
pub record_out: i64,
pub comment: Option<String>,
}
impl ExportEvent {
#[must_use]
pub fn new(
number: u32,
reel_name: impl Into<String>,
source_in: i64,
source_out: i64,
record_in: i64,
record_out: i64,
) -> Self {
Self {
number,
reel_name: reel_name.into(),
source_in,
source_out,
record_in,
record_out,
comment: None,
}
}
#[must_use]
pub fn duration_frames(&self) -> i64 {
(self.record_out - self.record_in).max(0)
}
}
#[derive(Debug, Clone, Default)]
pub struct AafTimelineExport {
events: Vec<ExportEvent>,
}
impl AafTimelineExport {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add_event(&mut self, event: ExportEvent) {
self.events.push(event);
}
#[must_use]
pub fn event_count(&self) -> usize {
self.events.len()
}
#[must_use]
pub fn events(&self) -> &[ExportEvent] {
&self.events
}
#[must_use]
pub fn total_duration_frames(&self) -> i64 {
self.events.iter().map(|e| e.duration_frames()).sum()
}
#[must_use]
pub fn referenced_reels(&self) -> Vec<&str> {
let mut seen: HashMap<&str, ()> = HashMap::new();
for e in &self.events {
seen.insert(e.reel_name.as_str(), ());
}
let mut reels: Vec<&str> = seen.into_keys().collect();
reels.sort_unstable();
reels
}
}
#[derive(Debug, Clone)]
pub struct AafExportConfig {
pub target: ExportTarget,
pub embed_metadata: bool,
pub high_fidelity: bool,
pub include_audio: bool,
pub include_video: bool,
pub max_reel_name_len: Option<usize>,
}
impl AafExportConfig {
#[must_use]
pub fn for_target(target: ExportTarget) -> Self {
Self {
target,
embed_metadata: true,
high_fidelity: false,
include_audio: true,
include_video: true,
max_reel_name_len: None,
}
}
#[must_use]
pub fn is_high_fidelity(&self) -> bool {
self.high_fidelity
}
pub fn enable_high_fidelity(&mut self) {
self.high_fidelity = true;
}
pub fn disable_metadata(&mut self) {
self.embed_metadata = false;
}
}
impl Default for AafExportConfig {
fn default() -> Self {
Self::for_target(ExportTarget::Cmx3600Edl)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cmx3600_does_not_support_markers() {
assert!(!ExportTarget::Cmx3600Edl.supports_markers());
}
#[test]
fn test_otio_supports_markers() {
assert!(ExportTarget::OpenTimelineIo.supports_markers());
}
#[test]
fn test_raw_aaf_supports_markers() {
assert!(ExportTarget::RawAaf.supports_markers());
}
#[test]
fn test_fcpxml_is_text_format() {
assert!(ExportTarget::FcpXml.is_text_format());
}
#[test]
fn test_avid_not_text_format() {
assert!(!ExportTarget::AvidInterchange.is_text_format());
}
#[test]
fn test_target_name() {
assert_eq!(ExportTarget::Cmx3600Edl.name(), "CMX 3600 EDL");
assert_eq!(ExportTarget::DaVinciResolve.name(), "DaVinci Resolve");
}
#[test]
fn test_event_duration() {
let ev = ExportEvent::new(1, "REEL_A", 0, 100, 0, 100);
assert_eq!(ev.duration_frames(), 100);
}
#[test]
fn test_event_zero_duration_clamped() {
let ev = ExportEvent::new(1, "REEL_A", 0, 0, 50, 40);
assert_eq!(ev.duration_frames(), 0);
}
#[test]
fn test_export_add_event_count() {
let mut export = AafTimelineExport::new();
assert_eq!(export.event_count(), 0);
export.add_event(ExportEvent::new(1, "R1", 0, 50, 0, 50));
export.add_event(ExportEvent::new(2, "R2", 50, 100, 50, 100));
assert_eq!(export.event_count(), 2);
}
#[test]
fn test_export_total_duration() {
let mut export = AafTimelineExport::new();
export.add_event(ExportEvent::new(1, "R1", 0, 25, 0, 25));
export.add_event(ExportEvent::new(2, "R1", 25, 75, 25, 75));
assert_eq!(export.total_duration_frames(), 75);
}
#[test]
fn test_export_referenced_reels_unique_sorted() {
let mut export = AafTimelineExport::new();
export.add_event(ExportEvent::new(1, "REEL_B", 0, 10, 0, 10));
export.add_event(ExportEvent::new(2, "REEL_A", 0, 10, 10, 20));
export.add_event(ExportEvent::new(3, "REEL_B", 0, 5, 20, 25));
let reels = export.referenced_reels();
assert_eq!(reels, vec!["REEL_A", "REEL_B"]);
}
#[test]
fn test_export_empty_reels() {
let export = AafTimelineExport::new();
assert!(export.referenced_reels().is_empty());
}
#[test]
fn test_config_default_not_high_fidelity() {
let cfg = AafExportConfig::for_target(ExportTarget::OpenTimelineIo);
assert!(!cfg.is_high_fidelity());
}
#[test]
fn test_config_enable_high_fidelity() {
let mut cfg = AafExportConfig::for_target(ExportTarget::OpenTimelineIo);
cfg.enable_high_fidelity();
assert!(cfg.is_high_fidelity());
}
#[test]
fn test_config_default_embed_metadata() {
let cfg = AafExportConfig::default();
assert!(cfg.embed_metadata);
}
#[test]
fn test_config_disable_metadata() {
let mut cfg = AafExportConfig::default();
cfg.disable_metadata();
assert!(!cfg.embed_metadata);
}
}