Skip to main content

st377_1/
track.rs

1//! Timeline Track, Event Track, and Static Track — SMPTE ST 377-1:2019
2//! Annex B §B.6/B.12/B.13/B.14 (`docs/st377-1.md`): the three concrete
3//! Track kinds used inside Material and Source Packages.
4//!
5//! All three inherit Generic Track properties (B.6): Track ID, Track
6//! Number, Track Name, and a Sequence strong reference.
7//!
8//! `TimelineTrack` (byte 14/15 = `0x01`/`0x3B`) adds Edit Rate + Origin.
9//! `EventTrack`    (byte 14/15 = `0x01`/`0x39`) adds Event Edit Rate +
10//!                 Event Origin.
11//! `StaticTrack`   (byte 14/15 = `0x01`/`0x3A`) has no additional
12//!                 properties.
13
14extern crate alloc;
15
16use alloc::string::String;
17use alloc::vec::Vec;
18
19use broadcast_common::{Parse, Serialize};
20
21use crate::error::{Error, Result};
22use crate::local_set::{LocalSet, StructuralSetKind};
23use crate::sets::{
24    InterchangeObjectFields, LocalSetOwnedItem, collect_dark, finish_owned_set, get_optional_fixed,
25    get_optional_raw, get_required_fixed, get_required_raw, owned_set_serialized_len,
26    serialize_owned_set,
27};
28use crate::types::{RATIONAL_LEN, Rational, StrongRef, decode_utf16_be, encode_utf16_be};
29
30// ── Generic Track local tags (B.6) ─────────────────────────────────────
31
32/// Local tag: Track ID (B.6).
33pub const TAG_TRACK_ID: u16 = 0x4801;
34/// Local tag: Track Name (B.6) — UTF-16 string, optional.
35pub const TAG_TRACK_NAME: u16 = 0x4802;
36/// Local tag: Sequence (B.6) — StrongRef.
37pub const TAG_SEQUENCE: u16 = 0x4803;
38/// Local tag: Track Number (B.6).
39pub const TAG_TRACK_NUMBER: u16 = 0x4804;
40
41// ── Timeline Track additional tags (B.12) ───────────────────────────────
42
43/// Local tag: Edit Rate (B.12) — Rational (8 bytes).
44pub const TAG_EDIT_RATE: u16 = 0x4B01;
45/// Local tag: Origin (B.12) — Position / Int64.
46pub const TAG_ORIGIN: u16 = 0x4B02;
47
48// ── Event Track additional tags (B.13) ──────────────────────────────────
49
50/// Local tag: Event Edit Rate (B.13) — Rational (8 bytes).
51pub const TAG_EVENT_EDIT_RATE: u16 = 0x4901;
52/// Local tag: Event Origin (B.13) — Position / Int64, optional (default 0).
53pub const TAG_EVENT_ORIGIN: u16 = 0x4902;
54
55// ── Known-tag tables ────────────────────────────────────────────────────
56
57const TIMELINE_KNOWN_TAGS: [u16; 9] = [
58    crate::sets::TAG_INSTANCE_UID,
59    crate::sets::TAG_GENERATION_UID,
60    crate::sets::TAG_OBJECT_CLASS,
61    TAG_TRACK_ID,
62    TAG_TRACK_NAME,
63    TAG_SEQUENCE,
64    TAG_TRACK_NUMBER,
65    TAG_EDIT_RATE,
66    TAG_ORIGIN,
67];
68
69const EVENT_KNOWN_TAGS: [u16; 9] = [
70    crate::sets::TAG_INSTANCE_UID,
71    crate::sets::TAG_GENERATION_UID,
72    crate::sets::TAG_OBJECT_CLASS,
73    TAG_TRACK_ID,
74    TAG_TRACK_NAME,
75    TAG_SEQUENCE,
76    TAG_TRACK_NUMBER,
77    TAG_EVENT_EDIT_RATE,
78    TAG_EVENT_ORIGIN,
79];
80
81const STATIC_KNOWN_TAGS: [u16; 7] = [
82    crate::sets::TAG_INSTANCE_UID,
83    crate::sets::TAG_GENERATION_UID,
84    crate::sets::TAG_OBJECT_CLASS,
85    TAG_TRACK_ID,
86    TAG_TRACK_NAME,
87    TAG_SEQUENCE,
88    TAG_TRACK_NUMBER,
89];
90
91// ═══════════════════════════════════════════════════════════════════════
92// TimelineTrack
93// ═══════════════════════════════════════════════════════════════════════
94
95/// The Timeline Track Set — SMPTE ST 377-1:2019 Annex B §B.12 (byte
96/// 14/15 = `0x01`/`0x3B`): a timed track with a fixed edit rate and
97/// origin position.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct TimelineTrack {
100    /// Interchange Object (A.1) base properties.
101    pub interchange: InterchangeObjectFields,
102    /// Track ID (`0x4801`, Req).
103    pub track_id: u32,
104    /// Track Number (`0x4804`, Req, default 0).
105    pub track_number: u32,
106    /// Track Name (`0x4802`, Opt) — human-readable name.
107    pub track_name: Option<String>,
108    /// Sequence (`0x4803`, Req) — strong reference to a Sequence.
109    pub sequence: StrongRef,
110    /// Edit Rate (`0x4B01`, Req) — the track's time base.
111    pub edit_rate: Rational,
112    /// Origin (`0x4B02`, Req) — the position of the first edit unit.
113    pub origin: i64,
114    /// Unrecognized properties preserved for round-trip fidelity.
115    pub dark: Vec<(u16, Vec<u8>)>,
116}
117
118impl<'a> Parse<'a> for TimelineTrack {
119    type Error = Error;
120
121    fn parse(bytes: &'a [u8]) -> Result<Self> {
122        let set = LocalSet::parse(bytes)?;
123        if set.kind() != StructuralSetKind::TimelineTrack {
124            return Err(Error::KeyPrefixMismatch {
125                what: "Timeline Track (Table 17)",
126            });
127        }
128        let items = &set.items;
129        let interchange = InterchangeObjectFields::decode(items, "Timeline Track")?;
130        let track_id = u32::from_be_bytes(get_required_fixed::<4>(
131            items,
132            TAG_TRACK_ID,
133            "Track ID",
134            "Timeline Track",
135        )?);
136        let track_number = u32::from_be_bytes(get_required_fixed::<4>(
137            items,
138            TAG_TRACK_NUMBER,
139            "Track Number",
140            "Timeline Track",
141        )?);
142        let track_name = get_optional_raw(items, TAG_TRACK_NAME)
143            .map(decode_utf16_be)
144            .transpose()
145            .map_err(|_| Error::InvalidUtf16 {
146                tag: TAG_TRACK_NAME,
147                name: "Track Name",
148            })?;
149        let sequence = get_required_fixed::<16>(items, TAG_SEQUENCE, "Sequence", "Timeline Track")?;
150        let edit_rate = Rational::parse(get_required_raw(
151            items,
152            TAG_EDIT_RATE,
153            "Edit Rate",
154            "Timeline Track",
155        )?)?;
156        let origin = i64::from_be_bytes(get_required_fixed::<8>(
157            items,
158            TAG_ORIGIN,
159            "Origin",
160            "Timeline Track",
161        )?);
162        let dark = collect_dark(items, &TIMELINE_KNOWN_TAGS);
163
164        Ok(TimelineTrack {
165            interchange,
166            track_id,
167            track_number,
168            track_name,
169            sequence,
170            edit_rate,
171            origin,
172            dark,
173        })
174    }
175}
176
177impl TimelineTrack {
178    fn owned_items(&self) -> Vec<LocalSetOwnedItem> {
179        let mut out = Vec::new();
180        self.interchange.encode_into(&mut out);
181        out.push(LocalSetOwnedItem::fixed(
182            TAG_TRACK_ID,
183            self.track_id.to_be_bytes(),
184        ));
185        out.push(LocalSetOwnedItem::fixed(
186            TAG_TRACK_NUMBER,
187            self.track_number.to_be_bytes(),
188        ));
189        if let Some(n) = &self.track_name {
190            out.push(LocalSetOwnedItem::owned(TAG_TRACK_NAME, encode_utf16_be(n)));
191        }
192        out.push(LocalSetOwnedItem::fixed(TAG_SEQUENCE, self.sequence));
193        {
194            let mut buf = [0u8; RATIONAL_LEN];
195            self.edit_rate
196                .serialize_into(&mut buf)
197                .expect("fixed-size buffer");
198            out.push(LocalSetOwnedItem::owned(TAG_EDIT_RATE, buf.to_vec()));
199        }
200        out.push(LocalSetOwnedItem::fixed(
201            TAG_ORIGIN,
202            self.origin.to_be_bytes(),
203        ));
204        out
205    }
206}
207
208impl Serialize for TimelineTrack {
209    type Error = Error;
210
211    fn serialized_len(&self) -> usize {
212        let (key, items) = finish_owned_set(
213            StructuralSetKind::TimelineTrack,
214            self.owned_items(),
215            &self.dark,
216        );
217        owned_set_serialized_len(key, &items)
218    }
219
220    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
221        let (key, items) = finish_owned_set(
222            StructuralSetKind::TimelineTrack,
223            self.owned_items(),
224            &self.dark,
225        );
226        serialize_owned_set(key, &items, buf)
227    }
228}
229
230// ═══════════════════════════════════════════════════════════════════════
231// EventTrack
232// ═══════════════════════════════════════════════════════════════════════
233
234/// The Event Track (DM) Set — SMPTE ST 377-1:2019 Annex B §B.13 (byte
235/// 14/15 = `0x01`/`0x39`): a DM event-driven track with an edit rate
236/// and optional origin.
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct EventTrack {
239    /// Interchange Object (A.1) base properties.
240    pub interchange: InterchangeObjectFields,
241    /// Track ID (`0x4801`, Req).
242    pub track_id: u32,
243    /// Track Number (`0x4804`, Req, default 0).
244    pub track_number: u32,
245    /// Track Name (`0x4802`, Opt).
246    pub track_name: Option<String>,
247    /// Sequence (`0x4803`, Req) — strong reference to a Sequence.
248    pub sequence: StrongRef,
249    /// Event Edit Rate (`0x4901`, Req).
250    pub event_edit_rate: Rational,
251    /// Event Origin (`0x4902`, Opt, default 0).
252    pub event_origin: Option<i64>,
253    /// Unrecognized properties preserved for round-trip fidelity.
254    pub dark: Vec<(u16, Vec<u8>)>,
255}
256
257impl<'a> Parse<'a> for EventTrack {
258    type Error = Error;
259
260    fn parse(bytes: &'a [u8]) -> Result<Self> {
261        let set = LocalSet::parse(bytes)?;
262        if set.kind() != StructuralSetKind::EventTrackDm {
263            return Err(Error::KeyPrefixMismatch {
264                what: "Event Track (Table 17)",
265            });
266        }
267        let items = &set.items;
268        let interchange = InterchangeObjectFields::decode(items, "Event Track")?;
269        let track_id = u32::from_be_bytes(get_required_fixed::<4>(
270            items,
271            TAG_TRACK_ID,
272            "Track ID",
273            "Event Track",
274        )?);
275        let track_number = u32::from_be_bytes(get_required_fixed::<4>(
276            items,
277            TAG_TRACK_NUMBER,
278            "Track Number",
279            "Event Track",
280        )?);
281        let track_name = get_optional_raw(items, TAG_TRACK_NAME)
282            .map(decode_utf16_be)
283            .transpose()
284            .map_err(|_| Error::InvalidUtf16 {
285                tag: TAG_TRACK_NAME,
286                name: "Track Name",
287            })?;
288        let sequence = get_required_fixed::<16>(items, TAG_SEQUENCE, "Sequence", "Event Track")?;
289        let event_edit_rate = Rational::parse(get_required_raw(
290            items,
291            TAG_EVENT_EDIT_RATE,
292            "Event Edit Rate",
293            "Event Track",
294        )?)?;
295        let event_origin = get_optional_fixed::<8>(items, TAG_EVENT_ORIGIN, "Event Origin")?
296            .map(i64::from_be_bytes);
297        let dark = collect_dark(items, &EVENT_KNOWN_TAGS);
298
299        Ok(EventTrack {
300            interchange,
301            track_id,
302            track_number,
303            track_name,
304            sequence,
305            event_edit_rate,
306            event_origin,
307            dark,
308        })
309    }
310}
311
312impl EventTrack {
313    fn owned_items(&self) -> Vec<LocalSetOwnedItem> {
314        let mut out = Vec::new();
315        self.interchange.encode_into(&mut out);
316        out.push(LocalSetOwnedItem::fixed(
317            TAG_TRACK_ID,
318            self.track_id.to_be_bytes(),
319        ));
320        out.push(LocalSetOwnedItem::fixed(
321            TAG_TRACK_NUMBER,
322            self.track_number.to_be_bytes(),
323        ));
324        if let Some(n) = &self.track_name {
325            out.push(LocalSetOwnedItem::owned(TAG_TRACK_NAME, encode_utf16_be(n)));
326        }
327        out.push(LocalSetOwnedItem::fixed(TAG_SEQUENCE, self.sequence));
328        {
329            let mut buf = [0u8; RATIONAL_LEN];
330            self.event_edit_rate
331                .serialize_into(&mut buf)
332                .expect("fixed-size buffer");
333            out.push(LocalSetOwnedItem::owned(TAG_EVENT_EDIT_RATE, buf.to_vec()));
334        }
335        if let Some(o) = self.event_origin {
336            out.push(LocalSetOwnedItem::fixed(TAG_EVENT_ORIGIN, o.to_be_bytes()));
337        }
338        out
339    }
340}
341
342impl Serialize for EventTrack {
343    type Error = Error;
344
345    fn serialized_len(&self) -> usize {
346        let (key, items) = finish_owned_set(
347            StructuralSetKind::EventTrackDm,
348            self.owned_items(),
349            &self.dark,
350        );
351        owned_set_serialized_len(key, &items)
352    }
353
354    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
355        let (key, items) = finish_owned_set(
356            StructuralSetKind::EventTrackDm,
357            self.owned_items(),
358            &self.dark,
359        );
360        serialize_owned_set(key, &items, buf)
361    }
362}
363
364// ═══════════════════════════════════════════════════════════════════════
365// StaticTrack
366// ═══════════════════════════════════════════════════════════════════════
367
368/// The Static Track (DM) Set — SMPTE ST 377-1:2019 Annex B §B.14 (byte
369/// 14/15 = `0x01`/`0x3A`): a DM track with no temporal extent.  Carries
370/// only the Generic Track properties (B.6) — no additional fields.
371#[derive(Debug, Clone, PartialEq, Eq)]
372pub struct StaticTrack {
373    /// Interchange Object (A.1) base properties.
374    pub interchange: InterchangeObjectFields,
375    /// Track ID (`0x4801`, Req).
376    pub track_id: u32,
377    /// Track Number (`0x4804`, Req, default 0).
378    pub track_number: u32,
379    /// Track Name (`0x4802`, Opt).
380    pub track_name: Option<String>,
381    /// Sequence (`0x4803`, Req) — strong reference to a Sequence.
382    pub sequence: StrongRef,
383    /// Unrecognized properties preserved for round-trip fidelity.
384    pub dark: Vec<(u16, Vec<u8>)>,
385}
386
387impl<'a> Parse<'a> for StaticTrack {
388    type Error = Error;
389
390    fn parse(bytes: &'a [u8]) -> Result<Self> {
391        let set = LocalSet::parse(bytes)?;
392        if set.kind() != StructuralSetKind::StaticTrackDm {
393            return Err(Error::KeyPrefixMismatch {
394                what: "Static Track (Table 17)",
395            });
396        }
397        let items = &set.items;
398        let interchange = InterchangeObjectFields::decode(items, "Static Track")?;
399        let track_id = u32::from_be_bytes(get_required_fixed::<4>(
400            items,
401            TAG_TRACK_ID,
402            "Track ID",
403            "Static Track",
404        )?);
405        let track_number = u32::from_be_bytes(get_required_fixed::<4>(
406            items,
407            TAG_TRACK_NUMBER,
408            "Track Number",
409            "Static Track",
410        )?);
411        let track_name = get_optional_raw(items, TAG_TRACK_NAME)
412            .map(decode_utf16_be)
413            .transpose()
414            .map_err(|_| Error::InvalidUtf16 {
415                tag: TAG_TRACK_NAME,
416                name: "Track Name",
417            })?;
418        let sequence = get_required_fixed::<16>(items, TAG_SEQUENCE, "Sequence", "Static Track")?;
419        let dark = collect_dark(items, &STATIC_KNOWN_TAGS);
420
421        Ok(StaticTrack {
422            interchange,
423            track_id,
424            track_number,
425            track_name,
426            sequence,
427            dark,
428        })
429    }
430}
431
432impl StaticTrack {
433    fn owned_items(&self) -> Vec<LocalSetOwnedItem> {
434        let mut out = Vec::new();
435        self.interchange.encode_into(&mut out);
436        out.push(LocalSetOwnedItem::fixed(
437            TAG_TRACK_ID,
438            self.track_id.to_be_bytes(),
439        ));
440        out.push(LocalSetOwnedItem::fixed(
441            TAG_TRACK_NUMBER,
442            self.track_number.to_be_bytes(),
443        ));
444        if let Some(n) = &self.track_name {
445            out.push(LocalSetOwnedItem::owned(TAG_TRACK_NAME, encode_utf16_be(n)));
446        }
447        out.push(LocalSetOwnedItem::fixed(TAG_SEQUENCE, self.sequence));
448        out
449    }
450}
451
452impl Serialize for StaticTrack {
453    type Error = Error;
454
455    fn serialized_len(&self) -> usize {
456        let (key, items) = finish_owned_set(
457            StructuralSetKind::StaticTrackDm,
458            self.owned_items(),
459            &self.dark,
460        );
461        owned_set_serialized_len(key, &items)
462    }
463
464    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
465        let (key, items) = finish_owned_set(
466            StructuralSetKind::StaticTrackDm,
467            self.owned_items(),
468            &self.dark,
469        );
470        serialize_owned_set(key, &items, buf)
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    fn sample_timeline_track() -> TimelineTrack {
479        TimelineTrack {
480            interchange: InterchangeObjectFields {
481                instance_uid: [0x01; 16],
482                generation_uid: None,
483                object_class: None,
484            },
485            track_id: 1,
486            track_number: 0x15010100,
487            track_name: Some(String::from("Video")),
488            sequence: [0x02; 16],
489            edit_rate: Rational {
490                numerator: 25,
491                denominator: 1,
492            },
493            origin: 0,
494            dark: Vec::new(),
495        }
496    }
497
498    fn sample_event_track() -> EventTrack {
499        EventTrack {
500            interchange: InterchangeObjectFields {
501                instance_uid: [0x10; 16],
502                generation_uid: None,
503                object_class: None,
504            },
505            track_id: 3,
506            track_number: 0,
507            track_name: None,
508            sequence: [0x20; 16],
509            event_edit_rate: Rational {
510                numerator: 25,
511                denominator: 1,
512            },
513            event_origin: Some(100),
514            dark: Vec::new(),
515        }
516    }
517
518    fn sample_static_track() -> StaticTrack {
519        StaticTrack {
520            interchange: InterchangeObjectFields {
521                instance_uid: [0x30; 16],
522                generation_uid: None,
523                object_class: None,
524            },
525            track_id: 4,
526            track_number: 0,
527            track_name: None,
528            sequence: [0x40; 16],
529            dark: Vec::new(),
530        }
531    }
532
533    #[test]
534    fn timeline_track_round_trip() {
535        let tt = sample_timeline_track();
536        let bytes = tt.to_bytes();
537        let parsed = TimelineTrack::parse(&bytes).unwrap();
538        assert_eq!(parsed, tt);
539        assert_eq!(parsed.to_bytes(), bytes);
540    }
541
542    #[test]
543    fn timeline_track_no_name_round_trip() {
544        let mut tt = sample_timeline_track();
545        tt.track_name = None;
546        let bytes = tt.to_bytes();
547        let parsed = TimelineTrack::parse(&bytes).unwrap();
548        assert_eq!(parsed.track_name, None);
549        assert_eq!(parsed.to_bytes(), bytes);
550    }
551
552    #[test]
553    fn timeline_track_dark_preserved() {
554        let mut tt = sample_timeline_track();
555        tt.dark = alloc::vec![(0x8001, alloc::vec![0xAB])];
556        let bytes = tt.to_bytes();
557        let parsed = TimelineTrack::parse(&bytes).unwrap();
558        assert_eq!(parsed.dark, tt.dark);
559    }
560
561    #[test]
562    fn event_track_round_trip() {
563        let et = sample_event_track();
564        let bytes = et.to_bytes();
565        let parsed = EventTrack::parse(&bytes).unwrap();
566        assert_eq!(parsed, et);
567        assert_eq!(parsed.to_bytes(), bytes);
568    }
569
570    #[test]
571    fn event_track_no_origin_round_trip() {
572        let mut et = sample_event_track();
573        et.event_origin = None;
574        let bytes = et.to_bytes();
575        let parsed = EventTrack::parse(&bytes).unwrap();
576        assert_eq!(parsed.event_origin, None);
577        assert_eq!(parsed.to_bytes(), bytes);
578    }
579
580    #[test]
581    fn static_track_round_trip() {
582        let st = sample_static_track();
583        let bytes = st.to_bytes();
584        let parsed = StaticTrack::parse(&bytes).unwrap();
585        assert_eq!(parsed, st);
586        assert_eq!(parsed.to_bytes(), bytes);
587    }
588
589    #[test]
590    fn wrong_kind_rejected_timeline() {
591        let st = sample_static_track();
592        let bytes = st.to_bytes();
593        assert!(matches!(
594            TimelineTrack::parse(&bytes),
595            Err(Error::KeyPrefixMismatch { .. })
596        ));
597    }
598
599    #[test]
600    fn wrong_kind_rejected_event() {
601        let tt = sample_timeline_track();
602        let bytes = tt.to_bytes();
603        assert!(matches!(
604            EventTrack::parse(&bytes),
605            Err(Error::KeyPrefixMismatch { .. })
606        ));
607    }
608
609    #[test]
610    fn wrong_kind_rejected_static() {
611        let et = sample_event_track();
612        let bytes = et.to_bytes();
613        assert!(matches!(
614            StaticTrack::parse(&bytes),
615            Err(Error::KeyPrefixMismatch { .. })
616        ));
617    }
618
619    #[test]
620    fn mutation_changes_serialized_bytes() {
621        let mut tt = sample_timeline_track();
622        let before = tt.to_bytes();
623        tt.origin = 42;
624        let after = tt.to_bytes();
625        assert_ne!(before, after);
626        assert_eq!(TimelineTrack::parse(&after).unwrap().origin, 42);
627    }
628}