use std::io::{Read, Seek, SeekFrom};
use oxideav_core::{
CodecId, CodecParameters, CodecResolver, Demuxer, Error, MediaType, Packet, PixelFormat,
ProbeData, Rational, ReadSeek, Result, StreamInfo, TimeBase,
};
use oxideav_core::{ContainerRegistry, ProbeScore};
use crate::jpeg::markers::{self, EOI, SOI};
use crate::jpeg::parser::{parse_sof, SofInfo};
const DEFAULT_FRAME_RATE: u32 = 25;
const SEEK_INDEX_INTERVAL: u64 = 5;
pub fn register(reg: &mut ContainerRegistry) {
reg.register_demuxer("mjpeg-raw", open_demuxer);
reg.register_extension("mjpeg", "mjpeg-raw");
reg.register_extension("mjpg", "mjpeg-raw");
reg.register_probe("mjpeg-raw", probe);
}
pub fn probe(p: &ProbeData) -> ProbeScore {
if p.buf.len() >= 3 && p.buf[0] == 0xFF && p.buf[1] == 0xD8 && p.buf[2] == 0xFF {
60
} else {
0
}
}
fn open_demuxer(
mut input: Box<dyn ReadSeek>,
_codecs: &dyn CodecResolver,
) -> Result<Box<dyn Demuxer>> {
input.seek(SeekFrom::Start(0))?;
let (first_start, first_end) = scan_one_frame(input.as_mut(), 0)?;
let mut first_bytes = vec![0u8; (first_end - first_start) as usize];
input.seek(SeekFrom::Start(first_start))?;
input.read_exact(&mut first_bytes)?;
let sof = scan_for_sof(&first_bytes)?;
let mut params = CodecParameters::video(CodecId::new(crate::CODEC_ID_STR));
params.media_type = MediaType::Video;
params.width = Some(sof.width as u32);
params.height = Some(sof.height as u32);
params.pixel_format = Some(pixel_format_for_sof(&sof));
params.frame_rate = Some(Rational::new(DEFAULT_FRAME_RATE as i64, 1));
let time_base = TimeBase::new(1, DEFAULT_FRAME_RATE as i64);
let stream = StreamInfo {
index: 0,
time_base,
duration: None,
start_time: Some(0),
params,
};
let mut idx = SeekIndex::default();
idx.maybe_push(0, first_start);
input.seek(SeekFrom::Start(0))?;
Ok(Box::new(MjpegDemuxer {
input,
stream,
time_base,
next_pts: 0,
next_offset: 0,
index: idx,
eof: false,
}))
}
fn scan_one_frame<R: Read + Seek + ?Sized>(input: &mut R, start_offset: u64) -> Result<(u64, u64)> {
input.seek(SeekFrom::Start(start_offset))?;
let mut pos = start_offset;
let mut buf = [0u8; 2];
pos = read_marker(input, pos, &mut buf)?;
if buf[0] != SOI {
return Err(Error::invalid("MJPEG: expected SOI at frame start"));
}
loop {
pos = read_marker(input, pos, &mut buf)?;
let marker = buf[0];
if marker == EOI {
return Ok((start_offset, pos));
}
if marker == SOI {
return Err(Error::invalid("MJPEG: unexpected SOI inside frame"));
}
if markers::is_rst(marker) {
continue;
}
if marker == markers::SOS {
pos = skip_length_prefixed_segment(input, pos)?;
pos = walk_entropy_scan(input, pos)?;
pos = post_scan_marker(input, pos, &mut buf)?;
input.seek(SeekFrom::Start(pos - 2))?;
pos -= 2;
continue;
}
pos = skip_length_prefixed_segment(input, pos)?;
}
}
fn read_marker<R: Read + ?Sized>(input: &mut R, mut pos: u64, buf: &mut [u8; 2]) -> Result<u64> {
let mut one = [0u8; 1];
let n = input.read(&mut one)?;
if n == 0 {
return Err(Error::Eof);
}
pos += 1;
if one[0] != 0xFF {
return Err(Error::invalid("MJPEG: expected 0xFF marker prefix"));
}
loop {
let n = input.read(&mut one)?;
if n == 0 {
return Err(Error::invalid("MJPEG: truncated marker"));
}
pos += 1;
if one[0] != 0xFF {
buf[0] = one[0];
buf[1] = 0xFF; return Ok(pos);
}
}
}
fn skip_length_prefixed_segment<R: Read + Seek + ?Sized>(input: &mut R, pos: u64) -> Result<u64> {
let mut lp = [0u8; 2];
let n = input.read(&mut lp)?;
if n < 2 {
return Err(Error::invalid("MJPEG: truncated segment length field"));
}
let len = u16::from_be_bytes(lp) as u64;
if len < 2 {
return Err(Error::invalid("MJPEG: segment length < 2"));
}
let body = len - 2;
input.seek(SeekFrom::Current(body as i64))?;
Ok(pos + 2 + body)
}
fn walk_entropy_scan<R: Read + Seek + ?Sized>(input: &mut R, mut pos: u64) -> Result<u64> {
let mut buf = [0u8; 1];
loop {
let n = input.read(&mut buf)?;
if n == 0 {
return Err(Error::invalid("MJPEG: EOF inside entropy scan"));
}
pos += 1;
if buf[0] != 0xFF {
continue;
}
let n = input.read(&mut buf)?;
if n == 0 {
return Err(Error::invalid("MJPEG: truncated marker in scan"));
}
pos += 1;
while buf[0] == 0xFF {
let n = input.read(&mut buf)?;
if n == 0 {
return Err(Error::invalid("MJPEG: truncated marker in scan"));
}
pos += 1;
}
if buf[0] == 0x00 {
continue;
}
if markers::is_rst(buf[0]) {
continue;
}
input.seek(SeekFrom::Current(-2))?;
return Ok(pos - 2);
}
}
fn post_scan_marker<R: Read + ?Sized>(
input: &mut R,
mut pos: u64,
buf: &mut [u8; 2],
) -> Result<u64> {
let n = input.read(buf)?;
if n < 2 {
return Err(Error::invalid("MJPEG: truncated post-scan marker"));
}
pos += 2;
if buf[0] != 0xFF {
return Err(Error::invalid("MJPEG: post-scan marker missing 0xFF"));
}
buf[0] = buf[1];
Ok(pos)
}
fn scan_for_sof(data: &[u8]) -> Result<SofInfo> {
if data.len() < 2 || data[0] != 0xFF || data[1] != SOI {
return Err(Error::invalid("MJPEG: first frame missing SOI"));
}
let body = &data[2..];
let mut walker = crate::jpeg::parser::MarkerWalker::new(body);
loop {
let Some(marker) = walker.next_marker()? else {
return Err(Error::invalid("MJPEG: SOF not found before EOF"));
};
if markers::is_sof(marker) {
let payload = walker.read_segment_payload()?;
return Ok(parse_sof(payload)?);
}
if marker == SOI || marker == EOI || markers::is_rst(marker) {
continue;
}
let _ = walker.read_segment_payload()?;
}
}
fn pixel_format_for_sof(sof: &SofInfo) -> PixelFormat {
match sof.components.len() {
1 => PixelFormat::Gray8,
3 => {
let y = sof.components[0];
match (y.h_factor, y.v_factor) {
(2, 2) => PixelFormat::Yuv420P,
(2, 1) => PixelFormat::Yuv422P,
(1, 1) => PixelFormat::Yuv444P,
_ => PixelFormat::Yuv420P,
}
}
_ => PixelFormat::Yuv420P,
}
}
#[derive(Default, Debug)]
struct SeekIndex {
pts: Vec<i64>,
offsets: Vec<u64>,
}
impl SeekIndex {
fn maybe_push(&mut self, pts: i64, offset: u64) {
if matches!(self.pts.last(), Some(&p) if p == pts) {
return;
}
self.pts.push(pts);
self.offsets.push(offset);
}
fn lookup(&self, target: i64) -> (i64, u64) {
if self.pts.is_empty() {
return (0, 0);
}
let idx = self.pts.partition_point(|&p| p <= target);
if idx == 0 {
(self.pts[0], self.offsets[0])
} else {
(self.pts[idx - 1], self.offsets[idx - 1])
}
}
}
pub(crate) struct MjpegDemuxer {
input: Box<dyn ReadSeek>,
stream: StreamInfo,
time_base: TimeBase,
next_pts: i64,
next_offset: u64,
index: SeekIndex,
eof: bool,
}
impl Demuxer for MjpegDemuxer {
fn format_name(&self) -> &str {
"mjpeg-raw"
}
fn streams(&self) -> &[StreamInfo] {
std::slice::from_ref(&self.stream)
}
fn next_packet(&mut self) -> Result<Packet> {
if self.eof {
return Err(Error::Eof);
}
self.input.seek(SeekFrom::Start(self.next_offset))?;
let frame_start = self.next_offset;
let frame_pts = self.next_pts;
let (start, end) = match scan_one_frame(self.input.as_mut(), frame_start) {
Ok(span) => span,
Err(Error::Eof) => {
self.eof = true;
return Err(Error::Eof);
}
Err(e) => return Err(e),
};
debug_assert_eq!(start, frame_start);
let len = (end - start) as usize;
let mut data = vec![0u8; len];
self.input.seek(SeekFrom::Start(start))?;
self.input.read_exact(&mut data)?;
self.next_pts = frame_pts + 1;
self.next_offset = end;
let frame_idx = frame_pts as u64;
if frame_idx % SEEK_INDEX_INTERVAL == 0 {
self.index.maybe_push(frame_pts, frame_start);
}
let mut pkt = Packet::new(0, self.time_base, data);
pkt.pts = Some(frame_pts);
pkt.dts = Some(frame_pts);
pkt.duration = Some(1);
pkt.flags.keyframe = true;
Ok(pkt)
}
fn seek_to(&mut self, stream_index: u32, pts: i64) -> Result<i64> {
if stream_index != 0 {
return Err(Error::invalid(format!(
"MJPEG: stream index {stream_index} out of range (only stream 0 exists)"
)));
}
let target = pts.max(0);
let (anchor_pts, anchor_offset) = self.index.lookup(target);
self.next_pts = anchor_pts;
self.next_offset = anchor_offset;
self.eof = false;
if anchor_pts == target {
return Ok(target);
}
let mut cur_pts = anchor_pts;
let mut cur_offset = anchor_offset;
let mut landed_pts = anchor_pts;
while cur_pts < target {
self.input.seek(SeekFrom::Start(cur_offset))?;
let span = scan_one_frame(self.input.as_mut(), cur_offset);
let (start, end) = match span {
Ok(s) => s,
Err(Error::Eof) | Err(Error::InvalidData(_)) => {
self.eof = true;
self.next_offset = cur_offset;
self.next_pts = cur_pts;
return Ok(landed_pts);
}
Err(e) => return Err(e),
};
let frame_idx = cur_pts as u64;
if frame_idx % SEEK_INDEX_INTERVAL == 0 {
self.index.maybe_push(cur_pts, start);
}
landed_pts = cur_pts;
cur_pts += 1;
cur_offset = end;
}
self.next_pts = cur_pts;
self.next_offset = cur_offset;
self.input.seek(SeekFrom::Start(cur_offset))?;
if (cur_pts as u64) % SEEK_INDEX_INTERVAL == 0 {
self.index.maybe_push(cur_pts, cur_offset);
}
Ok(target)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn fake_mjpeg(n_frames: usize) -> Vec<u8> {
let frame = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x02, 0xFF, 0xD9];
let mut buf = Vec::with_capacity(n_frames * frame.len());
for _ in 0..n_frames {
buf.extend_from_slice(&frame);
}
buf
}
#[test]
fn probe_score_below_still_container() {
let p = ProbeData {
buf: &[0xFF, 0xD8, 0xFF, 0xE0],
ext: None,
};
assert_eq!(probe(&p), 60);
}
#[test]
fn scan_one_frame_returns_soi_to_eoi_span() {
let bytes = fake_mjpeg(3);
let mut cur = Cursor::new(bytes.clone());
let (s, e) = scan_one_frame(&mut cur, 0).expect("first frame");
assert_eq!((s, e), (0, 8));
let (s2, e2) = scan_one_frame(&mut cur, e).expect("second frame");
assert_eq!((s2, e2), (8, 16));
}
#[test]
fn scan_one_frame_ignores_stuffed_ff_d8_in_entropy_data() {
let bytes = vec![
0xFF, 0xD8, 0xFF, 0xDA, 0x00, 0x02, 0xFF, 0x00, 0xD8, 0xD8, 0xFF, 0xD9, ];
let mut cur = Cursor::new(bytes.clone());
let (s, e) = scan_one_frame(&mut cur, 0).expect("scan");
assert_eq!((s, e), (0, bytes.len() as u64));
}
#[test]
fn seek_index_lookup_returns_largest_le() {
let mut idx = SeekIndex::default();
idx.maybe_push(0, 0);
idx.maybe_push(5, 100);
idx.maybe_push(10, 200);
assert_eq!(idx.lookup(0), (0, 0));
assert_eq!(idx.lookup(4), (0, 0));
assert_eq!(idx.lookup(5), (5, 100));
assert_eq!(idx.lookup(7), (5, 100));
assert_eq!(idx.lookup(10), (10, 200));
assert_eq!(idx.lookup(99), (10, 200));
}
}