Skip to main content

mpeg_ps/
scr.rs

1//! System Clock Reference — 42-bit timestamp at 27 MHz
2//! (ISO/IEC 13818-1 §2.5.3.3, Table 2-39).
3//!
4//! Encoded across 6 bytes: 33-bit base + 9-bit extension, with four interleaved
5//! `marker_bit`s and a `'01'` prefix.
6
7use crate::error::{Error, Result};
8
9/// 33-bit base mask.
10const BASE_MASK: u64 = (1 << 33) - 1;
11/// 9-bit extension mask.
12const EXT_MASK: u64 = (1 << 9) - 1;
13
14/// Number of bytes in the SCR field (including the `01` prefix).
15pub(crate) const SCR_FIELD_LEN: usize = 6;
16
17/// System Clock Reference (42-bit total: 33-bit base + 9-bit extension,
18/// in 27 MHz units).
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize))]
21pub struct Scr {
22    /// 33-bit SCR base (27 MHz ticks, integer part).
23    pub base: u64,
24    /// 9-bit SCR extension (27 MHz ticks, fractional part / 300).
25    pub extension: u16,
26}
27
28impl Scr {
29    /// The total SCR value in 27 MHz ticks.
30    ///
31    /// `base * 300 + extension`.
32    #[must_use]
33    pub const fn ticks(self) -> u64 {
34        broadcast_common::clock33::pcr_from_parts(self.base, self.extension)
35    }
36
37    /// Value in seconds.
38    #[must_use]
39    pub fn seconds(self) -> f64 {
40        self.ticks() as f64 / broadcast_common::clock33::PCR_HZ as f64
41    }
42}
43
44/// Bit-packed SCR field layout (48 bits total, 6 bytes):
45///
46/// `01`                                    | 2  [47:46]
47/// `system_clock_reference_base[32:30]`    | 3  [45:43]
48/// `marker_bit`                            | 1  [42]
49/// `system_clock_reference_base[29:28]`    | 2  [41:40]
50/// `system_clock_reference_base[27:20]`    | 8  [39:32]
51/// `system_clock_reference_base[19:15]`    | 5  [31:27]
52/// `marker_bit`                            | 1  [26]
53/// `system_clock_reference_base[14:13]`    | 2  [25:24]
54/// `system_clock_reference_base[12:5]`     | 8  [23:16]
55/// `system_clock_reference_base[4:0]`      | 5  [15:11]
56/// `marker_bit`                            | 1  [10]
57/// `system_clock_reference_extension[8:7]` | 2  [9:8]
58/// `system_clock_reference_extension[6:0]` | 7  [7:1]
59/// `marker_bit`                            | 1  [0]
60pub(crate) fn read_scr_field(b: &[u8], what: &'static str) -> Result<Scr> {
61    if b.len() < SCR_FIELD_LEN {
62        return Err(Error::BufferTooShort {
63            need: SCR_FIELD_LEN,
64            have: b.len(),
65            what,
66        });
67    }
68
69    let b0 = b[0];
70    let b1 = b[1];
71    let b2 = b[2];
72    let b3 = b[3];
73    let b4 = b[4];
74    let b5 = b[5];
75
76    // W4 (#1119): distinguish the ISO/IEC 11172-1 (MPEG-1) `'0010'` pack
77    // prefix — a real, different (12-byte, no SCR-extension) layout this
78    // crate does not implement — from bytes that are neither a valid
79    // MPEG-1 nor MPEG-2 pack header. Previously both cases returned the
80    // same `BadScrPrefix`, so a conformant plain-MPEG-1 `.mpg` (VCD etc.)
81    // looked identical to actual corruption despite this crate's docs
82    // claiming "MPEG-1/2 Program Stream" support.
83    if (b0 >> 4) == 0b0010 {
84        return Err(Error::Mpeg1NotSupported);
85    }
86    // Check '01' prefix
87    if (b0 >> 6) != 0b01 {
88        return Err(Error::BadScrPrefix(b0 >> 6));
89    }
90
91    // marker bits
92    if (b0 & 0x04) == 0 {
93        return Err(Error::BadMarker(what));
94    }
95    if (b2 & 0x04) == 0 {
96        return Err(Error::BadMarker(what));
97    }
98    if (b4 & 0x04) == 0 {
99        return Err(Error::BadMarker(what));
100    }
101    if (b5 & 0x01) == 0 {
102        return Err(Error::BadMarker(what));
103    }
104
105    // base[32:30] from b0[5:3]
106    let hi = u64::from((b0 >> 3) & 0x07);
107    // base[29:28] from b0[1:0]
108    let mid_hi = u64::from(b0 & 0x03);
109    // base[27:20] from b1[7:0]
110    let mid_mid = u64::from(b1);
111    // base[19:15] from b2[7:3]
112    let mid_lo = u64::from((b2 >> 3) & 0x1F);
113    // base[14:13] from b2[1:0]
114    let lo_hi = u64::from(b2 & 0x03);
115    // base[12:5] from b3[7:0]
116    let lo_mid = u64::from(b3);
117    // base[4:0] from b4[7:3]
118    let lo_lo = u64::from((b4 >> 3) & 0x1F);
119
120    let base = (hi << 30)
121        | (mid_hi << 28)
122        | (mid_mid << 20)
123        | (mid_lo << 15)
124        | (lo_hi << 13)
125        | (lo_mid << 5)
126        | lo_lo;
127
128    // extension[8:7] from b4[1:0]
129    let ext_hi = u16::from(b4 & 0x03);
130    // extension[6:0] from b5[7:1]
131    let ext_lo = u16::from(b5 >> 1);
132
133    let extension = (ext_hi << 7) | ext_lo;
134
135    Ok(Scr { base, extension })
136}
137
138/// Checked encode of an SCR field: rejects a `base` wider than 33 bits or an
139/// `extension` wider than 9 bits instead of silently masking (#1129 class).
140pub(crate) fn encode_scr_field(scr: Scr) -> crate::error::Result<[u8; SCR_FIELD_LEN]> {
141    broadcast_common::len::fit_bits(scr.base, 33, "pack_header.scr.base")?;
142    broadcast_common::len::fit_bits(u64::from(scr.extension), 9, "pack_header.scr.extension")?;
143    Ok(write_scr_field(scr))
144}
145
146/// Encode a 42-bit SCR into a 6-byte SCR field (without pack_start_code).
147/// Infallible and masking: callers must range-check first (see
148/// [`encode_scr_field`]).
149pub(crate) fn write_scr_field(scr: Scr) -> [u8; SCR_FIELD_LEN] {
150    let base = scr.base & BASE_MASK;
151    let extension = (scr.extension as u64) & EXT_MASK;
152
153    let b0 = 0x40 // '01' in bits [7:6]
154        | (((base >> 30) & 0x07) as u8) << 3   // base[32:30] in bits [5:3]
155        | 0x04 // marker_bit at bit2
156        | ((base >> 28) & 0x03) as u8; // base[29:28] in bits [1:0]
157
158    let b1 = ((base >> 20) & 0xFF) as u8; // base[27:20]
159
160    let b2 = (((base >> 15) & 0x1F) as u8) << 3 // base[19:15] in bits [7:3]
161        | 0x04 // marker_bit at bit2
162        | ((base >> 13) & 0x03) as u8; // base[14:13] in bits [1:0]
163
164    let b3 = ((base >> 5) & 0xFF) as u8; // base[12:5]
165
166    let b4 = ((base & 0x1F) as u8) << 3 // base[4:0] in bits [7:3]
167        | 0x04 // marker_bit at bit2
168        | ((extension >> 7) & 0x03) as u8; // extension[8:7] in bits [1:0]
169
170    let b5 = (((extension & 0x7F) as u8) << 1) // extension[6:0] in bits [7:1]
171        | 0x01; // marker_bit at bit0
172
173    [b0, b1, b2, b3, b4, b5]
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn scr_round_trip_boundary_values() {
182        let cases: &[(u64, u16)] = &[
183            (0, 0),
184            (1, 0),
185            (0, 1),
186            (BASE_MASK, EXT_MASK as u16),
187            (0x0001_9999, 0x0123),
188            (0x0123_4567, 0x1AB),
189        ];
190        for &(base_val, ext) in cases {
191            let scr = Scr {
192                base: base_val,
193                extension: ext,
194            };
195            let enc = write_scr_field(scr);
196            let dec = read_scr_field(&enc, "SCR").unwrap();
197            assert_eq!(dec, scr, "base={base_val:#x}, ext={ext:#x}");
198        }
199    }
200
201    #[test]
202    fn scr_zero_matches_fixture_pattern() {
203        // The ffmpeg-muxed fixture uses SCR base=0, extension=0
204        let scr = Scr {
205            base: 0,
206            extension: 0,
207        };
208        let enc = write_scr_field(scr);
209        // Matches fixture bytes for the constant part of SCR: 44 00 04 00 04 01
210        assert_eq!(enc, [0x44, 0x00, 0x04, 0x00, 0x04, 0x01]);
211        let dec = read_scr_field(&enc, "SCR").unwrap();
212        assert_eq!(dec, scr);
213    }
214
215    #[test]
216    fn scr_rejects_bad_prefix() {
217        let mut enc = write_scr_field(Scr {
218            base: 0,
219            extension: 0,
220        });
221        enc[0] |= 0x80; // change '01' to '11'
222        assert!(matches!(
223            read_scr_field(&enc, "SCR"),
224            Err(Error::BadScrPrefix(_))
225        ));
226    }
227
228    /// W4 (#1119): a real ISO/IEC 11172-1 (MPEG-1) pack prefix (`'0010'`,
229    /// top 4 bits of byte 0) must be reported distinctly from actual
230    /// corruption, not folded into the same `BadScrPrefix` a genuinely
231    /// invalid byte gets.
232    #[test]
233    fn scr_reports_mpeg1_prefix_distinctly_from_corruption() {
234        let mut enc = write_scr_field(Scr {
235            base: 0,
236            extension: 0,
237        });
238        enc[0] = (enc[0] & 0x0F) | 0x20; // top 4 bits '0010' (MPEG-1 prefix)
239        assert!(matches!(
240            read_scr_field(&enc, "SCR"),
241            Err(Error::Mpeg1NotSupported)
242        ));
243    }
244
245    #[test]
246    fn scr_rejects_bad_marker() {
247        for byte_idx in [0usize, 2, 4] {
248            let mut enc = write_scr_field(Scr {
249                base: 0,
250                extension: 0,
251            });
252            enc[byte_idx] &= !0x04; // clear marker at bit2
253            assert!(matches!(
254                read_scr_field(&enc, "SCR"),
255                Err(Error::BadMarker(_))
256            ));
257        }
258        // marker in bit0 of byte5
259        let mut enc = write_scr_field(Scr {
260            base: 0,
261            extension: 0,
262        });
263        enc[5] &= 0xFE;
264        assert!(matches!(
265            read_scr_field(&enc, "SCR"),
266            Err(Error::BadMarker(_))
267        ));
268    }
269
270    #[test]
271    fn scr_ticks() {
272        let scr = Scr {
273            base: 100,
274            extension: 50,
275        };
276        assert_eq!(scr.ticks(), 100 * 300 + 50);
277    }
278
279    #[test]
280    fn scr_seconds() {
281        let scr = Scr {
282            base: 27_000_000 / 300,
283            extension: 0,
284        };
285        assert!((scr.seconds() - 1.0).abs() < 1e-9);
286    }
287}