Skip to main content

oximedia_aaf/
metadata.rs

1//! Metadata support
2//!
3//! This module implements AAF metadata functionality:
4//! - Comments (name/value pairs)
5//! - Tagged values (typed metadata)
6//! - KLV data (key-length-value)
7//! - Descriptive metadata framework (DMF)
8//! - Timecode metadata
9
10use crate::dictionary::Auid;
11use crate::timeline::{EditRate, Position};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15/// Comment - simple name/value metadata pair
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct Comment {
18    /// Category or namespace
19    pub category: Option<String>,
20    /// Comment name
21    pub name: String,
22    /// Comment value
23    pub value: String,
24}
25
26impl Comment {
27    /// Create a new comment
28    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
29        Self {
30            category: None,
31            name: name.into(),
32            value: value.into(),
33        }
34    }
35
36    /// Set category
37    pub fn with_category(mut self, category: impl Into<String>) -> Self {
38        self.category = Some(category.into());
39        self
40    }
41}
42
43/// Tagged value - typed metadata with AUID
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct TaggedValue {
46    /// Tag name
47    pub name: String,
48    /// Value
49    pub value: TaggedValueData,
50}
51
52impl TaggedValue {
53    /// Create a new tagged value
54    pub fn new(name: impl Into<String>, value: TaggedValueData) -> Self {
55        Self {
56            name: name.into(),
57            value,
58        }
59    }
60
61    /// Create a string tagged value
62    pub fn string(name: impl Into<String>, value: impl Into<String>) -> Self {
63        Self::new(name, TaggedValueData::String(value.into()))
64    }
65
66    /// Create an integer tagged value
67    pub fn integer(name: impl Into<String>, value: i64) -> Self {
68        Self::new(name, TaggedValueData::Integer(value))
69    }
70
71    /// Create a float tagged value
72    pub fn float(name: impl Into<String>, value: f64) -> Self {
73        Self::new(name, TaggedValueData::Float(value))
74    }
75
76    /// Create a boolean tagged value
77    pub fn boolean(name: impl Into<String>, value: bool) -> Self {
78        Self::new(name, TaggedValueData::Boolean(value))
79    }
80}
81
82/// Tagged value data types
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub enum TaggedValueData {
85    /// String value
86    String(String),
87    /// Integer value
88    Integer(i64),
89    /// Float value
90    Float(f64),
91    /// Boolean value
92    Boolean(bool),
93    /// Binary data
94    Binary(Vec<u8>),
95    /// AUID
96    Auid(Auid),
97    /// Rational (numerator, denominator)
98    Rational(i64, i64),
99}
100
101/// KLV data - key-length-value metadata
102#[derive(Debug, Clone)]
103pub struct KlvData {
104    /// Key (UL or UUID)
105    pub key: Vec<u8>,
106    /// Value
107    pub value: Vec<u8>,
108}
109
110impl KlvData {
111    /// Create new KLV data
112    #[must_use]
113    pub fn new(key: Vec<u8>, value: Vec<u8>) -> Self {
114        Self { key, value }
115    }
116
117    /// Get key as bytes
118    #[must_use]
119    pub fn key(&self) -> &[u8] {
120        &self.key
121    }
122
123    /// Get value as bytes
124    #[must_use]
125    pub fn value(&self) -> &[u8] {
126        &self.value
127    }
128
129    /// Get key length
130    #[must_use]
131    pub fn key_length(&self) -> usize {
132        self.key.len()
133    }
134
135    /// Get value length
136    #[must_use]
137    pub fn value_length(&self) -> usize {
138        self.value.len()
139    }
140}
141
142/// Timecode for AAF (wrapper around oximedia-timecode)
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
144pub struct Timecode {
145    /// Hours (0-23)
146    pub hours: u8,
147    /// Minutes (0-59)
148    pub minutes: u8,
149    /// Seconds (0-59)
150    pub seconds: u8,
151    /// Frames (0 to fps-1)
152    pub frames: u8,
153    /// Drop frame flag
154    pub drop_frame: bool,
155    /// Frame rate
156    pub fps: u8,
157}
158
159impl Timecode {
160    /// Create a new timecode
161    #[must_use]
162    pub fn new(hours: u8, minutes: u8, seconds: u8, frames: u8, fps: u8, drop_frame: bool) -> Self {
163        Self {
164            hours,
165            minutes,
166            seconds,
167            frames,
168            drop_frame,
169            fps,
170        }
171    }
172
173    /// Create from position and edit rate
174    #[must_use]
175    pub fn from_position(position: Position, edit_rate: EditRate) -> Self {
176        let fps = edit_rate.to_float().round() as u8;
177        let total_frames = position.to_frames(edit_rate);
178
179        let hours = (total_frames / (i64::from(fps) * 3600)) as u8;
180        let remaining = total_frames % (i64::from(fps) * 3600);
181        let minutes = (remaining / (i64::from(fps) * 60)) as u8;
182        let remaining = remaining % (i64::from(fps) * 60);
183        let seconds = (remaining / i64::from(fps)) as u8;
184        let frames = (remaining % i64::from(fps)) as u8;
185
186        Self {
187            hours,
188            minutes,
189            seconds,
190            frames,
191            drop_frame: edit_rate.is_ntsc(),
192            fps,
193        }
194    }
195
196    /// Convert to position given edit rate
197    #[must_use]
198    pub fn to_position(&self, edit_rate: EditRate) -> Position {
199        let fps = i64::from(self.fps);
200        let total_frames = i64::from(self.hours) * 3600 * fps
201            + i64::from(self.minutes) * 60 * fps
202            + i64::from(self.seconds) * fps
203            + i64::from(self.frames);
204
205        Position::from_frames(total_frames, edit_rate)
206    }
207
208    /// Parse from string (format: HH:MM:SS:FF or HH:MM:SS;FF for drop frame)
209    pub fn parse(s: &str, fps: u8) -> Result<Self, MetadataError> {
210        let parts: Vec<&str> = s.split(&[':', ';'][..]).collect();
211        if parts.len() != 4 {
212            return Err(MetadataError::InvalidTimecode(s.to_string()));
213        }
214
215        let hours = parts[0]
216            .parse::<u8>()
217            .map_err(|_| MetadataError::InvalidTimecode(s.to_string()))?;
218        let minutes = parts[1]
219            .parse::<u8>()
220            .map_err(|_| MetadataError::InvalidTimecode(s.to_string()))?;
221        let seconds = parts[2]
222            .parse::<u8>()
223            .map_err(|_| MetadataError::InvalidTimecode(s.to_string()))?;
224        let frames = parts[3]
225            .parse::<u8>()
226            .map_err(|_| MetadataError::InvalidTimecode(s.to_string()))?;
227
228        let drop_frame = s.contains(';');
229
230        Ok(Self {
231            hours,
232            minutes,
233            seconds,
234            frames,
235            drop_frame,
236            fps,
237        })
238    }
239}
240
241impl std::fmt::Display for Timecode {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        let separator = if self.drop_frame { ';' } else { ':' };
244        write!(
245            f,
246            "{:02}:{:02}:{:02}{}{:02}",
247            self.hours, self.minutes, self.seconds, separator, self.frames
248        )
249    }
250}
251
252/// Descriptive metadata framework (DMF)
253#[derive(Debug, Clone)]
254pub struct DescriptiveMetadata {
255    /// Metadata items
256    items: HashMap<String, MetadataValue>,
257    /// Linked objects
258    linked_objects: Vec<DescriptiveObjectReference>,
259}
260
261impl DescriptiveMetadata {
262    /// Create new descriptive metadata
263    #[must_use]
264    pub fn new() -> Self {
265        Self {
266            items: HashMap::new(),
267            linked_objects: Vec::new(),
268        }
269    }
270
271    /// Add metadata item
272    pub fn add_item(&mut self, key: impl Into<String>, value: MetadataValue) {
273        self.items.insert(key.into(), value);
274    }
275
276    /// Get metadata item
277    #[must_use]
278    pub fn get_item(&self, key: &str) -> Option<&MetadataValue> {
279        self.items.get(key)
280    }
281
282    /// Add linked object
283    pub fn add_linked_object(&mut self, reference: DescriptiveObjectReference) {
284        self.linked_objects.push(reference);
285    }
286
287    /// Get all items
288    #[must_use]
289    pub fn items(&self) -> &HashMap<String, MetadataValue> {
290        &self.items
291    }
292
293    /// Get linked objects
294    #[must_use]
295    pub fn linked_objects(&self) -> &[DescriptiveObjectReference] {
296        &self.linked_objects
297    }
298}
299
300impl Default for DescriptiveMetadata {
301    fn default() -> Self {
302        Self::new()
303    }
304}
305
306/// Metadata value types
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub enum MetadataValue {
309    /// String value
310    String(String),
311    /// Integer value
312    Integer(i64),
313    /// Float value
314    Float(f64),
315    /// Boolean value
316    Boolean(bool),
317    /// Date/time (ISO 8601 string)
318    DateTime(String),
319    /// URI
320    Uri(String),
321    /// Array of values
322    Array(Vec<MetadataValue>),
323    /// Nested metadata
324    Object(HashMap<String, MetadataValue>),
325}
326
327/// Reference to a descriptive object
328#[derive(Debug, Clone)]
329pub struct DescriptiveObjectReference {
330    /// Object ID
331    pub object_id: String,
332    /// Object type
333    pub object_type: String,
334}
335
336/// Production metadata
337#[derive(Debug, Clone)]
338pub struct ProductionMetadata {
339    /// Production title
340    pub title: Option<String>,
341    /// Episode title
342    pub episode_title: Option<String>,
343    /// Series title
344    pub series_title: Option<String>,
345    /// Production number
346    pub production_number: Option<String>,
347    /// Copyright
348    pub copyright: Option<String>,
349    /// Creation date
350    pub creation_date: Option<String>,
351    /// Production company
352    pub production_company: Option<String>,
353    /// Director
354    pub director: Option<String>,
355    /// Producer
356    pub producer: Option<String>,
357    /// Additional metadata
358    pub additional: HashMap<String, String>,
359}
360
361impl ProductionMetadata {
362    /// Create new production metadata
363    #[must_use]
364    pub fn new() -> Self {
365        Self {
366            title: None,
367            episode_title: None,
368            series_title: None,
369            production_number: None,
370            copyright: None,
371            creation_date: None,
372            production_company: None,
373            director: None,
374            producer: None,
375            additional: HashMap::new(),
376        }
377    }
378
379    /// Set title
380    pub fn with_title(mut self, title: impl Into<String>) -> Self {
381        self.title = Some(title.into());
382        self
383    }
384
385    /// Set production company
386    pub fn with_company(mut self, company: impl Into<String>) -> Self {
387        self.production_company = Some(company.into());
388        self
389    }
390
391    /// Add additional metadata
392    pub fn add_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
393        self.additional.insert(key.into(), value.into());
394    }
395}
396
397impl Default for ProductionMetadata {
398    fn default() -> Self {
399        Self::new()
400    }
401}
402
403/// Technical metadata
404#[derive(Debug, Clone)]
405pub struct TechnicalMetadata {
406    /// Video format
407    pub video_format: Option<String>,
408    /// Audio format
409    pub audio_format: Option<String>,
410    /// Frame rate
411    pub frame_rate: Option<EditRate>,
412    /// Aspect ratio
413    pub aspect_ratio: Option<String>,
414    /// Resolution
415    pub resolution: Option<(u32, u32)>,
416    /// Duration
417    pub duration: Option<i64>,
418    /// File size
419    pub file_size: Option<u64>,
420    /// Codec
421    pub codec: Option<String>,
422    /// Additional metadata
423    pub additional: HashMap<String, String>,
424}
425
426impl TechnicalMetadata {
427    /// Create new technical metadata
428    #[must_use]
429    pub fn new() -> Self {
430        Self {
431            video_format: None,
432            audio_format: None,
433            frame_rate: None,
434            aspect_ratio: None,
435            resolution: None,
436            duration: None,
437            file_size: None,
438            codec: None,
439            additional: HashMap::new(),
440        }
441    }
442
443    /// Set video format
444    pub fn with_video_format(mut self, format: impl Into<String>) -> Self {
445        self.video_format = Some(format.into());
446        self
447    }
448
449    /// Set frame rate
450    #[must_use]
451    pub fn with_frame_rate(mut self, rate: EditRate) -> Self {
452        self.frame_rate = Some(rate);
453        self
454    }
455
456    /// Set resolution
457    #[must_use]
458    pub fn with_resolution(mut self, width: u32, height: u32) -> Self {
459        self.resolution = Some((width, height));
460        self
461    }
462}
463
464impl Default for TechnicalMetadata {
465    fn default() -> Self {
466        Self::new()
467    }
468}
469
470/// Metadata error
471#[derive(Debug, Clone, PartialEq, Eq)]
472pub enum MetadataError {
473    /// Invalid timecode format
474    InvalidTimecode(String),
475    /// Invalid metadata value
476    InvalidValue(String),
477    /// Metadata not found
478    NotFound(String),
479}
480
481impl std::fmt::Display for MetadataError {
482    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483        match self {
484            MetadataError::InvalidTimecode(s) => write!(f, "Invalid timecode: {s}"),
485            MetadataError::InvalidValue(s) => write!(f, "Invalid metadata value: {s}"),
486            MetadataError::NotFound(s) => write!(f, "Metadata not found: {s}"),
487        }
488    }
489}
490
491impl std::error::Error for MetadataError {}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    #[test]
498    fn test_comment() {
499        let comment = Comment::new("Author", "John Doe").with_category("Production");
500        assert_eq!(comment.name, "Author");
501        assert_eq!(comment.value, "John Doe");
502        assert_eq!(comment.category, Some("Production".to_string()));
503    }
504
505    #[test]
506    fn test_tagged_value() {
507        let tv_str = TaggedValue::string("Title", "My Video");
508        assert_eq!(tv_str.name, "Title");
509        if let TaggedValueData::String(s) = &tv_str.value {
510            assert_eq!(s, "My Video");
511        } else {
512            panic!("Expected string value");
513        }
514
515        let tv_int = TaggedValue::integer("FrameCount", 1000);
516        assert_eq!(tv_int.name, "FrameCount");
517    }
518
519    #[test]
520    fn test_klv_data() {
521        let key = vec![1, 2, 3, 4];
522        let value = vec![5, 6, 7, 8, 9];
523        let klv = KlvData::new(key.clone(), value.clone());
524
525        assert_eq!(klv.key(), &key);
526        assert_eq!(klv.value(), &value);
527        assert_eq!(klv.key_length(), 4);
528        assert_eq!(klv.value_length(), 5);
529    }
530
531    #[test]
532    fn test_timecode() {
533        let tc = Timecode::new(1, 2, 3, 4, 25, false);
534        assert_eq!(tc.hours, 1);
535        assert_eq!(tc.minutes, 2);
536        assert_eq!(tc.seconds, 3);
537        assert_eq!(tc.frames, 4);
538        assert_eq!(tc.to_string(), "01:02:03:04");
539    }
540
541    #[test]
542    fn test_timecode_parse() {
543        let tc = Timecode::parse("01:02:03:04", 25).expect("tc should be valid");
544        assert_eq!(tc.hours, 1);
545        assert_eq!(tc.minutes, 2);
546        assert_eq!(tc.seconds, 3);
547        assert_eq!(tc.frames, 4);
548        assert!(!tc.drop_frame);
549
550        let tc_df = Timecode::parse("01:02:03;04", 30).expect("tc_df should be valid");
551        assert!(tc_df.drop_frame);
552    }
553
554    #[test]
555    fn test_timecode_position_conversion() {
556        let edit_rate = EditRate::new(25, 1);
557        let tc = Timecode::new(0, 0, 1, 0, 25, false);
558        let pos = tc.to_position(edit_rate);
559        assert_eq!(pos.0, 25);
560
561        let tc2 = Timecode::from_position(pos, edit_rate);
562        assert_eq!(tc2.seconds, 1);
563        assert_eq!(tc2.frames, 0);
564    }
565
566    #[test]
567    fn test_descriptive_metadata() {
568        let mut dm = DescriptiveMetadata::new();
569        dm.add_item("title", MetadataValue::String("My Film".to_string()));
570        dm.add_item("year", MetadataValue::Integer(2024));
571
572        assert!(dm.get_item("title").is_some());
573        assert!(dm.get_item("year").is_some());
574        assert_eq!(dm.items().len(), 2);
575    }
576
577    #[test]
578    fn test_production_metadata() {
579        let pm = ProductionMetadata::new()
580            .with_title("Episode 1")
581            .with_company("ABC Productions");
582
583        assert_eq!(pm.title, Some("Episode 1".to_string()));
584        assert_eq!(pm.production_company, Some("ABC Productions".to_string()));
585    }
586
587    #[test]
588    fn test_technical_metadata() {
589        let tm = TechnicalMetadata::new()
590            .with_video_format("HD")
591            .with_frame_rate(EditRate::new(25, 1))
592            .with_resolution(1920, 1080);
593
594        assert_eq!(tm.video_format, Some("HD".to_string()));
595        assert_eq!(tm.resolution, Some((1920, 1080)));
596    }
597}