Skip to main content

siglus_assets/
mpeg2.rs

1//! Minimal MPEG-1/2 video probing.
2//!
3//! Siglus titles sometimes ship MPEG-PS/MPEG-2 video alongside OMV.
4//! For now we only implement lightweight parsing of the sequence header
5//! (0x000001B3) to retrieve dimensions and frame-rate code.
6
7use anyhow::{bail, Context, Result};
8use std::fs::File;
9use std::io::{Read, Seek, SeekFrom};
10use std::path::Path;
11
12#[derive(Debug, Clone, Copy)]
13pub struct MpegSeqHeader {
14    pub width: u16,
15    pub height: u16,
16    pub aspect_ratio_code: u8,
17    pub frame_rate_code: u8,
18    pub bit_rate: u32,
19    pub vbv_buffer_size: u16,
20    pub constrained_parameters_flag: bool,
21}
22
23/// Convert MPEG frame_rate_code to nominal FPS.
24///
25/// ISO/IEC 11172-2 / 13818-2 table 6-4.
26pub fn fps_from_frame_rate_code(code: u8) -> Option<f32> {
27    match code {
28        1 => Some(24000.0 / 1001.0),
29        2 => Some(24.0),
30        3 => Some(25.0),
31        4 => Some(30000.0 / 1001.0),
32        5 => Some(30.0),
33        6 => Some(50.0),
34        7 => Some(60000.0 / 1001.0),
35        8 => Some(60.0),
36        _ => None,
37    }
38}
39
40/// Scan the first `max_scan_bytes` bytes of a file for a MPEG sequence header.
41pub fn probe_sequence_header(
42    path: impl AsRef<Path>,
43    max_scan_bytes: usize,
44) -> Result<Option<MpegSeqHeader>> {
45    let mut f =
46        File::open(&path).with_context(|| format!("open MPEG: {}", path.as_ref().display()))?;
47    let file_len = f.seek(SeekFrom::End(0))?;
48    f.seek(SeekFrom::Start(0))?;
49    let to_read = std::cmp::min(max_scan_bytes as u64, file_len) as usize;
50    let mut buf = vec![0u8; to_read];
51    f.read_exact(&mut buf)?;
52    Ok(find_sequence_header(&buf))
53}
54
55pub fn find_sequence_header(data: &[u8]) -> Option<MpegSeqHeader> {
56    // Sequence header start code: 00 00 01 B3
57    let mut i = 0usize;
58    while i + 4 < data.len() {
59        if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 && data[i + 3] == 0xB3 {
60            // Need at least 8 bytes after the start code.
61            if i + 12 <= data.len() {
62                return parse_sequence_header(&data[i + 4..]);
63            }
64            return None;
65        }
66        i += 1;
67    }
68    None
69}
70
71fn parse_sequence_header(p: &[u8]) -> Option<MpegSeqHeader> {
72    // ISO/IEC 11172-2 / 13818-2 sequence_header()
73    // width: 12 bits, height: 12 bits, aspect_ratio:4, frame_rate:4,
74    // bit_rate: 18, marker:1, vbv_buffer_size:10, constrained:1
75    if p.len() < 8 {
76        return None;
77    }
78    let width = ((p[0] as u16) << 4) | ((p[1] as u16) >> 4);
79    let height = (((p[1] as u16) & 0x0F) << 8) | (p[2] as u16);
80    let aspect_ratio_code = (p[3] >> 4) & 0x0F;
81    let frame_rate_code = p[3] & 0x0F;
82
83    let bit_rate = ((p[4] as u32) << 10) | ((p[5] as u32) << 2) | ((p[6] as u32) >> 6);
84    let marker_bit = (p[6] >> 5) & 0x01;
85    if marker_bit != 1 {
86        // Not a hard failure, but usually indicates false-positive.
87        return None;
88    }
89    let vbv_buffer_size = (((p[6] as u16) & 0x1F) << 5) | ((p[7] as u16) >> 3);
90    let constrained_parameters_flag = ((p[7] >> 2) & 0x01) != 0;
91
92    Some(MpegSeqHeader {
93        width,
94        height,
95        aspect_ratio_code,
96        frame_rate_code,
97        bit_rate,
98        vbv_buffer_size,
99        constrained_parameters_flag,
100    })
101}
102
103/// Convenience: validate that a file looks like MPEG by finding a sequence header.
104pub fn ensure_mpeg_like(path: impl AsRef<Path>) -> Result<MpegSeqHeader> {
105    match probe_sequence_header(path, 1 << 20)? {
106        Some(h) => Ok(h),
107        None => bail!("no MPEG sequence header found"),
108    }
109}
110
111// -----------------------------------------------------------------------------------------------
112// Decode (na_mpeg2_decoder)
113// -----------------------------------------------------------------------------------------------
114
115/// A decoded video frame in interleaved RGBA8.
116#[derive(Debug, Clone)]
117pub struct VideoFrameRgba {
118    pub width: u32,
119    pub height: u32,
120    pub pts: Option<i64>,
121    pub rgba: Vec<u8>,
122}
123
124pub fn decode_mpeg2_to_rgba_frames(
125    path: impl AsRef<Path>,
126    max_frames: Option<usize>,
127) -> Result<Vec<VideoFrameRgba>> {
128    let path = path.as_ref();
129    let bytes = std::fs::read(path).with_context(|| format!("read MPEG: {}", path.display()))?;
130    let mut out = Vec::<VideoFrameRgba>::new();
131    let mut pipeline = na_mpeg2_decoder::MpegVideoPipeline::new();
132    pipeline
133        .push_with(&bytes, None, |f| {
134            if max_frames.map_or(false, |limit| out.len() >= limit) {
135                return;
136            }
137            let w = f.width as u32;
138            let h = f.height as u32;
139            let mut rgba = vec![0u8; (w as usize).saturating_mul(h as usize).saturating_mul(4)];
140            na_mpeg2_decoder::frame_to_rgba_bt601_limited(&f, &mut rgba);
141            out.push(VideoFrameRgba {
142                width: w,
143                height: h,
144                pts: None,
145                rgba,
146            });
147        })
148        .context("mpeg2 decode")?;
149    pipeline.flush_with(|f| {
150        if max_frames.map_or(false, |limit| out.len() >= limit) {
151            return;
152        }
153        let w = f.width as u32;
154        let h = f.height as u32;
155        let mut rgba = vec![0u8; (w as usize).saturating_mul(h as usize).saturating_mul(4)];
156        na_mpeg2_decoder::frame_to_rgba_bt601_limited(&f, &mut rgba);
157        out.push(VideoFrameRgba {
158            width: w,
159            height: h,
160            pts: None,
161            rgba,
162        });
163    })?;
164    if let Some(limit) = max_frames {
165        out.truncate(limit);
166    }
167    Ok(out)
168}