Skip to main content

st377_1/
timecode_component.rs

1//! Timecode Component — SMPTE ST 377-1:2019 Annex B §B.17
2//! (`docs/st377-1.md`): a Structural Component carrying a timecode
3//! reference (byte 14/15 = `0x01`/`0x14`).
4//!
5//! Inherits the Structural Component base properties (B.8): Data
6//! Definition and Duration.  Adds Start Timecode, Rounded Timecode
7//! Base, and Drop Frame flag.
8
9extern crate alloc;
10
11use alloc::vec::Vec;
12
13use broadcast_common::{Parse, Serialize};
14
15use crate::error::{Error, Result};
16use crate::local_set::{LocalSet, StructuralSetKind};
17use crate::sets::{
18    InterchangeObjectFields, LocalSetOwnedItem, collect_dark, finish_owned_set, get_optional_fixed,
19    get_required_fixed, owned_set_serialized_len, serialize_owned_set,
20};
21use crate::types::UlBytes;
22
23// ── Structural Component base tags (B.8) ────────────────────────────────
24
25/// Local tag: Data Definition (B.8).
26pub const TAG_DATA_DEFINITION: u16 = 0x0201;
27/// Local tag: Duration (B.8).
28pub const TAG_DURATION: u16 = 0x0202;
29
30// ── Timecode Component own tags (B.17) ──────────────────────────────────
31
32/// Local tag: Start Timecode (B.17) — Int64.
33pub const TAG_START_TIMECODE: u16 = 0x1501;
34/// Local tag: Rounded Timecode Base (B.17) — UInt16.
35pub const TAG_ROUNDED_TIMECODE_BASE: u16 = 0x1502;
36/// Local tag: Drop Frame (B.17) — Boolean (UInt8).
37pub const TAG_DROP_FRAME: u16 = 0x1503;
38
39const KNOWN_TAGS: [u16; 8] = [
40    crate::sets::TAG_INSTANCE_UID,
41    crate::sets::TAG_GENERATION_UID,
42    crate::sets::TAG_OBJECT_CLASS,
43    TAG_DATA_DEFINITION,
44    TAG_DURATION,
45    TAG_START_TIMECODE,
46    TAG_ROUNDED_TIMECODE_BASE,
47    TAG_DROP_FRAME,
48];
49
50/// The Timecode Component Set — SMPTE ST 377-1:2019 Annex B §B.17 (byte
51/// 14/15 = `0x01`/`0x14`): supplies a timecode reference inside a Track
52/// Sequence (typically in a Timecode Track).
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct TimecodeComponent {
55    /// Interchange Object (A.1) base properties.
56    pub interchange: InterchangeObjectFields,
57    /// Data Definition (`0x0201`, Req) — UL identifying the essence kind
58    /// (usually Timecode Data Definition).
59    pub data_definition: UlBytes,
60    /// Duration (`0x0202`, Opt) — edit-unit count.
61    pub duration: Option<i64>,
62    /// Start Timecode (`0x1501`, Req) — the initial timecode value (in
63    /// edit units counted from midnight).
64    pub start_timecode: i64,
65    /// Rounded Timecode Base (`0x1502`, Req) — the integer frame rate
66    /// the timecode runs at (e.g. 25, 30).
67    pub rounded_timecode_base: u16,
68    /// Drop Frame (`0x1503`, Req) — whether drop-frame counting applies
69    /// (NTSC 29.97).
70    pub drop_frame: bool,
71    /// Unrecognized properties preserved for round-trip fidelity.
72    pub dark: Vec<(u16, Vec<u8>)>,
73}
74
75impl<'a> Parse<'a> for TimecodeComponent {
76    type Error = Error;
77
78    fn parse(bytes: &'a [u8]) -> Result<Self> {
79        let set = LocalSet::parse(bytes)?;
80        if set.kind() != StructuralSetKind::TimecodeComponent {
81            return Err(Error::KeyPrefixMismatch {
82                what: "Timecode Component (Table 17)",
83            });
84        }
85        let items = &set.items;
86        let interchange = InterchangeObjectFields::decode(items, "Timecode Component")?;
87        let data_definition = get_required_fixed::<16>(
88            items,
89            TAG_DATA_DEFINITION,
90            "Data Definition",
91            "Timecode Component",
92        )?;
93        let duration =
94            get_optional_fixed::<8>(items, TAG_DURATION, "Duration")?.map(i64::from_be_bytes);
95        let start_timecode = i64::from_be_bytes(get_required_fixed::<8>(
96            items,
97            TAG_START_TIMECODE,
98            "Start Timecode",
99            "Timecode Component",
100        )?);
101        let rounded_timecode_base = u16::from_be_bytes(get_required_fixed::<2>(
102            items,
103            TAG_ROUNDED_TIMECODE_BASE,
104            "Rounded Timecode Base",
105            "Timecode Component",
106        )?);
107        let drop_frame =
108            get_required_fixed::<1>(items, TAG_DROP_FRAME, "Drop Frame", "Timecode Component")?[0]
109                != 0;
110        let dark = collect_dark(items, &KNOWN_TAGS);
111
112        Ok(TimecodeComponent {
113            interchange,
114            data_definition,
115            duration,
116            start_timecode,
117            rounded_timecode_base,
118            drop_frame,
119            dark,
120        })
121    }
122}
123
124impl TimecodeComponent {
125    fn owned_items(&self) -> Vec<LocalSetOwnedItem> {
126        let mut out = Vec::new();
127        self.interchange.encode_into(&mut out);
128        out.push(LocalSetOwnedItem::fixed(
129            TAG_DATA_DEFINITION,
130            self.data_definition,
131        ));
132        if let Some(d) = self.duration {
133            out.push(LocalSetOwnedItem::fixed(TAG_DURATION, d.to_be_bytes()));
134        }
135        out.push(LocalSetOwnedItem::fixed(
136            TAG_START_TIMECODE,
137            self.start_timecode.to_be_bytes(),
138        ));
139        out.push(LocalSetOwnedItem::fixed(
140            TAG_ROUNDED_TIMECODE_BASE,
141            self.rounded_timecode_base.to_be_bytes(),
142        ));
143        out.push(LocalSetOwnedItem::fixed(
144            TAG_DROP_FRAME,
145            [u8::from(self.drop_frame)],
146        ));
147        out
148    }
149}
150
151impl Serialize for TimecodeComponent {
152    type Error = Error;
153
154    fn serialized_len(&self) -> usize {
155        let (key, items) = finish_owned_set(
156            StructuralSetKind::TimecodeComponent,
157            self.owned_items(),
158            &self.dark,
159        );
160        owned_set_serialized_len(key, &items)
161    }
162
163    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
164        let (key, items) = finish_owned_set(
165            StructuralSetKind::TimecodeComponent,
166            self.owned_items(),
167            &self.dark,
168        );
169        serialize_owned_set(key, &items, buf)
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    /// SMPTE-RP 224 Timecode data definition UL (test placeholder).
178    const TIMECODE_DD: UlBytes = [
179        0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x01, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00,
180        0x00,
181    ];
182
183    fn sample() -> TimecodeComponent {
184        TimecodeComponent {
185            interchange: InterchangeObjectFields {
186                instance_uid: [0x01; 16],
187                generation_uid: None,
188                object_class: None,
189            },
190            data_definition: TIMECODE_DD,
191            duration: Some(250),
192            start_timecode: 90000,
193            rounded_timecode_base: 25,
194            drop_frame: false,
195            dark: Vec::new(),
196        }
197    }
198
199    #[test]
200    fn round_trip() {
201        let tc = sample();
202        let bytes = tc.to_bytes();
203        let parsed = TimecodeComponent::parse(&bytes).unwrap();
204        assert_eq!(parsed, tc);
205        assert_eq!(parsed.to_bytes(), bytes);
206    }
207
208    #[test]
209    fn drop_frame_true_round_trips() {
210        let mut tc = sample();
211        tc.drop_frame = true;
212        tc.rounded_timecode_base = 30;
213        let bytes = tc.to_bytes();
214        let parsed = TimecodeComponent::parse(&bytes).unwrap();
215        assert!(parsed.drop_frame);
216        assert_eq!(parsed.rounded_timecode_base, 30);
217        assert_eq!(parsed.to_bytes(), bytes);
218    }
219
220    #[test]
221    fn no_duration_round_trip() {
222        let mut tc = sample();
223        tc.duration = None;
224        let bytes = tc.to_bytes();
225        let parsed = TimecodeComponent::parse(&bytes).unwrap();
226        assert_eq!(parsed.duration, None);
227        assert_eq!(parsed.to_bytes(), bytes);
228    }
229
230    #[test]
231    fn dark_preserved() {
232        let mut tc = sample();
233        tc.dark = alloc::vec![(0x9004, alloc::vec![0xCA, 0xFE])];
234        let bytes = tc.to_bytes();
235        let parsed = TimecodeComponent::parse(&bytes).unwrap();
236        assert_eq!(parsed.dark, tc.dark);
237    }
238
239    #[test]
240    fn wrong_kind_rejected() {
241        let key = LocalSet::build_key(
242            StructuralSetKind::SourceClip,
243            crate::local_set::ItemLengthMode::TwoByte,
244        );
245        let set = LocalSet {
246            key,
247            items: Vec::new(),
248        };
249        let bytes = set.to_bytes();
250        assert!(matches!(
251            TimecodeComponent::parse(&bytes),
252            Err(Error::KeyPrefixMismatch { .. })
253        ));
254    }
255
256    #[test]
257    fn mutation_changes_serialized_bytes() {
258        let mut tc = sample();
259        let before = tc.to_bytes();
260        tc.start_timecode = 1800000;
261        let after = tc.to_bytes();
262        assert_ne!(before, after);
263        assert_eq!(
264            TimecodeComponent::parse(&after).unwrap().start_timecode,
265            1800000
266        );
267    }
268}