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