Skip to main content

oxihuman_export/
aaf_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! AAF (Advanced Authoring Format) stub export.
6
7/// AAF essence kind.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum AafEssenceKind {
10    Video,
11    Audio,
12    Data,
13}
14
15/// An AAF component (clip or filler).
16#[derive(Debug, Clone)]
17pub struct AafComponent {
18    pub name: String,
19    pub kind: AafEssenceKind,
20    pub length_frames: u32,
21    pub source_ref: String,
22}
23
24/// An AAF track.
25#[derive(Debug, Clone)]
26pub struct AafTrack {
27    pub track_id: u32,
28    pub label: String,
29    pub components: Vec<AafComponent>,
30}
31
32/// An AAF composition (master mob).
33#[derive(Debug, Clone)]
34pub struct AafExport {
35    pub name: String,
36    pub edit_rate: (u32, u32), /* numerator/denominator */
37    pub tracks: Vec<AafTrack>,
38}
39
40/// Create a new AAF export.
41pub fn new_aaf_export(name: &str, edit_rate_num: u32, edit_rate_den: u32) -> AafExport {
42    AafExport {
43        name: name.to_string(),
44        edit_rate: (edit_rate_num, edit_rate_den),
45        tracks: Vec::new(),
46    }
47}
48
49/// Add a track.
50pub fn aaf_add_track(export: &mut AafExport, track_id: u32, label: &str) {
51    export.tracks.push(AafTrack {
52        track_id,
53        label: label.to_string(),
54        components: Vec::new(),
55    });
56}
57
58/// Add a component to the last track.
59pub fn aaf_add_component(
60    export: &mut AafExport,
61    name: &str,
62    kind: AafEssenceKind,
63    length: u32,
64    source_ref: &str,
65) {
66    if let Some(track) = export.tracks.last_mut() {
67        track.components.push(AafComponent {
68            name: name.to_string(),
69            kind,
70            length_frames: length,
71            source_ref: source_ref.to_string(),
72        });
73    }
74}
75
76/// Return the track count.
77pub fn aaf_track_count(export: &AafExport) -> usize {
78    export.tracks.len()
79}
80
81/// Return the total component count.
82pub fn aaf_component_count(export: &AafExport) -> usize {
83    export.tracks.iter().map(|t| t.components.len()).sum()
84}
85
86/// Total duration of the first track in frames.
87pub fn aaf_duration_frames(export: &AafExport) -> u32 {
88    export
89        .tracks
90        .first()
91        .map(|t| t.components.iter().map(|c| c.length_frames).sum())
92        .unwrap_or(0)
93}
94
95/// Validate the export.
96pub fn validate_aaf(export: &AafExport) -> bool {
97    !export.name.is_empty() && export.edit_rate.1 > 0
98}
99
100/// Generate a stub AAF XML description.
101pub fn aaf_to_xml_stub(export: &AafExport) -> String {
102    format!(
103        "<AAFFile><Composition name=\"{}\" editRate=\"{}/{}\"/></AAFFile>",
104        export.name, export.edit_rate.0, export.edit_rate.1
105    )
106}
107
108/// Estimate the AAF file size.
109pub fn aaf_size_estimate(export: &AafExport) -> usize {
110    aaf_to_xml_stub(export).len() + aaf_component_count(export) * 64
111}
112
113/// Find a track by ID.
114pub fn aaf_find_track(export: &AafExport, track_id: u32) -> Option<&AafTrack> {
115    export.tracks.iter().find(|t| t.track_id == track_id)
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    fn sample() -> AafExport {
123        let mut exp = new_aaf_export("MyComp", 24, 1);
124        aaf_add_track(&mut exp, 1, "V1");
125        aaf_add_component(&mut exp, "clip1", AafEssenceKind::Video, 48, "mob:001");
126        aaf_add_component(&mut exp, "clip2", AafEssenceKind::Video, 24, "mob:002");
127        aaf_add_track(&mut exp, 2, "A1");
128        aaf_add_component(&mut exp, "audio1", AafEssenceKind::Audio, 72, "mob:003");
129        exp
130    }
131
132    #[test]
133    fn test_track_count() {
134        assert_eq!(aaf_track_count(&sample()), 2);
135    }
136
137    #[test]
138    fn test_component_count() {
139        assert_eq!(aaf_component_count(&sample()), 3);
140    }
141
142    #[test]
143    fn test_duration_frames() {
144        assert_eq!(aaf_duration_frames(&sample()), 72);
145    }
146
147    #[test]
148    fn test_validate_valid() {
149        assert!(validate_aaf(&sample()));
150    }
151
152    #[test]
153    fn test_validate_bad_rate() {
154        let exp = new_aaf_export("X", 24, 0);
155        assert!(!validate_aaf(&exp));
156    }
157
158    #[test]
159    fn test_to_xml() {
160        let s = aaf_to_xml_stub(&sample());
161        assert!(s.contains("MyComp"));
162    }
163
164    #[test]
165    fn test_find_track() {
166        let exp = sample();
167        assert!(aaf_find_track(&exp, 1).is_some());
168        assert!(aaf_find_track(&exp, 99).is_none());
169    }
170
171    #[test]
172    fn test_size_estimate() {
173        assert!(aaf_size_estimate(&sample()) > 0);
174    }
175
176    #[test]
177    fn test_empty_duration() {
178        let exp = new_aaf_export("empty", 25, 1);
179        assert_eq!(aaf_duration_frames(&exp), 0);
180    }
181}