use crate::dictionary::Auid;
use crate::object_model::{
Component, FillerSegment, Mob, MobSlot, MobType, OperationGroupSegment, Segment,
SequenceSegment, SourceClipSegment, TransitionSegment,
};
use crate::timeline::{EditRate, Position};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct CompositionMob {
mob: Mob,
pub default_fade_length: Option<i64>,
pub default_fade_type: Option<FadeType>,
pub usage_code: Option<UsageCode>,
}
impl CompositionMob {
pub fn new(mob_id: Uuid, name: impl Into<String>) -> Self {
Self {
mob: Mob::new(mob_id, name.into(), MobType::Composition),
default_fade_length: None,
default_fade_type: None,
usage_code: None,
}
}
#[must_use]
pub fn mob_id(&self) -> Uuid {
self.mob.mob_id()
}
#[must_use]
pub fn name(&self) -> &str {
self.mob.name()
}
#[must_use]
pub fn tracks(&self) -> Vec<Track> {
self.mob
.slots()
.iter()
.map(|slot| Track::from_mob_slot(slot.clone()))
.collect()
}
#[must_use]
pub fn get_track(&self, slot_id: u32) -> Option<Track> {
self.mob
.get_slot(slot_id)
.map(|slot| Track::from_mob_slot(slot.clone()))
}
pub fn add_track(&mut self, track: Track) {
self.mob.add_slot(track.into_mob_slot());
}
#[must_use]
pub fn edit_rate(&self) -> Option<EditRate> {
self.tracks().first().map(|t| t.edit_rate)
}
#[must_use]
pub fn duration(&self) -> Option<i64> {
self.tracks().iter().filter_map(Track::duration).max()
}
#[must_use]
pub fn picture_tracks(&self) -> Vec<Track> {
self.tracks()
.into_iter()
.filter(Track::is_picture)
.collect()
}
#[must_use]
pub fn sound_tracks(&self) -> Vec<Track> {
self.tracks().into_iter().filter(Track::is_sound).collect()
}
#[must_use]
pub fn timecode_tracks(&self) -> Vec<Track> {
self.tracks()
.into_iter()
.filter(Track::is_timecode)
.collect()
}
#[must_use]
pub fn mob(&self) -> &Mob {
&self.mob
}
pub fn mob_mut(&mut self) -> &mut Mob {
&mut self.mob
}
pub fn set_default_fade(&mut self, length: i64, fade_type: FadeType) {
self.default_fade_length = Some(length);
self.default_fade_type = Some(fade_type);
}
pub fn set_usage_code(&mut self, code: UsageCode) {
self.usage_code = Some(code);
}
pub fn tracks_mut(&mut self) -> Vec<&mut MobSlot> {
self.mob.slots.iter_mut().collect()
}
pub fn insert_track_at(&mut self, index: usize, track: Track) {
let slot = track.into_mob_slot();
let len = self.mob.slots.len();
let idx = index.min(len);
self.mob.slots.insert(idx, slot);
}
pub fn remove_track_at(&mut self, index: usize) -> Option<Track> {
if index >= self.mob.slots.len() {
None
} else {
Some(Track::from_mob_slot(self.mob.slots.remove(index)))
}
}
pub fn move_track(&mut self, from_index: usize, to_index: usize) -> bool {
let len = self.mob.slots.len();
if from_index >= len || to_index >= len || from_index == to_index {
return false;
}
let slot = self.mob.slots.remove(from_index);
let adjusted_to = if to_index > from_index {
to_index - 1
} else {
to_index
};
self.mob.slots.insert(adjusted_to, slot);
true
}
pub fn reorder_tracks(&mut self, order: &[usize]) -> crate::Result<()> {
let len = self.mob.slots.len();
if order.len() != len {
return Err(crate::AafError::TimelineError(format!(
"reorder_tracks: order length {} != track count {}",
order.len(),
len
)));
}
let mut seen = vec![false; len];
for &idx in order {
if idx >= len {
return Err(crate::AafError::TimelineError(format!(
"reorder_tracks: index {idx} out of bounds (len={len})"
)));
}
if seen[idx] {
return Err(crate::AafError::TimelineError(format!(
"reorder_tracks: duplicate index {idx}"
)));
}
seen[idx] = true;
}
let old_slots = self.mob.slots.clone();
for (pos, &src_idx) in order.iter().enumerate() {
self.mob.slots[pos] = old_slots[src_idx].clone();
}
Ok(())
}
#[must_use]
pub fn track_count(&self) -> usize {
self.mob.slots.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FadeType {
Linear,
Logarithmic,
Exponential,
SCurve,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UsageCode {
TopLevel,
LowerLevel,
SubClip,
AdjustedClip,
Template,
}
#[derive(Debug, Clone)]
pub struct Track {
pub track_id: u32,
pub name: String,
pub edit_rate: EditRate,
pub origin: Position,
pub physical_track_number: Option<u32>,
pub sequence: Option<Sequence>,
pub track_type: TrackType,
}
impl Track {
pub fn new(
track_id: u32,
name: impl Into<String>,
edit_rate: EditRate,
track_type: TrackType,
) -> Self {
Self {
track_id,
name: name.into(),
edit_rate,
origin: Position::zero(),
physical_track_number: None,
sequence: None,
track_type,
}
}
#[must_use]
pub fn from_mob_slot(slot: MobSlot) -> Self {
let track_type = if let Some(ref segment) = slot.segment {
determine_track_type(segment.as_ref())
} else {
TrackType::Unknown
};
let sequence = if let Some(ref segment) = slot.segment {
extract_sequence(segment.as_ref())
} else {
None
};
Self {
track_id: slot.slot_id,
name: slot.name,
edit_rate: slot.edit_rate,
origin: slot.origin,
physical_track_number: slot.physical_track_number,
sequence,
track_type,
}
}
#[must_use]
pub fn into_mob_slot(self) -> MobSlot {
let segment = self
.sequence
.map(|sequence| Box::new(Segment::Sequence(sequence.into_segment())));
MobSlot {
slot_id: self.track_id,
name: self.name,
physical_track_number: self.physical_track_number,
edit_rate: self.edit_rate,
origin: self.origin,
segment,
slot_type: crate::object_model::SlotType::Timeline,
}
}
#[must_use]
pub fn duration(&self) -> Option<i64> {
self.sequence.as_ref().and_then(Sequence::duration)
}
#[must_use]
pub fn is_picture(&self) -> bool {
matches!(self.track_type, TrackType::Picture)
}
#[must_use]
pub fn is_sound(&self) -> bool {
matches!(self.track_type, TrackType::Sound)
}
#[must_use]
pub fn is_timecode(&self) -> bool {
matches!(self.track_type, TrackType::Timecode)
}
pub fn set_sequence(&mut self, sequence: Sequence) {
self.sequence = Some(sequence);
}
#[must_use]
pub fn source_clips(&self) -> Vec<&SourceClip> {
if let Some(ref sequence) = self.sequence {
sequence.source_clips()
} else {
Vec::new()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrackType {
Picture,
Sound,
Timecode,
Data,
Unknown,
}
#[derive(Debug, Clone)]
pub struct Sequence {
pub components: Vec<SequenceComponent>,
pub data_definition: Auid,
}
impl Sequence {
#[must_use]
pub fn new(data_definition: Auid) -> Self {
Self {
components: Vec::new(),
data_definition,
}
}
pub fn add_component(&mut self, component: SequenceComponent) {
self.components.push(component);
}
#[must_use]
pub fn duration(&self) -> Option<i64> {
let mut total = 0i64;
for component in &self.components {
total += component.length()?;
}
Some(total)
}
#[must_use]
pub fn source_clips(&self) -> Vec<&SourceClip> {
let mut clips = Vec::new();
for component in &self.components {
if let SequenceComponent::SourceClip(clip) = component {
clips.push(clip);
}
}
clips
}
#[must_use]
pub fn into_segment(self) -> SequenceSegment {
let components = self
.components
.into_iter()
.map(|c| c.into_component(self.data_definition))
.collect();
SequenceSegment {
components,
length: None,
}
}
#[must_use]
pub fn is_picture(&self) -> bool {
self.data_definition.is_picture()
}
#[must_use]
pub fn is_sound(&self) -> bool {
self.data_definition.is_sound()
}
}
#[derive(Debug, Clone)]
pub enum SequenceComponent {
SourceClip(SourceClip),
Filler(Filler),
Transition(Transition),
Effect(Effect),
}
impl SequenceComponent {
#[must_use]
pub fn length(&self) -> Option<i64> {
match self {
SequenceComponent::SourceClip(clip) => Some(clip.length),
SequenceComponent::Filler(filler) => Some(filler.length),
SequenceComponent::Transition(transition) => Some(transition.length),
SequenceComponent::Effect(effect) => effect.length,
}
}
#[must_use]
pub fn into_component(self, data_definition: Auid) -> Component {
let segment = match self {
SequenceComponent::SourceClip(clip) => Segment::SourceClip(clip.into_segment()),
SequenceComponent::Filler(filler) => Segment::Filler(filler.into_segment()),
SequenceComponent::Transition(transition) => {
Segment::Transition(transition.into_segment())
}
SequenceComponent::Effect(effect) => Segment::OperationGroup(effect.into_segment()),
};
Component::new(data_definition, segment)
}
}
#[derive(Debug, Clone)]
pub struct SourceClip {
pub length: i64,
pub start_time: Position,
pub source_mob_id: Uuid,
pub source_mob_slot_id: u32,
pub source_track_id: Option<u32>,
}
impl SourceClip {
#[must_use]
pub fn new(
length: i64,
start_time: Position,
source_mob_id: Uuid,
source_mob_slot_id: u32,
) -> Self {
Self {
length,
start_time,
source_mob_id,
source_mob_slot_id,
source_track_id: None,
}
}
#[must_use]
pub fn with_source_track_id(mut self, track_id: u32) -> Self {
self.source_track_id = Some(track_id);
self
}
#[must_use]
pub fn into_segment(self) -> SourceClipSegment {
SourceClipSegment::new(
self.length,
self.start_time,
self.source_mob_id,
self.source_mob_slot_id,
)
}
#[must_use]
pub fn end_time(&self) -> Position {
Position(self.start_time.0 + self.length)
}
}
#[derive(Debug, Clone)]
pub struct Filler {
pub length: i64,
}
impl Filler {
#[must_use]
pub fn new(length: i64) -> Self {
Self { length }
}
#[must_use]
pub fn into_segment(self) -> FillerSegment {
FillerSegment::new(self.length)
}
}
#[derive(Debug, Clone)]
pub struct Transition {
pub length: i64,
pub cut_point: Position,
pub effect: Option<Effect>,
}
impl Transition {
#[must_use]
pub fn new(length: i64, cut_point: Position) -> Self {
Self {
length,
cut_point,
effect: None,
}
}
#[must_use]
pub fn with_effect(mut self, effect: Effect) -> Self {
self.effect = Some(effect);
self
}
#[must_use]
pub fn into_segment(self) -> TransitionSegment {
let effect = self.effect.map(|e| Box::new(e.into_segment()));
TransitionSegment {
length: self.length,
cut_point: self.cut_point,
effect,
}
}
}
#[derive(Debug, Clone)]
pub struct Effect {
pub operation_id: Auid,
pub inputs: Vec<SequenceComponent>,
pub parameters: HashMap<String, EffectParameter>,
pub length: Option<i64>,
}
impl Effect {
#[must_use]
pub fn new(operation_id: Auid) -> Self {
Self {
operation_id,
inputs: Vec::new(),
parameters: HashMap::new(),
length: None,
}
}
pub fn add_input(&mut self, input: SequenceComponent) {
self.inputs.push(input);
}
pub fn add_parameter(&mut self, name: impl Into<String>, parameter: EffectParameter) {
self.parameters.insert(name.into(), parameter);
}
pub fn set_length(&mut self, length: i64) {
self.length = Some(length);
}
#[must_use]
pub fn into_segment(self) -> OperationGroupSegment {
OperationGroupSegment {
operation_id: self.operation_id,
input_segments: Vec::new(), parameters: Vec::new(), length: self.length,
}
}
}
#[derive(Debug, Clone)]
pub enum EffectParameter {
Constant(f64),
Varying(Vec<Keyframe>),
}
#[derive(Debug, Clone)]
pub struct Keyframe {
pub time: Position,
pub value: f64,
pub interpolation: InterpolationType,
}
impl Keyframe {
#[must_use]
pub fn new(time: Position, value: f64, interpolation: InterpolationType) -> Self {
Self {
time,
value,
interpolation,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterpolationType {
None,
Linear,
Bezier,
Cubic,
}
fn determine_track_type(segment: &Segment) -> TrackType {
match segment {
Segment::Sequence(seq) => {
if let Some(component) = seq.components.first() {
if component.is_picture() {
TrackType::Picture
} else if component.is_sound() {
TrackType::Sound
} else if component.is_timecode() {
TrackType::Timecode
} else {
TrackType::Unknown
}
} else {
TrackType::Unknown
}
}
Segment::SourceClip(_) => TrackType::Unknown,
Segment::Filler(_) => TrackType::Unknown,
_ => TrackType::Unknown,
}
}
fn extract_sequence(segment: &Segment) -> Option<Sequence> {
match segment {
Segment::Sequence(seq) => {
let data_def = seq
.components
.first()
.map_or_else(Auid::null, |c| c.data_definition);
let mut sequence = Sequence::new(data_def);
for component in &seq.components {
if let Some(seq_component) = convert_component_to_sequence_component(component) {
sequence.add_component(seq_component);
}
}
Some(sequence)
}
_ => None,
}
}
fn convert_component_to_sequence_component(component: &Component) -> Option<SequenceComponent> {
match &component.segment {
Segment::SourceClip(clip) => Some(SequenceComponent::SourceClip(SourceClip {
length: clip.length,
start_time: clip.start_time,
source_mob_id: clip.source_mob_id,
source_mob_slot_id: clip.source_mob_slot_id,
source_track_id: None,
})),
Segment::Filler(filler) => Some(SequenceComponent::Filler(Filler {
length: filler.length,
})),
Segment::Transition(trans) => Some(SequenceComponent::Transition(Transition {
length: trans.length,
cut_point: trans.cut_point,
effect: None,
})),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_composition_mob_creation() {
let mob_id = Uuid::new_v4();
let comp = CompositionMob::new(mob_id, "Test Composition");
assert_eq!(comp.mob_id(), mob_id);
assert_eq!(comp.name(), "Test Composition");
}
#[test]
fn test_track_creation() {
let track = Track::new(1, "Video", EditRate::PAL_25, TrackType::Picture);
assert_eq!(track.track_id, 1);
assert_eq!(track.name, "Video");
assert!(track.is_picture());
}
#[test]
fn test_sequence_creation() {
let seq = Sequence::new(Auid::PICTURE);
assert!(seq.is_picture());
assert_eq!(seq.duration(), Some(0));
}
#[test]
fn test_sequence_with_clips() {
let mut seq = Sequence::new(Auid::PICTURE);
let clip1 = SourceClip::new(100, Position::zero(), Uuid::new_v4(), 1);
let clip2 = SourceClip::new(50, Position::new(100), Uuid::new_v4(), 1);
seq.add_component(SequenceComponent::SourceClip(clip1));
seq.add_component(SequenceComponent::SourceClip(clip2));
assert_eq!(seq.duration(), Some(150));
assert_eq!(seq.source_clips().len(), 2);
}
#[test]
fn test_source_clip() {
let clip = SourceClip::new(100, Position::new(50), Uuid::new_v4(), 1);
assert_eq!(clip.length, 100);
assert_eq!(clip.start_time.0, 50);
assert_eq!(clip.end_time().0, 150);
}
#[test]
fn test_filler() {
let filler = Filler::new(25);
assert_eq!(filler.length, 25);
}
#[test]
fn test_transition() {
let trans = Transition::new(10, Position::new(5));
assert_eq!(trans.length, 10);
assert_eq!(trans.cut_point.0, 5);
}
#[test]
fn test_effect() {
let mut effect = Effect::new(Auid::null());
effect.set_length(50);
assert_eq!(effect.length, Some(50));
}
#[test]
fn test_keyframe() {
let kf = Keyframe::new(Position::new(10), 0.5, InterpolationType::Linear);
assert_eq!(kf.time.0, 10);
assert_eq!(kf.value, 0.5);
assert_eq!(kf.interpolation, InterpolationType::Linear);
}
}