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