use crate::composition::{
CompositionMob, Sequence, SequenceComponent, SourceClip, Track, TrackType,
};
use crate::dictionary::Auid;
use crate::object_model::Segment;
use crate::timeline::{EditRate, Position};
use crate::{AafError, AafFile, ContentStorage, EssenceData, Result};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct MergeOptions {
pub master_comp_name: String,
pub create_master_comp: bool,
pub master_edit_rate: EditRate,
}
impl Default for MergeOptions {
fn default() -> Self {
Self {
master_comp_name: "Merged Composition".to_string(),
create_master_comp: true,
master_edit_rate: EditRate::PAL_25,
}
}
}
impl MergeOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.master_comp_name = name.into();
self
}
#[must_use]
pub fn with_master_comp(mut self, create: bool) -> Self {
self.create_master_comp = create;
self
}
#[must_use]
pub fn with_edit_rate(mut self, rate: EditRate) -> Self {
self.master_edit_rate = rate;
self
}
}
pub fn merge_aaf_files(files: &[AafFile], options: &MergeOptions) -> Result<AafFile> {
if files.is_empty() {
return Err(AafError::InvalidFile(
"merge_aaf_files: no input files provided".to_string(),
));
}
let mut uuid_remap: HashMap<Uuid, Uuid> = HashMap::new();
let mut source_comp_mob_ids: Vec<Uuid> = Vec::new();
let mut merged_storage = ContentStorage::new();
let mut merged_essence: Vec<EssenceData> = Vec::new();
for file in files {
for mob in file.master_mobs().iter().chain(file.source_mobs().iter()) {
let new_id = if merged_storage.find_mob(&mob.mob_id()).is_some() {
let fresh = Uuid::new_v4();
uuid_remap.insert(mob.mob_id(), fresh);
fresh
} else {
mob.mob_id()
};
let mut remapped = (*mob).clone();
*remapped.mob_id_mut() = new_id;
merged_storage.add_mob(remapped);
}
for comp_mob in file.composition_mobs() {
let new_id = if merged_storage
.find_composition_mob(&comp_mob.mob_id())
.is_some()
{
let fresh = Uuid::new_v4();
uuid_remap.insert(comp_mob.mob_id(), fresh);
fresh
} else {
comp_mob.mob_id()
};
let mut remapped = comp_mob.clone();
*remapped.mob_mut().mob_id_mut() = new_id;
remap_composition_mob_refs(&mut remapped, &uuid_remap);
source_comp_mob_ids.push(new_id);
merged_storage.add_composition_mob(remapped);
}
for essence in file.essence_data() {
let remapped_id = uuid_remap
.get(&essence.mob_id())
.copied()
.unwrap_or_else(|| essence.mob_id());
merged_essence.push(EssenceData::new(remapped_id, essence.data().to_vec()));
}
}
if options.create_master_comp && !source_comp_mob_ids.is_empty() {
let master_comp = build_master_composition(&source_comp_mob_ids, &merged_storage, options)?;
merged_storage.add_composition_mob(master_comp);
}
let first = &files[0];
Ok(AafFile {
header: first.header().clone(),
dictionary: first.dictionary().clone(),
content_storage: merged_storage,
essence_data: merged_essence,
})
}
fn remap_composition_mob_refs(comp: &mut CompositionMob, remap: &HashMap<Uuid, Uuid>) {
if remap.is_empty() {
return;
}
for track in comp.tracks_mut() {
if let Some(ref mut seg_box) = track.segment {
if let Segment::Sequence(ref mut seq) = **seg_box {
for component in &mut seq.components {
if let Segment::SourceClip(ref mut clip) = component.segment {
if let Some(&new_id) = remap.get(&clip.source_mob_id) {
clip.source_mob_id = new_id;
}
}
}
}
}
}
}
fn build_master_composition(
source_ids: &[Uuid],
storage: &ContentStorage,
options: &MergeOptions,
) -> Result<CompositionMob> {
let master_id = Uuid::new_v4();
let mut master = CompositionMob::new(master_id, &options.master_comp_name);
let mut video_seq = Sequence::new(Auid::PICTURE);
for &src_id in source_ids {
let duration = storage
.find_composition_mob(&src_id)
.and_then(|c| {
c.picture_tracks()
.into_iter()
.next()
.and_then(|t| t.duration())
})
.unwrap_or(0);
if duration > 0 {
video_seq.add_component(SequenceComponent::SourceClip(SourceClip::new(
duration,
Position::zero(),
src_id,
1, )));
}
}
let mut video_track = Track::new(1, "V1", options.master_edit_rate, TrackType::Picture);
video_track.set_sequence(video_seq);
master.add_track(video_track);
Ok(master)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::composition::{Filler, Sequence, SequenceComponent, SourceClip, Track, TrackType};
use crate::dictionary::Auid;
use crate::object_model::{Mob, MobType};
use crate::timeline::{EditRate, Position};
use crate::{AafFile, ContentStorage};
use uuid::Uuid;
fn make_simple_aaf(name: &str, clip_duration: i64) -> AafFile {
let mut storage = ContentStorage::new();
let source_id = Uuid::new_v4();
storage.add_mob(Mob::new(
source_id,
"source.mov".to_string(),
MobType::Source,
));
let comp_id = Uuid::new_v4();
let mut comp = CompositionMob::new(comp_id, name);
let mut seq = Sequence::new(Auid::PICTURE);
seq.add_component(SequenceComponent::SourceClip(SourceClip::new(
clip_duration,
Position::zero(),
source_id,
1,
)));
let mut track = Track::new(1, "V1", EditRate::PAL_25, TrackType::Picture);
track.set_sequence(seq);
comp.add_track(track);
storage.add_composition_mob(comp);
AafFile {
header: crate::object_model::Header::new(),
dictionary: crate::dictionary::Dictionary::new(),
content_storage: storage,
essence_data: Vec::new(),
}
}
#[test]
fn test_merge_empty_files_returns_error() {
let options = MergeOptions::default();
let result = merge_aaf_files(&[], &options);
assert!(result.is_err());
}
#[test]
fn test_merge_single_file() {
let file = make_simple_aaf("Edit1", 100);
let options = MergeOptions::new().with_master_comp(false);
let merged = merge_aaf_files(&[file], &options).expect("merge should succeed");
assert_eq!(merged.composition_mobs().len(), 1);
}
#[test]
fn test_merge_two_files_without_master_comp() {
let file1 = make_simple_aaf("Edit1", 100);
let file2 = make_simple_aaf("Edit2", 200);
let options = MergeOptions::new().with_master_comp(false);
let merged = merge_aaf_files(&[file1, file2], &options).expect("merge should succeed");
assert_eq!(merged.composition_mobs().len(), 2);
}
#[test]
fn test_merge_two_files_with_master_comp() {
let file1 = make_simple_aaf("Edit1", 100);
let file2 = make_simple_aaf("Edit2", 200);
let options = MergeOptions::new()
.with_master_comp(true)
.with_name("Big Merge");
let merged = merge_aaf_files(&[file1, file2], &options).expect("merge should succeed");
assert_eq!(merged.composition_mobs().len(), 3);
let master = merged
.content_storage()
.find_composition_mob_by_name("Big Merge")
.expect("master comp should exist");
assert!(!master.picture_tracks().is_empty());
}
#[test]
fn test_merge_master_comp_duration() {
let file1 = make_simple_aaf("A", 100);
let file2 = make_simple_aaf("B", 50);
let options = MergeOptions::new()
.with_master_comp(true)
.with_name("Combined");
let merged = merge_aaf_files(&[file1, file2], &options).expect("merge should succeed");
let master = merged
.content_storage()
.find_composition_mob_by_name("Combined")
.expect("master comp");
assert_eq!(master.duration(), Some(150));
}
#[test]
fn test_merge_uuid_collision_resolved() {
let shared_id = Uuid::new_v4();
let make_file = |clip: i64| -> AafFile {
let mut storage = ContentStorage::new();
let mut comp = CompositionMob::new(shared_id, "Comp");
let mut seq = Sequence::new(Auid::PICTURE);
seq.add_component(SequenceComponent::Filler(Filler::new(clip)));
let mut track = Track::new(1, "V", EditRate::PAL_25, TrackType::Picture);
track.set_sequence(seq);
comp.add_track(track);
storage.add_composition_mob(comp);
AafFile {
header: crate::object_model::Header::new(),
dictionary: crate::dictionary::Dictionary::new(),
content_storage: storage,
essence_data: Vec::new(),
}
};
let f1 = make_file(100);
let f2 = make_file(200);
let options = MergeOptions::new().with_master_comp(false);
let merged = merge_aaf_files(&[f1, f2], &options).expect("merge");
assert_eq!(merged.composition_mobs().len(), 2);
let ids: Vec<Uuid> = merged
.composition_mobs()
.iter()
.map(|c| c.mob_id())
.collect();
assert_ne!(ids[0], ids[1]);
}
#[test]
fn test_merge_essence_data_concatenated() {
let mob1 = Uuid::new_v4();
let mob2 = Uuid::new_v4();
let mut storage = ContentStorage::new();
let comp = CompositionMob::new(Uuid::new_v4(), "E");
storage.add_composition_mob(comp);
let file1 = AafFile {
header: crate::object_model::Header::new(),
dictionary: crate::dictionary::Dictionary::new(),
content_storage: storage,
essence_data: vec![EssenceData::new(mob1, vec![1, 2, 3])],
};
let mut storage2 = ContentStorage::new();
let comp2 = CompositionMob::new(Uuid::new_v4(), "F");
storage2.add_composition_mob(comp2);
let file2 = AafFile {
header: crate::object_model::Header::new(),
dictionary: crate::dictionary::Dictionary::new(),
content_storage: storage2,
essence_data: vec![EssenceData::new(mob2, vec![4, 5, 6])],
};
let options = MergeOptions::new().with_master_comp(false);
let merged = merge_aaf_files(&[file1, file2], &options).expect("merge");
assert_eq!(merged.essence_data().len(), 2);
}
}