Skip to main content

motion_core/audio/
metadata.rs

1//! Bounded container tags and chapters attached to exported audio.
2
3use super::invalid;
4use crate::{Time, ValidationError};
5
6/// Maximum container tags in one metadata request.
7const MAX_TAGS: usize = 64;
8
9/// Maximum chapters in one metadata request.
10const MAX_CHAPTERS: usize = 4096;
11
12/// Maximum UTF-8 bytes in a tag key.
13const MAX_TAG_KEY_BYTES: usize = 128;
14
15/// Maximum UTF-8 bytes in a tag value or chapter title.
16const MAX_TEXT_BYTES: usize = 4096;
17
18/// Latest chapter end, in seconds (one day).
19const MAX_CHAPTER_END_SECONDS: f64 = 86400.0;
20
21/// Container chapter in the same integer tick clock as the timeline.
22#[derive(Clone, Debug)]
23pub struct AudioChapter {
24    /// Inclusive chapter start.
25    pub start: Time,
26    /// Exclusive chapter end.
27    pub end: Time,
28    /// Chapter title, at most 4096 bytes.
29    pub title: String,
30}
31
32/// Optional media tags and chapters; empty metadata writes none.
33#[derive(Clone, Debug, Default)]
34pub struct AudioMetadata {
35    /// Container tags, including title, language, R128_TRACK_GAIN or ReplayGain.
36    pub tags: std::collections::BTreeMap<String, String>,
37    /// Ordered, nonoverlapping chapters in timeline coordinates.
38    pub chapters: Vec<AudioChapter>,
39}
40
41impl AudioMetadata {
42    /// Validates finite tick ranges, byte limits and NUL-free tags before opening output.
43    pub fn validate(&self) -> Result<(), ValidationError> {
44        if self.tags.len() > MAX_TAGS
45            || self.chapters.len() > MAX_CHAPTERS
46            || self.tags.iter().any(|(key, value)| {
47                key.is_empty()
48                    || key.len() > MAX_TAG_KEY_BYTES
49                    || value.len() > MAX_TEXT_BYTES
50                    || key.contains('\0')
51                    || value.contains('\0')
52            })
53            || self.chapters.iter().any(|chapter| {
54                chapter.start < Time::ZERO
55                    || chapter.end <= chapter.start
56                    || chapter.end.as_seconds() > MAX_CHAPTER_END_SECONDS
57                    || chapter.title.len() > MAX_TEXT_BYTES
58                    || chapter.title.contains('\0')
59            })
60            || self
61                .chapters
62                .windows(2)
63                .any(|pair| pair[0].end > pair[1].start)
64        {
65            return Err(invalid(
66                "metadata",
67                "Invalid bounded container tags or ordered chapters",
68            ));
69        }
70        Ok(())
71    }
72
73    /// Whether the request leaves container metadata unchanged.
74    pub fn is_empty(&self) -> bool {
75        self.tags.is_empty() && self.chapters.is_empty()
76    }
77}