use std::io::{Read, SeekFrom, Write};
use oxideav_core::{
CodecId, CodecParameters, CodecResolver, Error, MediaType, Packet, PixelFormat, Result,
StreamInfo, TimeBase,
};
use oxideav_core::{ContainerRegistry, Demuxer, Muxer, ProbeData, ReadSeek, WriteSeek};
use crate::jpeg::markers::{self, EOI, SOI};
use crate::jpeg::parser::{parse_sof, SofInfo};
pub fn register(reg: &mut ContainerRegistry) {
reg.register_demuxer("jpeg", open_demuxer);
reg.register_muxer("jpeg", open_muxer);
reg.register_extension("jpg", "jpeg");
reg.register_extension("jpeg", "jpeg");
reg.register_extension("jpe", "jpeg");
reg.register_extension("jfif", "jpeg");
reg.register_probe("jpeg", probe);
}
pub fn probe(p: &ProbeData) -> u8 {
if p.buf.len() >= 3 && p.buf[0] == 0xFF && p.buf[1] == 0xD8 && p.buf[2] == 0xFF {
100
} else {
0
}
}
fn open_demuxer(
mut input: Box<dyn ReadSeek>,
_codecs: &dyn CodecResolver,
) -> Result<Box<dyn Demuxer>> {
input.seek(SeekFrom::Start(0))?;
let mut buf = Vec::new();
input.read_to_end(&mut buf)?;
drop(input);
let (start, end) = find_soi_eoi(&buf)?;
if start != 0 {
return Err(Error::invalid("JPEG: SOI not at offset 0"));
}
let data = buf[start..end].to_vec();
let sof = scan_for_sof(&data)?;
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));
let time_base = TimeBase::new(1, 1);
let stream = StreamInfo {
index: 0,
time_base,
duration: Some(1),
start_time: Some(0),
params,
};
let mut pkt = Packet::new(0, time_base, data);
pkt.pts = Some(0);
pkt.dts = Some(0);
pkt.duration = Some(1);
pkt.flags.keyframe = true;
Ok(Box::new(JpegDemuxer {
stream,
pending: Some(pkt),
}))
}
fn find_soi_eoi(buf: &[u8]) -> Result<(usize, usize)> {
if buf.len() < 4 {
return Err(Error::invalid("JPEG: file too short"));
}
if buf[0] != 0xFF || buf[1] != SOI {
return Err(Error::invalid("JPEG: missing SOI"));
}
let mut i = 2;
while i + 1 < buf.len() {
if buf[i] != 0xFF {
i += 1;
continue;
}
let mut j = i + 1;
while j < buf.len() && buf[j] == 0xFF {
j += 1;
}
if j >= buf.len() {
break;
}
let m = buf[j];
if m == 0x00 {
i = j + 1;
continue;
}
if markers::is_rst(m) {
i = j + 1;
continue;
}
if m == EOI {
return Ok((0, j + 1));
}
i = j + 1;
}
Err(Error::invalid("JPEG: no EOI marker found"))
}
fn scan_for_sof(data: &[u8]) -> Result<SofInfo> {
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("JPEG: 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,
}
}
struct JpegDemuxer {
stream: StreamInfo,
pending: Option<Packet>,
}
impl Demuxer for JpegDemuxer {
fn format_name(&self) -> &str {
"jpeg"
}
fn streams(&self) -> &[StreamInfo] {
std::slice::from_ref(&self.stream)
}
fn next_packet(&mut self) -> Result<Packet> {
self.pending.take().ok_or(Error::Eof)
}
}
fn open_muxer(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
if streams.len() != 1 {
return Err(Error::invalid(
"JPEG container holds exactly one video stream",
));
}
let s = &streams[0];
if s.params.media_type != MediaType::Video {
return Err(Error::invalid("JPEG container: stream must be video"));
}
if s.params.codec_id.as_str() != crate::CODEC_ID_STR {
return Err(Error::invalid(format!(
"JPEG container requires codec_id={} (got {})",
crate::CODEC_ID_STR,
s.params.codec_id
)));
}
Ok(Box::new(JpegMuxer {
output,
header_written: false,
wrote_packet: false,
trailer_written: false,
}))
}
struct JpegMuxer {
output: Box<dyn WriteSeek>,
header_written: bool,
wrote_packet: bool,
trailer_written: bool,
}
impl Muxer for JpegMuxer {
fn format_name(&self) -> &str {
"jpeg"
}
fn write_header(&mut self) -> Result<()> {
if self.header_written {
return Err(Error::other("JPEG muxer: write_header called twice"));
}
self.header_written = true;
Ok(())
}
fn write_packet(&mut self, packet: &Packet) -> Result<()> {
if !self.header_written {
return Err(Error::other("JPEG muxer: write_header not called"));
}
if self.wrote_packet {
return Err(Error::invalid(
"JPEG container is single-frame: exactly one packet per file",
));
}
self.output.write_all(&packet.data)?;
self.wrote_packet = true;
Ok(())
}
fn write_trailer(&mut self) -> Result<()> {
if self.trailer_written {
return Ok(());
}
self.output.flush()?;
self.trailer_written = true;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn probe_accepts_soi_ff() {
let p = ProbeData {
buf: &[0xFF, 0xD8, 0xFF, 0xE0],
ext: None,
};
assert_eq!(probe(&p), 100);
}
#[test]
fn probe_rejects_short_buffer() {
let p = ProbeData {
buf: &[0xFF, 0xD8],
ext: None,
};
assert_eq!(probe(&p), 0);
}
#[test]
fn probe_rejects_non_jpeg() {
let p = ProbeData {
buf: &[0x00, 0x00, 0x00, 0x00],
ext: None,
};
assert_eq!(probe(&p), 0);
}
#[test]
fn find_soi_eoi_trims_trailing_garbage() {
let jpeg = [0xFF, 0xD8, 0xFF, 0xD9, 0x00, 0x11, 0x22, 0x33];
let (s, e) = find_soi_eoi(&jpeg).expect("span");
assert_eq!((s, e), (0, 4));
}
#[test]
fn find_soi_eoi_rejects_missing_soi() {
let not_jpeg = [0x00, 0x01, 0x02, 0x03];
assert!(find_soi_eoi(¬_jpeg).is_err());
}
}