use crate::error::{MjpegError as Error, Result};
use crate::jpeg::huffman::{
STD_AC_CHROMA_BITS, STD_AC_CHROMA_VALS, STD_AC_LUMA_BITS, STD_AC_LUMA_VALS, STD_DC_CHROMA_BITS,
STD_DC_CHROMA_VALS, STD_DC_LUMA_BITS, STD_DC_LUMA_VALS,
};
use crate::jpeg::markers;
use crate::jpeg::quant::{scale_for_quality, DEFAULT_CHROMA_Q50, DEFAULT_LUMA_Q50};
use crate::jpeg::zigzag::ZIGZAG;
const MAIN_HDR_LEN: usize = 8;
const RST_HDR_LEN: usize = 4;
const QTBL_HDR_LEN: usize = 4;
const TYPE_RESTART_BIT: u8 = 0x40;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MainHeader {
pub type_specific: u8,
pub fragment_offset: u32,
pub typ: u8,
pub q: u8,
pub width: u16,
pub height: u16,
}
impl MainHeader {
pub fn has_restart(&self) -> bool {
self.typ & TYPE_RESTART_BIT != 0
}
pub fn base_type(&self) -> u8 {
self.typ & !TYPE_RESTART_BIT
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RestartHeader {
pub restart_interval: u16,
pub first: bool,
pub last: bool,
pub count: u16,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Progress {
NeedMore,
Frame(Vec<u8>),
}
pub fn parse_main_header(payload: &[u8]) -> Result<MainHeader> {
if payload.len() < MAIN_HDR_LEN {
return Err(Error::invalid("RTP/JPEG: payload shorter than main header"));
}
let fragment_offset =
((payload[1] as u32) << 16) | ((payload[2] as u32) << 8) | (payload[3] as u32);
Ok(MainHeader {
type_specific: payload[0],
fragment_offset,
typ: payload[4],
q: payload[5],
width: (payload[6] as u16) * 8,
height: (payload[7] as u16) * 8,
})
}
pub fn parse_restart_header(bytes: &[u8]) -> Result<RestartHeader> {
if bytes.len() < RST_HDR_LEN {
return Err(Error::invalid("RTP/JPEG: truncated restart-marker header"));
}
let restart_interval = u16::from_be_bytes([bytes[0], bytes[1]]);
if restart_interval == 0 {
return Err(Error::invalid("RTP/JPEG: zero restart interval"));
}
let fl_count = u16::from_be_bytes([bytes[2], bytes[3]]);
Ok(RestartHeader {
restart_interval,
first: fl_count & 0x8000 != 0,
last: fl_count & 0x4000 != 0,
count: fl_count & 0x3FFF,
})
}
#[derive(Clone, Copy)]
struct QuantPair {
luma: [u8; 64],
chroma: [u8; 64],
}
fn tables_from_q(q: u8) -> QuantPair {
let luma_nat = scale_for_quality(&DEFAULT_LUMA_Q50, q);
let chroma_nat = scale_for_quality(&DEFAULT_CHROMA_Q50, q);
let mut luma = [0u8; 64];
let mut chroma = [0u8; 64];
for k in 0..64 {
luma[k] = luma_nat[ZIGZAG[k]].min(255) as u8;
chroma[k] = chroma_nat[ZIGZAG[k]].min(255) as u8;
}
QuantPair { luma, chroma }
}
fn write_dqt8(out: &mut Vec<u8>, table_id: u8, zigzag_vals: &[u8; 64]) {
out.push(0xFF);
out.push(markers::DQT);
out.push(0x00);
out.push(67);
out.push(table_id & 0x0F); out.extend_from_slice(zigzag_vals);
}
fn write_sof0(out: &mut Vec<u8>, width: u16, height: u16, luma_h: u8, luma_v: u8) {
out.push(0xFF);
out.push(markers::SOF0);
out.push(0x00);
out.push(17); out.push(8); out.extend_from_slice(&height.to_be_bytes());
out.extend_from_slice(&width.to_be_bytes());
out.push(3); out.push(1);
out.push((luma_h << 4) | luma_v);
out.push(0);
out.push(2);
out.push(0x11);
out.push(1);
out.push(3);
out.push(0x11);
out.push(1);
}
fn write_dht(out: &mut Vec<u8>, class: u8, id: u8, bits: &[u8; 16], vals: &[u8]) {
out.push(0xFF);
out.push(markers::DHT);
let len = 2 + 1 + 16 + vals.len();
out.extend_from_slice(&(len as u16).to_be_bytes());
out.push(((class & 0x01) << 4) | (id & 0x0F));
out.extend_from_slice(bits);
out.extend_from_slice(vals);
}
fn write_dri(out: &mut Vec<u8>, interval: u16) {
out.push(0xFF);
out.push(markers::DRI);
out.push(0x00);
out.push(0x04);
out.extend_from_slice(&interval.to_be_bytes());
}
fn write_sos(out: &mut Vec<u8>) {
out.push(0xFF);
out.push(markers::SOS);
out.push(0x00);
out.push(12); out.push(3); out.push(1);
out.push(0x00); out.push(2);
out.push(0x11); out.push(3);
out.push(0x11); out.push(0); out.push(63); out.push(0); }
fn build_headers(
width: u16,
height: u16,
base_type: u8,
quant: &QuantPair,
dri: u16,
) -> Result<Vec<u8>> {
let (luma_h, luma_v) = match base_type {
0 => (2u8, 1u8),
1 => (2u8, 2u8),
other => {
return Err(Error::unsupported(format!(
"RTP/JPEG: type {other} is not a well-known fixed mapping (only 0/1 + 64/65)"
)));
}
};
let mut out = Vec::with_capacity(700);
out.push(0xFF);
out.push(markers::SOI);
write_dqt8(&mut out, 0, &quant.luma);
write_dqt8(&mut out, 1, &quant.chroma);
write_sof0(&mut out, width, height, luma_h, luma_v);
write_dht(&mut out, 0, 0, &STD_DC_LUMA_BITS, &STD_DC_LUMA_VALS);
write_dht(&mut out, 1, 0, &STD_AC_LUMA_BITS, &STD_AC_LUMA_VALS);
write_dht(&mut out, 0, 1, &STD_DC_CHROMA_BITS, &STD_DC_CHROMA_VALS);
write_dht(&mut out, 1, 1, &STD_AC_CHROMA_BITS, &STD_AC_CHROMA_VALS);
if dri != 0 {
write_dri(&mut out, dri);
}
write_sos(&mut out);
Ok(out)
}
struct FrameState {
main: MainHeader,
dri: u16,
quant: Option<QuantPair>,
scan: Vec<u8>,
scan_len: usize,
}
#[derive(Default)]
pub struct JpegDepacketizer {
state: Option<FrameState>,
cached_tables: Option<(u8, QuantPair)>,
}
impl JpegDepacketizer {
pub fn new() -> Self {
Self {
state: None,
cached_tables: None,
}
}
pub fn reset(&mut self) {
self.state = None;
}
pub fn push(&mut self, payload: &[u8], marker: bool) -> Result<Progress> {
let main = parse_main_header(payload)?;
let mut cursor = MAIN_HDR_LEN;
let dri = if main.has_restart() {
let rst = parse_restart_header(&payload[cursor..])?;
cursor += RST_HDR_LEN;
rst.restart_interval
} else {
0
};
let inband_quant = if main.q >= 128 && main.fragment_offset == 0 {
let (qp, consumed) = parse_qtable_header(&payload[cursor..], main.q)?;
cursor += consumed;
if let Some(qp) = qp {
if (128..=254).contains(&main.q) {
self.cached_tables = Some((main.q, qp));
}
}
qp
} else {
None
};
let scan = &payload[cursor..];
if main.fragment_offset == 0 || self.state.is_none() {
if main.fragment_offset != 0 && self.state.is_none() {
return Err(Error::invalid(
"RTP/JPEG: first fragment has non-zero offset (lost frame start)",
));
}
self.state = Some(FrameState {
main,
dri,
quant: inband_quant,
scan: Vec::new(),
scan_len: 0,
});
}
let st = self.state.as_mut().expect("frame state initialised above");
if st.main.typ != main.typ
|| st.main.q != main.q
|| st.main.width != main.width
|| st.main.height != main.height
{
return Err(Error::invalid(
"RTP/JPEG: fragment header disagrees with frame start",
));
}
let off = main.fragment_offset as usize;
let end = off + scan.len();
if end > st.scan.len() {
st.scan.resize(end, 0);
}
st.scan[off..end].copy_from_slice(scan);
st.scan_len = st.scan_len.max(end);
if !marker {
return Ok(Progress::NeedMore);
}
let st = self.state.take().expect("frame state present at marker");
let jpeg = self.assemble(st)?;
Ok(Progress::Frame(jpeg))
}
fn assemble(&self, st: FrameState) -> Result<Vec<u8>> {
let quant = match st.quant {
Some(q) => q,
None => {
if st.main.q >= 128 {
match self.cached_tables {
Some((cached_q, qp)) if cached_q == st.main.q => qp,
_ => {
return Err(Error::unsupported(
"RTP/JPEG: Q >= 128 without in-band tables and none cached \
for this Q (out-of-band negotiation unsupported)",
));
}
}
} else if st.main.q == 0 {
return Err(Error::invalid("RTP/JPEG: Q = 0 is reserved"));
} else {
tables_from_q(st.main.q)
}
}
};
let mut out = build_headers(
st.main.width,
st.main.height,
st.main.base_type(),
&quant,
st.dri,
)?;
out.extend_from_slice(&st.scan[..st.scan_len]);
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
}
fn parse_qtable_header(bytes: &[u8], q: u8) -> Result<(Option<QuantPair>, usize)> {
if bytes.len() < QTBL_HDR_LEN {
return Err(Error::invalid(
"RTP/JPEG: truncated quantization-table header",
));
}
let precision = bytes[1];
let length = u16::from_be_bytes([bytes[2], bytes[3]]) as usize;
if length == 0 {
if q == 255 {
return Err(Error::invalid("RTP/JPEG: Q = 255 with zero table length"));
}
return Ok((None, QTBL_HDR_LEN));
}
let data = &bytes[QTBL_HDR_LEN..];
if length > data.len() {
return Err(Error::invalid(
"RTP/JPEG: quantization-table length exceeds payload",
));
}
let mut offset = 0usize;
let mut read_table = |table_idx: usize| -> Result<[u8; 64]> {
let is16 = precision & (1 << table_idx) != 0;
let need = if is16 { 128 } else { 64 };
if offset + need > length {
return Err(Error::invalid(
"RTP/JPEG: quantization-table data truncated",
));
}
let mut out = [0u8; 64];
if is16 {
for k in 0..64 {
let v = u16::from_be_bytes([data[offset + k * 2], data[offset + k * 2 + 1]]);
out[k] = v.min(255) as u8;
}
} else {
out.copy_from_slice(&data[offset..offset + 64]);
}
offset += need;
Ok(out)
};
let luma = read_table(0)?;
let chroma = read_table(1)?;
Ok((Some(QuantPair { luma, chroma }), QTBL_HDR_LEN + length))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct JpegPacket {
pub payload: Vec<u8>,
pub marker: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QMode {
Quality(u8),
InBand(u8),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PacketizeOpts {
pub qmode: QMode,
pub restart_align: bool,
}
impl PacketizeOpts {
pub fn new(qmode: QMode) -> Self {
Self {
qmode,
restart_align: false,
}
}
pub fn with_restart_align(mut self, enabled: bool) -> Self {
self.restart_align = enabled;
self
}
}
struct JpegParts {
width: u16,
height: u16,
base_type: u8,
qt_luma: [u8; 64],
qt_chroma: [u8; 64],
dri: u16,
scan_start: usize,
scan_end: usize,
}
fn seg_len(jpeg: &[u8], pos: usize) -> Result<usize> {
if pos + 2 > jpeg.len() {
return Err(Error::invalid(
"RTP/JPEG packetize: truncated segment length",
));
}
Ok(u16::from_be_bytes([jpeg[pos], jpeg[pos + 1]]) as usize)
}
fn parse_jpeg(jpeg: &[u8]) -> Result<JpegParts> {
if jpeg.len() < 4 || jpeg[0] != 0xFF || jpeg[1] != markers::SOI {
return Err(Error::invalid("RTP/JPEG packetize: missing SOI"));
}
let mut pos = 2usize;
let mut width = 0u16;
let mut height = 0u16;
let mut base_type: Option<u8> = None;
let mut dqt: [Option<[u8; 64]>; 4] = [None, None, None, None];
let mut dri = 0u16;
loop {
while pos < jpeg.len() && jpeg[pos] == 0xFF && pos + 1 < jpeg.len() && jpeg[pos + 1] == 0xFF
{
pos += 1;
}
if pos + 1 >= jpeg.len() {
return Err(Error::invalid("RTP/JPEG packetize: ran off end before SOS"));
}
if jpeg[pos] != 0xFF {
return Err(Error::invalid("RTP/JPEG packetize: expected marker"));
}
let marker = jpeg[pos + 1];
pos += 2;
match marker {
markers::SOF0 | markers::SOF1 => {
let len = seg_len(jpeg, pos)?;
if len < 8 {
return Err(Error::invalid("RTP/JPEG packetize: truncated SOF"));
}
let body = pos + 2;
if body + (len - 2) > jpeg.len() {
return Err(Error::invalid("RTP/JPEG packetize: truncated SOF"));
}
let precision = jpeg[body];
if precision != 8 {
return Err(Error::unsupported(
"RTP/JPEG packetize: only 8-bit precision is carryable",
));
}
height = u16::from_be_bytes([jpeg[body + 1], jpeg[body + 2]]);
width = u16::from_be_bytes([jpeg[body + 3], jpeg[body + 4]]);
let nc = jpeg[body + 5];
if nc != 3 {
return Err(Error::unsupported(
"RTP/JPEG packetize: only three-component YUV is carryable",
));
}
if len < 8 + 3 * (nc as usize) {
return Err(Error::invalid(
"RTP/JPEG packetize: truncated SOF components",
));
}
let c0 = body + 6;
let y_samp = jpeg[c0 + 1];
let cb_samp = jpeg[c0 + 3 + 1];
let cr_samp = jpeg[c0 + 6 + 1];
let (yh, yv) = (y_samp >> 4, y_samp & 0x0F);
if cb_samp != 0x11 || cr_samp != 0x11 {
return Err(Error::unsupported(
"RTP/JPEG packetize: chroma must be 1x1 sampled (type 0/1)",
));
}
base_type = Some(match (yh, yv) {
(2, 1) => 0,
(2, 2) => 1,
_ => return Err(Error::unsupported(
"RTP/JPEG packetize: luma sampling must be 2x1 (type 0) or 2x2 (type 1)",
)),
});
pos = body + len - 2;
}
markers::DQT => {
let len = seg_len(jpeg, pos)?;
if len < 2 {
return Err(Error::invalid("RTP/JPEG packetize: truncated DQT"));
}
let body = pos + 2;
let end = body + (len - 2);
if end > jpeg.len() {
return Err(Error::invalid("RTP/JPEG packetize: truncated DQT"));
}
let mut i = body;
while i < end {
let pq_tq = jpeg[i];
let pq = pq_tq >> 4;
let tq = (pq_tq & 0x0F) as usize;
i += 1;
if pq != 0 {
return Err(Error::unsupported(
"RTP/JPEG packetize: 16-bit DQT not carryable in 8-bit type 0/1",
));
}
if tq >= 4 || i + 64 > end {
return Err(Error::invalid("RTP/JPEG packetize: bad DQT entry"));
}
let mut t = [0u8; 64];
t.copy_from_slice(&jpeg[i..i + 64]);
dqt[tq] = Some(t);
i += 64;
}
pos = end;
}
markers::DRI => {
let len = seg_len(jpeg, pos)?;
if len != 4 || pos + 4 > jpeg.len() {
return Err(Error::invalid("RTP/JPEG packetize: bad DRI"));
}
dri = u16::from_be_bytes([jpeg[pos + 2], jpeg[pos + 3]]);
pos += len;
}
markers::SOS => {
let len = seg_len(jpeg, pos)?;
if len < 2 || pos + len > jpeg.len() {
return Err(Error::invalid("RTP/JPEG packetize: truncated SOS"));
}
let scan_start = pos + len;
let mut s = scan_start;
let scan_end = loop {
if s + 1 >= jpeg.len() {
return Err(Error::invalid("RTP/JPEG packetize: scan has no EOI"));
}
if jpeg[s] == 0xFF {
let m = jpeg[s + 1];
if m == markers::EOI {
break s;
}
if m == 0xFF {
s += 1;
continue;
}
if m == 0x00 || markers::is_rst(m) {
s += 2;
continue;
}
break s;
}
s += 1;
};
let base_type = base_type
.ok_or_else(|| Error::invalid("RTP/JPEG packetize: SOS before SOF"))?;
let qt_luma = dqt[0]
.ok_or_else(|| Error::invalid("RTP/JPEG packetize: missing luma DQT (id 0)"))?;
let qt_chroma = dqt[1].ok_or_else(|| {
Error::invalid("RTP/JPEG packetize: missing chroma DQT (id 1)")
})?;
return Ok(JpegParts {
width,
height,
base_type,
qt_luma,
qt_chroma,
dri,
scan_start,
scan_end,
});
}
markers::SOF2 | markers::SOF3 | markers::SOF9 => {
return Err(Error::unsupported(
"RTP/JPEG packetize: only baseline SOF0/SOF1 is carryable",
));
}
markers::EOI => {
return Err(Error::invalid("RTP/JPEG packetize: EOI before SOS"));
}
_ if markers::is_rst(marker) => { }
_ => {
let len = seg_len(jpeg, pos)?;
if len < 2 || pos + len > jpeg.len() {
return Err(Error::invalid(
"RTP/JPEG packetize: truncated/oversized segment",
));
}
pos += len;
}
}
}
}
fn dim_units(width: u16, height: u16) -> Result<(u8, u8)> {
if width == 0 || height == 0 || width > 2040 || height > 2040 {
return Err(Error::unsupported(
"RTP/JPEG packetize: dimensions must be 8..=2040 px (8-pixel-unit wire field)",
));
}
Ok((width.div_ceil(8) as u8, height.div_ceil(8) as u8))
}
pub fn packetize(jpeg: &[u8], max_payload: usize, qmode: QMode) -> Result<Vec<JpegPacket>> {
packetize_with_opts(jpeg, max_payload, PacketizeOpts::new(qmode))
}
pub fn packetize_with_opts(
jpeg: &[u8],
max_payload: usize,
opts: PacketizeOpts,
) -> Result<Vec<JpegPacket>> {
let parts = parse_jpeg(jpeg)?;
let (w_units, h_units) = dim_units(parts.width, parts.height)?;
let (q_field, qtable_hdr): (u8, Vec<u8>) = match opts.qmode {
QMode::Quality(q) => {
if !(1..=99).contains(&q) {
return Err(Error::invalid(
"RTP/JPEG packetize: QMode::Quality must be 1..=99",
));
}
(q, Vec::new())
}
QMode::InBand(q) => {
if q < 128 {
return Err(Error::invalid(
"RTP/JPEG packetize: QMode::InBand Q must be 128..=255",
));
}
let mut h = Vec::with_capacity(QTBL_HDR_LEN + 128);
h.push(0); h.push(0); h.extend_from_slice(&128u16.to_be_bytes()); h.extend_from_slice(&parts.qt_luma);
h.extend_from_slice(&parts.qt_chroma);
(q, h)
}
};
let typ = if parts.dri != 0 {
parts.base_type | TYPE_RESTART_BIT
} else {
parts.base_type
};
let rst_len = if parts.dri != 0 { RST_HDR_LEN } else { 0 };
let first_hdr = MAIN_HDR_LEN + rst_len + qtable_hdr.len();
let cont_hdr = MAIN_HDR_LEN + rst_len;
if max_payload <= first_hdr || max_payload <= cont_hdr {
return Err(Error::invalid(
"RTP/JPEG packetize: max_payload too small for the headers",
));
}
let scan = &jpeg[parts.scan_start..parts.scan_end];
if opts.restart_align && parts.dri != 0 {
let intervals = scan_restart_intervals(scan);
return packetize_restart_aligned(
&intervals,
scan,
max_payload,
typ,
q_field,
w_units,
h_units,
parts.dri,
&qtable_hdr,
first_hdr,
cont_hdr,
);
}
let mut packets = Vec::new();
let mut offset = 0usize;
let mut first = true;
loop {
let hdr_room = if first { first_hdr } else { cont_hdr };
let chunk = (max_payload - hdr_room).min(scan.len() - offset);
let is_last = offset + chunk >= scan.len();
let mut payload = Vec::with_capacity(hdr_room + chunk);
payload.push(0); payload.push((offset >> 16) as u8);
payload.push((offset >> 8) as u8);
payload.push(offset as u8);
payload.push(typ);
payload.push(q_field);
payload.push(w_units);
payload.push(h_units);
if parts.dri != 0 {
payload.extend_from_slice(&parts.dri.to_be_bytes());
payload.extend_from_slice(&0xFFFFu16.to_be_bytes());
}
if first {
payload.extend_from_slice(&qtable_hdr);
}
payload.extend_from_slice(&scan[offset..offset + chunk]);
packets.push(JpegPacket {
payload,
marker: is_last,
});
offset += chunk;
first = false;
if is_last {
break;
}
}
Ok(packets)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct ScanInterval {
start: usize,
end: usize,
}
fn scan_restart_intervals(scan: &[u8]) -> Vec<ScanInterval> {
let mut out = Vec::new();
let mut start = 0usize;
let mut i = 0usize;
while i < scan.len() {
if scan[i] == 0xFF && i + 1 < scan.len() {
let m = scan[i + 1];
if markers::is_rst(m) {
out.push(ScanInterval { start, end: i + 2 });
start = i + 2;
i += 2;
continue;
}
if m == 0x00 || m == 0xFF {
i += 2;
continue;
}
break;
}
i += 1;
}
if start < scan.len() {
out.push(ScanInterval {
start,
end: scan.len(),
});
}
out
}
#[allow(clippy::too_many_arguments)]
fn packetize_restart_aligned(
intervals: &[ScanInterval],
scan: &[u8],
max_payload: usize,
typ: u8,
q_field: u8,
w_units: u8,
h_units: u8,
dri: u16,
qtable_hdr: &[u8],
first_hdr: usize,
cont_hdr: usize,
) -> Result<Vec<JpegPacket>> {
let mut packets = Vec::new();
let mut first = true;
let mut idx = 0usize;
while idx < intervals.len() {
let hdr_room = if first { first_hdr } else { cont_hdr };
let scan_budget = max_payload - hdr_room;
let mut taken = 0usize;
let mut bytes = 0usize;
let first_iv_idx = idx;
while idx + taken < intervals.len() {
let iv = intervals[idx + taken];
let iv_len = iv.end - iv.start;
if iv_len > scan_budget {
if taken == 0 {
return Err(Error::unsupported(
"RTP/JPEG packetize: restart-aligned splitting requires every \
interval to fit in max_payload (a single interval exceeds it)",
));
}
break;
}
if bytes + iv_len > scan_budget {
break;
}
bytes += iv_len;
taken += 1;
}
debug_assert!(taken > 0, "loop must accept at least one interval");
let fragment_offset = intervals[first_iv_idx].start;
let end_byte = intervals[first_iv_idx + taken - 1].end;
let is_last = first_iv_idx + taken == intervals.len();
let mut payload = Vec::with_capacity(hdr_room + bytes);
payload.push(0); payload.push((fragment_offset >> 16) as u8);
payload.push((fragment_offset >> 8) as u8);
payload.push(fragment_offset as u8);
payload.push(typ);
payload.push(q_field);
payload.push(w_units);
payload.push(h_units);
let count = (first_iv_idx as u16) % 0x3FFF;
let fl_count = 0x8000 | 0x4000 | count;
payload.extend_from_slice(&dri.to_be_bytes());
payload.extend_from_slice(&fl_count.to_be_bytes());
if first {
payload.extend_from_slice(qtable_hdr);
}
payload.extend_from_slice(&scan[fragment_offset..end_byte]);
packets.push(JpegPacket {
payload,
marker: is_last,
});
first = false;
idx += taken;
}
if packets.is_empty() {
let mut payload = Vec::with_capacity(first_hdr);
payload.push(0);
payload.extend_from_slice(&[0, 0, 0]);
payload.push(typ);
payload.push(q_field);
payload.push(w_units);
payload.push(h_units);
payload.extend_from_slice(&dri.to_be_bytes());
payload.extend_from_slice(&0xFFFFu16.to_be_bytes());
payload.extend_from_slice(qtable_hdr);
packets.push(JpegPacket {
payload,
marker: true,
});
}
Ok(packets)
}
#[cfg(test)]
mod tests {
use super::*;
fn payload_type1(width: u16, height: u16, q: u8, offset: u32, scan: &[u8]) -> Vec<u8> {
let mut p = vec![
0, (offset >> 16) as u8, (offset >> 8) as u8,
offset as u8,
1, q,
(width / 8) as u8, (height / 8) as u8, ];
p.extend_from_slice(scan);
p
}
#[test]
fn parses_main_header_fields() {
let p = payload_type1(320, 240, 50, 0, &[0xAA, 0xBB]);
let h = parse_main_header(&p).unwrap();
assert_eq!(h.fragment_offset, 0);
assert_eq!(h.typ, 1);
assert_eq!(h.q, 50);
assert_eq!(h.width, 320);
assert_eq!(h.height, 240);
assert!(!h.has_restart());
assert_eq!(h.base_type(), 1);
}
#[test]
fn width_height_are_eight_pixel_units() {
let p = payload_type1(320, 240, 1, 0, &[]);
let h = parse_main_header(&p).unwrap();
assert_eq!(h.width, 320);
assert_eq!(h.height, 240);
}
#[test]
fn rejects_short_payload() {
assert!(parse_main_header(&[0, 1, 2, 3]).is_err());
}
#[test]
fn single_packet_frame_reconstructs_valid_jpeg() {
let scan = vec![0x12, 0x34, 0x56, 0x78];
let p = payload_type1(64, 64, 50, 0, &scan);
let mut dp = JpegDepacketizer::new();
let prog = dp.push(&p, true).unwrap();
let jpeg = match prog {
Progress::Frame(j) => j,
Progress::NeedMore => panic!("expected a complete frame"),
};
assert_eq!(&jpeg[0..2], &[0xFF, markers::SOI]);
assert_eq!(&jpeg[jpeg.len() - 2..], &[0xFF, markers::EOI]);
let scan_start = jpeg.len() - 2 - scan.len();
assert_eq!(&jpeg[scan_start..jpeg.len() - 2], &scan[..]);
let count = |m: u8| {
jpeg.windows(2)
.filter(|w| w[0] == 0xFF && w[1] == m)
.count()
};
assert_eq!(count(markers::DQT), 2);
assert_eq!(count(markers::SOF0), 1);
assert_eq!(count(markers::DHT), 4);
assert_eq!(count(markers::DRI), 0);
}
#[test]
fn type1_emits_420_sampling() {
let p = payload_type1(64, 64, 50, 0, &[0u8; 8]);
let mut dp = JpegDepacketizer::new();
let jpeg = match dp.push(&p, true).unwrap() {
Progress::Frame(j) => j,
_ => panic!(),
};
let sof = jpeg
.windows(2)
.position(|w| w[0] == 0xFF && w[1] == markers::SOF0)
.unwrap();
let comp0_samp = jpeg[sof + 2 + 2 + 1 + 2 + 2 + 1 + 1];
assert_eq!(comp0_samp, 0x22); }
#[test]
fn type0_emits_422_sampling() {
let mut p = payload_type1(64, 64, 50, 0, &[0u8; 8]);
p[4] = 0; let mut dp = JpegDepacketizer::new();
let jpeg = match dp.push(&p, true).unwrap() {
Progress::Frame(j) => j,
_ => panic!(),
};
let sof = jpeg
.windows(2)
.position(|w| w[0] == 0xFF && w[1] == markers::SOF0)
.unwrap();
let comp0_samp = jpeg[sof + 2 + 2 + 1 + 2 + 2 + 1 + 1];
assert_eq!(comp0_samp, 0x21); }
#[test]
fn multi_fragment_reassembly_in_order() {
let first = payload_type1(64, 64, 50, 0, &[1, 2, 3, 4]);
let second = payload_type1(64, 64, 50, 4, &[5, 6, 7, 8]);
let mut dp = JpegDepacketizer::new();
assert_eq!(dp.push(&first, false).unwrap(), Progress::NeedMore);
let jpeg = match dp.push(&second, true).unwrap() {
Progress::Frame(j) => j,
_ => panic!(),
};
let scan_start = jpeg.len() - 2 - 8;
assert_eq!(&jpeg[scan_start..jpeg.len() - 2], &[1, 2, 3, 4, 5, 6, 7, 8]);
}
#[test]
fn multi_fragment_reassembly_out_of_order() {
let first = payload_type1(64, 64, 50, 0, &[1, 2, 3, 4]);
let second = payload_type1(64, 64, 50, 4, &[5, 6, 7, 8]);
let mut dp = JpegDepacketizer::new();
assert_eq!(dp.push(&first, false).unwrap(), Progress::NeedMore);
let jpeg = match dp.push(&second, true).unwrap() {
Progress::Frame(j) => j,
_ => panic!(),
};
let scan_start = jpeg.len() - 2 - 8;
assert_eq!(&jpeg[scan_start..jpeg.len() - 2], &[1, 2, 3, 4, 5, 6, 7, 8]);
}
#[test]
fn mid_frame_join_without_start_is_error() {
let mid = payload_type1(64, 64, 50, 16, &[9, 9]);
let mut dp = JpegDepacketizer::new();
assert!(dp.push(&mid, false).is_err());
}
#[test]
fn restart_type_emits_dri_and_consumes_header() {
let mut p = Vec::new();
p.extend_from_slice(&[0, 0, 0, 0]); p.push(64); p.push(50); p.push(8); p.push(8); p.extend_from_slice(&4u16.to_be_bytes());
p.extend_from_slice(&0xFFFFu16.to_be_bytes());
p.extend_from_slice(&[0xAA, 0xBB]); let mut dp = JpegDepacketizer::new();
let jpeg = match dp.push(&p, true).unwrap() {
Progress::Frame(j) => j,
_ => panic!(),
};
let dri_pos = jpeg
.windows(2)
.position(|w| w[0] == 0xFF && w[1] == markers::DRI)
.expect("DRI segment present for restart type");
let interval = u16::from_be_bytes([jpeg[dri_pos + 4], jpeg[dri_pos + 5]]);
assert_eq!(interval, 4);
assert_eq!(&jpeg[jpeg.len() - 4..jpeg.len() - 2], &[0xAA, 0xBB]);
}
#[test]
fn zero_restart_interval_rejected() {
let mut p = Vec::new();
p.extend_from_slice(&[0, 0, 0, 0]);
p.push(64);
p.push(50);
p.push(8);
p.push(8);
p.extend_from_slice(&0u16.to_be_bytes()); p.extend_from_slice(&0u16.to_be_bytes());
let mut dp = JpegDepacketizer::new();
assert!(dp.push(&p, true).is_err());
}
#[test]
fn inband_quant_tables_used() {
let mut p = Vec::new();
p.extend_from_slice(&[0, 0, 0, 0]); p.push(1); p.push(200); p.push(8);
p.push(8);
p.push(0);
p.push(0);
p.extend_from_slice(&128u16.to_be_bytes());
let luma: Vec<u8> = (0..64).map(|i| (i as u8) + 1).collect();
let chroma: Vec<u8> = (0..64).map(|i| 200 - i as u8).collect();
p.extend_from_slice(&luma);
p.extend_from_slice(&chroma);
p.extend_from_slice(&[0xCC, 0xDD]);
let mut dp = JpegDepacketizer::new();
let jpeg = match dp.push(&p, true).unwrap() {
Progress::Frame(j) => j,
_ => panic!(),
};
let dqt0 = jpeg
.windows(2)
.position(|w| w[0] == 0xFF && w[1] == markers::DQT)
.unwrap();
let table_start = dqt0 + 5;
assert_eq!(&jpeg[table_start..table_start + 64], &luma[..]);
}
#[test]
fn q255_zero_length_rejected() {
let mut p = Vec::new();
p.extend_from_slice(&[0, 0, 0, 0]);
p.push(1);
p.push(255); p.push(8);
p.push(8);
p.push(0);
p.push(0);
p.extend_from_slice(&0u16.to_be_bytes()); let mut dp = JpegDepacketizer::new();
assert!(dp.push(&p, true).is_err());
}
fn payload_q_inband(q: u8, tables: Option<(&[u8; 64], &[u8; 64])>, scan: &[u8]) -> Vec<u8> {
let mut p = vec![0, 0, 0, 0, 1, q, 8, 8]; p.push(0); p.push(0); match tables {
Some((luma, chroma)) => {
p.extend_from_slice(&128u16.to_be_bytes()); p.extend_from_slice(luma);
p.extend_from_slice(chroma);
}
None => p.extend_from_slice(&0u16.to_be_bytes()), }
p.extend_from_slice(scan);
p
}
fn dqt_of(jpeg: &[u8], which: usize) -> [u8; 64] {
let mut found = 0;
let mut pos = 2usize;
while pos + 4 < jpeg.len() {
if jpeg[pos] == 0xFF && jpeg[pos + 1] == markers::DQT {
if found == which {
let mut t = [0u8; 64];
t.copy_from_slice(&jpeg[pos + 5..pos + 5 + 64]);
return t;
}
found += 1;
let len = u16::from_be_bytes([jpeg[pos + 2], jpeg[pos + 3]]) as usize;
pos += 2 + len;
} else {
pos += 1;
}
}
panic!("DQT #{which} not found");
}
#[test]
fn static_q_tables_cached_then_reused_when_omitted() {
let luma: [u8; 64] = std::array::from_fn(|i| (i as u8) + 1);
let chroma: [u8; 64] = std::array::from_fn(|i| 200 - i as u8);
let mut dp = JpegDepacketizer::new();
let p1 = payload_q_inband(200, Some((&luma, &chroma)), &[0x01, 0x02]);
let j1 = match dp.push(&p1, true).unwrap() {
Progress::Frame(j) => j,
_ => panic!("frame 1"),
};
assert_eq!(dqt_of(&j1, 0), luma);
assert_eq!(dqt_of(&j1, 1), chroma);
let p2 = payload_q_inband(200, None, &[0x03, 0x04]);
let j2 = match dp.push(&p2, true).unwrap() {
Progress::Frame(j) => j,
_ => panic!("frame 2"),
};
assert_eq!(dqt_of(&j2, 0), luma, "frame 2 reuses cached luma table");
assert_eq!(dqt_of(&j2, 1), chroma, "frame 2 reuses cached chroma table");
assert_eq!(&j2[j2.len() - 4..j2.len() - 2], &[0x03, 0x04]);
}
#[test]
fn static_q_length_zero_without_prior_tables_errors() {
let mut dp = JpegDepacketizer::new();
let p = payload_q_inband(200, None, &[0xAA, 0xBB]);
assert!(dp.push(&p, true).is_err());
}
#[test]
fn cache_is_keyed_on_the_exact_q_value() {
let luma: [u8; 64] = std::array::from_fn(|i| (i as u8) + 1);
let chroma: [u8; 64] = std::array::from_fn(|i| 200 - i as u8);
let mut dp = JpegDepacketizer::new();
dp.push(
&payload_q_inband(200, Some((&luma, &chroma)), &[0x01]),
true,
)
.unwrap();
let other = payload_q_inband(201, None, &[0x02]);
assert!(dp.push(&other, true).is_err());
}
#[test]
fn q255_does_not_populate_the_static_cache() {
let luma: [u8; 64] = std::array::from_fn(|i| (i as u8) + 1);
let chroma: [u8; 64] = std::array::from_fn(|i| 200 - i as u8);
let mut dp = JpegDepacketizer::new();
dp.push(
&payload_q_inband(255, Some((&luma, &chroma)), &[0x01]),
true,
)
.unwrap();
let p = payload_q_inband(200, None, &[0x02]);
assert!(dp.push(&p, true).is_err());
}
#[test]
fn reset_keeps_the_table_cache() {
let luma: [u8; 64] = std::array::from_fn(|i| (i as u8) + 1);
let chroma: [u8; 64] = std::array::from_fn(|i| 200 - i as u8);
let mut dp = JpegDepacketizer::new();
dp.push(
&payload_q_inband(200, Some((&luma, &chroma)), &[0x01]),
true,
)
.unwrap();
assert_eq!(
dp.push(&payload_q_inband(200, None, &[0x02]), false)
.unwrap(),
Progress::NeedMore
);
dp.reset();
let j = match dp
.push(&payload_q_inband(200, None, &[0x03]), true)
.unwrap()
{
Progress::Frame(j) => j,
_ => panic!(),
};
assert_eq!(dqt_of(&j, 0), luma);
}
#[test]
fn tables_from_q_matches_ijg_formula() {
let qp = tables_from_q(50);
assert_eq!(qp.luma[0], DEFAULT_LUMA_Q50[0].min(255) as u8);
assert_eq!(qp.chroma[0], DEFAULT_CHROMA_Q50[0].min(255) as u8);
}
#[test]
fn fragment_header_mismatch_rejected() {
let first = payload_type1(64, 64, 50, 0, &[1, 2]);
let second = payload_type1(64, 64, 60, 2, &[3, 4]);
let mut dp = JpegDepacketizer::new();
assert_eq!(dp.push(&first, false).unwrap(), Progress::NeedMore);
assert!(dp.push(&second, true).is_err());
}
#[cfg(feature = "registry")]
use crate::decoder::decode_jpeg;
#[cfg(feature = "registry")]
use crate::encoder::encode_jpeg;
#[cfg(feature = "registry")]
use oxideav_core::frame::VideoPlane;
#[cfg(feature = "registry")]
use oxideav_core::{PixelFormat, VideoFrame};
#[cfg(feature = "registry")]
fn scan_span(jpeg: &[u8]) -> (usize, usize) {
let sos = jpeg
.windows(2)
.position(|w| w[0] == 0xFF && w[1] == markers::SOS)
.expect("SOS present");
let sos_len = u16::from_be_bytes([jpeg[sos + 2], jpeg[sos + 3]]) as usize;
let scan_start = sos + 2 + sos_len;
let eoi = jpeg.len() - 2;
assert_eq!(&jpeg[eoi..], &[0xFF, markers::EOI]);
(scan_start, eoi)
}
#[cfg(feature = "registry")]
fn dqt_table(jpeg: &[u8], which: usize) -> [u8; 64] {
let mut found = 0;
let mut pos = 2usize;
while pos + 4 < jpeg.len() {
if jpeg[pos] == 0xFF && jpeg[pos + 1] == markers::DQT {
if found == which {
let table_start = pos + 5; let mut t = [0u8; 64];
t.copy_from_slice(&jpeg[table_start..table_start + 64]);
return t;
}
found += 1;
let len = u16::from_be_bytes([jpeg[pos + 2], jpeg[pos + 3]]) as usize;
pos += 2 + len;
} else {
pos += 1;
}
}
panic!("DQT #{which} not found");
}
#[cfg(feature = "registry")]
fn make_420_frame(w: usize, h: usize) -> VideoFrame {
let cw = w.div_ceil(2);
let ch = h.div_ceil(2);
let y: Vec<u8> = (0..w * h).map(|i| ((i * 3) & 0xFF) as u8).collect();
let cb: Vec<u8> = (0..cw * ch).map(|i| ((i * 5 + 40) & 0xFF) as u8).collect();
let cr: Vec<u8> = (0..cw * ch).map(|i| ((i * 7 + 80) & 0xFF) as u8).collect();
VideoFrame {
pts: None,
planes: vec![
VideoPlane { stride: w, data: y },
VideoPlane {
stride: cw,
data: cb,
},
VideoPlane {
stride: cw,
data: cr,
},
],
}
}
#[cfg(feature = "registry")]
#[test]
fn end_to_end_inband_qtables_decodes() {
let (w, h) = (64usize, 64usize);
let frame = make_420_frame(w, h);
let jpeg =
encode_jpeg(&frame, w as u32, h as u32, PixelFormat::Yuv420P, 75).expect("encode");
let luma = dqt_table(&jpeg, 0);
let chroma = dqt_table(&jpeg, 1);
let (s0, s1) = scan_span(&jpeg);
let scan = &jpeg[s0..s1];
let mut payload = Vec::new();
payload.extend_from_slice(&[0, 0, 0, 0]); payload.push(1); payload.push(200); payload.push((w / 8) as u8);
payload.push((h / 8) as u8);
payload.push(0);
payload.push(0);
payload.extend_from_slice(&128u16.to_be_bytes());
payload.extend_from_slice(&luma);
payload.extend_from_slice(&chroma);
payload.extend_from_slice(scan);
let mut dp = JpegDepacketizer::new();
let rebuilt = match dp.push(&payload, true).unwrap() {
Progress::Frame(j) => j,
_ => panic!("expected complete frame"),
};
let decoded = decode_jpeg(&rebuilt, None).expect("decode reconstructed RTP/JPEG");
assert_eq!(decoded.planes.len(), 3);
assert_eq!(decoded.planes[0].data.len(), w * h);
assert_eq!(decoded.planes[1].data.len(), w.div_ceil(2) * h.div_ceil(2));
}
#[cfg(feature = "registry")]
#[test]
fn end_to_end_q_field_tables_decodes() {
let (w, h) = (64usize, 48usize);
let frame = make_420_frame(w, h);
let jpeg =
encode_jpeg(&frame, w as u32, h as u32, PixelFormat::Yuv420P, 50).expect("encode");
let (s0, s1) = scan_span(&jpeg);
let scan = &jpeg[s0..s1];
let payload = {
let mut p = Vec::new();
p.extend_from_slice(&[0, 0, 0, 0]);
p.push(1); p.push(50); p.push((w / 8) as u8);
p.push((h / 8) as u8);
p.extend_from_slice(scan);
p
};
let mut dp = JpegDepacketizer::new();
let rebuilt = match dp.push(&payload, true).unwrap() {
Progress::Frame(j) => j,
_ => panic!(),
};
let decoded = decode_jpeg(&rebuilt, None).expect("decode Q-field RTP/JPEG");
assert_eq!(decoded.planes.len(), 3);
assert_eq!(decoded.planes[0].data.len(), w * h);
}
fn handmade_jpeg(width: u16, height: u16, luma_samp: u8, dri: u16, scan: &[u8]) -> Vec<u8> {
let mut j = vec![0xFF, markers::SOI];
j.extend_from_slice(&[0xFF, markers::DQT, 0x00, 67, 0x00]);
j.extend((0..64).map(|i| (i as u8) + 1));
j.extend_from_slice(&[0xFF, markers::DQT, 0x00, 67, 0x01]);
j.extend((0..64).map(|i| 64 - i as u8));
j.extend_from_slice(&[0xFF, markers::SOF0, 0x00, 17, 8]);
j.extend_from_slice(&height.to_be_bytes());
j.extend_from_slice(&width.to_be_bytes());
j.push(3);
j.extend_from_slice(&[1, luma_samp, 0]); j.extend_from_slice(&[2, 0x11, 1]); j.extend_from_slice(&[3, 0x11, 1]); if dri != 0 {
j.extend_from_slice(&[0xFF, markers::DRI, 0x00, 0x04]);
j.extend_from_slice(&dri.to_be_bytes());
}
j.extend_from_slice(&[
0xFF,
markers::SOS,
0x00,
12,
3,
1,
0x00,
2,
0x11,
3,
0x11,
0,
63,
0,
]);
j.extend_from_slice(scan);
j.extend_from_slice(&[0xFF, markers::EOI]);
j
}
#[test]
fn packetize_single_fragment_inband() {
let scan = vec![0x11, 0x22, 0x33, 0x44];
let jpeg = handmade_jpeg(64, 64, 0x22, 0, &scan); let pkts = packetize(&jpeg, 1400, QMode::InBand(255)).unwrap();
assert_eq!(pkts.len(), 1);
assert!(pkts[0].marker);
let p = &pkts[0].payload;
let h = parse_main_header(p).unwrap();
assert_eq!(h.fragment_offset, 0);
assert_eq!(h.typ, 1);
assert_eq!(h.q, 255);
assert_eq!(h.width, 64);
assert_eq!(h.height, 64);
assert!(!h.has_restart());
let qh = &p[MAIN_HDR_LEN..];
assert_eq!(u16::from_be_bytes([qh[2], qh[3]]), 128);
assert_eq!(&qh[QTBL_HDR_LEN..QTBL_HDR_LEN + 64][..4], &[1, 2, 3, 4]);
assert_eq!(&p[p.len() - 4..], &scan[..]);
}
#[test]
fn packetize_type0_from_422_sampling() {
let jpeg = handmade_jpeg(64, 64, 0x21, 0, &[0xAB; 8]); let pkts = packetize(&jpeg, 1400, QMode::Quality(50)).unwrap();
assert_eq!(pkts.len(), 1);
let h = parse_main_header(&pkts[0].payload).unwrap();
assert_eq!(h.typ, 0);
assert_eq!(h.q, 50);
assert_eq!(&pkts[0].payload[MAIN_HDR_LEN..], &[0xAB; 8]);
}
#[test]
fn packetize_restart_sets_type_bit_and_header() {
let jpeg = handmade_jpeg(64, 64, 0x22, 7, &[0x5A; 6]);
let pkts = packetize(&jpeg, 1400, QMode::Quality(60)).unwrap();
let p = &pkts[0].payload;
let h = parse_main_header(p).unwrap();
assert_eq!(h.typ, 1 | TYPE_RESTART_BIT); assert!(h.has_restart());
let rh = parse_restart_header(&p[MAIN_HDR_LEN..]).unwrap();
assert_eq!(rh.restart_interval, 7);
assert!(rh.first && rh.last);
assert_eq!(rh.count, 0x3FFF);
}
#[test]
fn packetize_fragments_long_scan() {
let scan: Vec<u8> = (0..100).map(|i| i as u8).collect();
let jpeg = handmade_jpeg(64, 64, 0x22, 0, &scan);
let pkts = packetize(&jpeg, MAIN_HDR_LEN + 10, QMode::Quality(50)).unwrap();
assert_eq!(pkts.len(), 10);
let mut expect_off = 0u32;
for (i, pk) in pkts.iter().enumerate() {
let h = parse_main_header(&pk.payload).unwrap();
assert_eq!(h.fragment_offset, expect_off);
expect_off += (pk.payload.len() - MAIN_HDR_LEN) as u32;
assert_eq!(pk.marker, i == pkts.len() - 1);
}
assert_eq!(expect_off, 100);
}
#[test]
fn packetize_rejects_progressive() {
let mut jpeg = handmade_jpeg(64, 64, 0x22, 0, &[0u8; 4]);
let sof = jpeg
.windows(2)
.position(|w| w[0] == 0xFF && w[1] == markers::SOF0)
.unwrap();
jpeg[sof + 1] = markers::SOF2;
assert!(packetize(&jpeg, 1400, QMode::Quality(50)).is_err());
}
#[test]
fn packetize_rejects_unsupported_qmode_ranges() {
let jpeg = handmade_jpeg(64, 64, 0x22, 0, &[0u8; 4]);
assert!(packetize(&jpeg, 1400, QMode::Quality(0)).is_err());
assert!(packetize(&jpeg, 1400, QMode::Quality(100)).is_err());
assert!(packetize(&jpeg, 1400, QMode::InBand(127)).is_err());
}
#[test]
fn packetize_rejects_oversize_dimensions() {
let jpeg = handmade_jpeg(2048, 64, 0x22, 0, &[0u8; 4]);
assert!(packetize(&jpeg, 1400, QMode::Quality(50)).is_err());
}
#[test]
fn packetize_then_depacketize_roundtrips_scan() {
let scan: Vec<u8> = (0..200).map(|i| (i % 0xF0) as u8).collect();
let jpeg = handmade_jpeg(128, 96, 0x22, 0, &scan);
let pkts = packetize(&jpeg, MAIN_HDR_LEN + 132 + 40, QMode::InBand(200)).unwrap();
assert!(pkts.len() > 1);
let mut dp = JpegDepacketizer::new();
let mut rebuilt = None;
for pk in &pkts {
match dp.push(&pk.payload, pk.marker).unwrap() {
Progress::NeedMore => {}
Progress::Frame(j) => rebuilt = Some(j),
}
}
let rebuilt = rebuilt.expect("frame reassembled");
assert_eq!(&rebuilt[0..2], &[0xFF, markers::SOI]);
assert_eq!(&rebuilt[rebuilt.len() - 2..], &[0xFF, markers::EOI]);
let scan_start = rebuilt.len() - 2 - scan.len();
assert_eq!(&rebuilt[scan_start..rebuilt.len() - 2], &scan[..]);
let sof = rebuilt
.windows(2)
.position(|w| w[0] == 0xFF && w[1] == markers::SOF0)
.unwrap();
assert_eq!(rebuilt[sof + 2 + 2 + 1 + 2 + 2 + 1 + 1], 0x22);
}
#[cfg(feature = "registry")]
#[test]
fn end_to_end_packetize_inband_decodes() {
let (w, h) = (96usize, 64usize);
let frame = make_420_frame(w, h);
let jpeg =
encode_jpeg(&frame, w as u32, h as u32, PixelFormat::Yuv420P, 75).expect("encode");
let pkts = packetize(&jpeg, MAIN_HDR_LEN + 200, QMode::InBand(255)).expect("packetize");
assert!(pkts.len() > 1, "scan should span multiple fragments");
assert!(pkts.last().unwrap().marker);
let mut dp = JpegDepacketizer::new();
let mut rebuilt = None;
for pk in &pkts {
if let Progress::Frame(j) = dp.push(&pk.payload, pk.marker).unwrap() {
rebuilt = Some(j);
}
}
let rebuilt = rebuilt.expect("frame reassembled");
let decoded = decode_jpeg(&rebuilt, None).expect("decode packetized RTP/JPEG");
assert_eq!(decoded.planes.len(), 3);
assert_eq!(decoded.planes[0].data.len(), w * h);
assert_eq!(decoded.planes[1].data.len(), w.div_ceil(2) * h.div_ceil(2));
}
#[cfg(feature = "registry")]
#[test]
fn end_to_end_packetize_q_field_decodes() {
let (w, h) = (64usize, 64usize);
let frame = make_420_frame(w, h);
let jpeg =
encode_jpeg(&frame, w as u32, h as u32, PixelFormat::Yuv420P, 50).expect("encode");
let pkts = packetize(&jpeg, 1400, QMode::Quality(50)).expect("packetize");
let mut dp = JpegDepacketizer::new();
let mut rebuilt = None;
for pk in &pkts {
if let Progress::Frame(j) = dp.push(&pk.payload, pk.marker).unwrap() {
rebuilt = Some(j);
}
}
let decoded = decode_jpeg(&rebuilt.unwrap(), None).expect("decode");
assert_eq!(decoded.planes.len(), 3);
assert_eq!(decoded.planes[0].data.len(), w * h);
}
fn handmade_jpeg_with_intervals(
width: u16,
height: u16,
luma_samp: u8,
dri: u16,
intervals: usize,
bytes_per_interval: usize,
seed: u8,
) -> Vec<u8> {
let mut scan = Vec::new();
for i in 0..intervals {
for k in 0..bytes_per_interval {
scan.push(seed.wrapping_add(((i * bytes_per_interval) + k) as u8) & 0xEF);
}
if i + 1 < intervals {
scan.push(0xFF);
scan.push(markers::RST0 + ((i as u8) & 0x07));
}
}
handmade_jpeg(width, height, luma_samp, dri, &scan)
}
fn rst_positions(jpeg: &[u8]) -> Vec<usize> {
let mut out = Vec::new();
let mut i = 0;
while i + 1 < jpeg.len() {
if jpeg[i] == 0xFF && markers::is_rst(jpeg[i + 1]) {
out.push(i);
i += 2;
} else {
i += 1;
}
}
out
}
#[test]
fn scan_restart_intervals_walks_three_whole_intervals() {
let mut scan = Vec::new();
for i in 0..3 {
scan.extend((0..10).map(|k| 0x10 + i * 16 + k));
if i < 2 {
scan.push(0xFF);
scan.push(markers::RST0 + i);
}
}
let ivs = scan_restart_intervals(&scan);
assert_eq!(ivs.len(), 3);
assert_eq!(ivs[0], ScanInterval { start: 0, end: 12 });
assert_eq!(ivs[1], ScanInterval { start: 12, end: 24 });
assert_eq!(ivs[2], ScanInterval { start: 24, end: 34 });
}
#[test]
fn scan_restart_intervals_tolerates_stuffed_ff_inside_an_interval() {
let scan: Vec<u8> = vec![0x12, 0xFF, 0x00, 0x34, 0xFF, markers::RST0, 0x56];
let ivs = scan_restart_intervals(&scan);
assert_eq!(ivs.len(), 2);
assert_eq!(ivs[0].start, 0);
assert_eq!(ivs[0].end, 6); assert_eq!(ivs[1].start, 6);
assert_eq!(ivs[1].end, 7);
}
#[test]
fn restart_align_one_interval_per_packet_when_mtu_is_tight() {
let jpeg = handmade_jpeg_with_intervals(128, 128, 0x22, 4, 4, 12, 0x10);
let opts = PacketizeOpts::new(QMode::Quality(50)).with_restart_align(true);
let pkts = packetize_with_opts(&jpeg, MAIN_HDR_LEN + RST_HDR_LEN + 14, opts).unwrap();
assert_eq!(pkts.len(), 4);
for (i, pk) in pkts.iter().enumerate() {
let h = parse_main_header(&pk.payload).unwrap();
assert!(h.has_restart());
let rh = parse_restart_header(&pk.payload[MAIN_HDR_LEN..]).unwrap();
assert_eq!(rh.restart_interval, 4);
assert!(rh.first, "F bit set on whole-interval fragment");
assert!(rh.last, "L bit set on whole-interval fragment");
assert_eq!(rh.count, i as u16);
assert_eq!(pk.marker, i == pkts.len() - 1);
}
}
#[test]
fn restart_align_packs_multiple_intervals_per_packet_when_mtu_allows() {
let jpeg = handmade_jpeg_with_intervals(128, 128, 0x22, 7, 4, 8, 0x20);
let opts = PacketizeOpts::new(QMode::Quality(50)).with_restart_align(true);
let pkts = packetize_with_opts(&jpeg, MAIN_HDR_LEN + RST_HDR_LEN + 22, opts).unwrap();
assert_eq!(pkts.len(), 2);
let rh0 = parse_restart_header(&pkts[0].payload[MAIN_HDR_LEN..]).unwrap();
assert_eq!(rh0.count, 0);
assert!(rh0.first && rh0.last);
let rh1 = parse_restart_header(&pkts[1].payload[MAIN_HDR_LEN..]).unwrap();
assert_eq!(rh1.count, 2);
assert!(rh1.first && rh1.last);
assert!(pkts[1].marker);
}
#[test]
fn restart_align_is_noop_when_source_has_no_dri() {
let scan: Vec<u8> = (0..200).map(|i| (i & 0xEF) as u8).collect();
let jpeg = handmade_jpeg(128, 96, 0x22, 0, &scan);
let baseline = packetize(&jpeg, MAIN_HDR_LEN + 40, QMode::Quality(50)).unwrap();
let aligned = packetize_with_opts(
&jpeg,
MAIN_HDR_LEN + 40,
PacketizeOpts::new(QMode::Quality(50)).with_restart_align(true),
)
.unwrap();
assert_eq!(baseline, aligned);
}
#[test]
fn restart_align_rejects_oversize_interval() {
let jpeg = handmade_jpeg_with_intervals(64, 64, 0x22, 4, 1, 200, 0x10);
let err = packetize_with_opts(
&jpeg,
MAIN_HDR_LEN + RST_HDR_LEN + 50,
PacketizeOpts::new(QMode::Quality(50)).with_restart_align(true),
)
.unwrap_err();
assert!(format!("{err}").to_lowercase().contains("interval"));
}
#[test]
fn restart_align_first_fragment_carries_qtable_header() {
let jpeg = handmade_jpeg_with_intervals(64, 64, 0x22, 5, 3, 6, 0x10);
let pkts = packetize_with_opts(
&jpeg,
MAIN_HDR_LEN + RST_HDR_LEN + 128 + 32,
PacketizeOpts::new(QMode::InBand(200)).with_restart_align(true),
)
.unwrap();
let p0 = &pkts[0].payload;
let qhdr_start = MAIN_HDR_LEN + RST_HDR_LEN;
let length = u16::from_be_bytes([p0[qhdr_start + 2], p0[qhdr_start + 3]]);
assert_eq!(length, 128);
for pk in &pkts[1..] {
let h = parse_main_header(&pk.payload).unwrap();
assert!(h.fragment_offset > 0);
}
}
#[test]
fn restart_align_then_depacketize_reassembles_scan_bytes() {
let jpeg = handmade_jpeg_with_intervals(128, 96, 0x22, 6, 5, 10, 0x40);
let pkts = packetize_with_opts(
&jpeg,
MAIN_HDR_LEN + RST_HDR_LEN + 30,
PacketizeOpts::new(QMode::Quality(50)).with_restart_align(true),
)
.unwrap();
let mut dp = JpegDepacketizer::new();
let mut rebuilt = None;
for pk in &pkts {
if let Progress::Frame(j) = dp.push(&pk.payload, pk.marker).unwrap() {
rebuilt = Some(j);
}
}
let rebuilt = rebuilt.expect("frame reassembled");
let src_rsts = rst_positions(&jpeg);
let dst_rsts = rst_positions(&rebuilt);
assert_eq!(dst_rsts.len(), src_rsts.len());
}
fn jpeg_with_truncated_sof(sof_len: u16) -> Vec<u8> {
let mut j = vec![0xFF, markers::SOI, 0xFF, markers::SOF0];
j.extend_from_slice(&sof_len.to_be_bytes());
j.push(0xFF);
j.push(markers::EOI);
j
}
#[test]
fn packetize_rejects_sof_with_underflowing_length() {
let j = jpeg_with_truncated_sof(0);
let err = packetize(&j, 1400, QMode::Quality(50)).unwrap_err();
assert!(format!("{err}").contains("truncated SOF"));
}
#[test]
fn packetize_rejects_sof_with_undersized_component_records() {
let mut j = vec![0xFF, markers::SOI, 0xFF, markers::SOF0];
j.extend_from_slice(&12u16.to_be_bytes());
j.push(8); j.extend_from_slice(&16u16.to_be_bytes()); j.extend_from_slice(&16u16.to_be_bytes()); j.push(3); j.extend_from_slice(&[0, 0, 0, 0]);
j.push(0xFF);
j.push(markers::EOI);
let err = packetize(&j, 1400, QMode::Quality(50)).unwrap_err();
assert!(format!("{err}").contains("truncated SOF components"));
}
#[test]
fn packetize_rejects_dqt_with_underflowing_length() {
let mut j = vec![0xFF, markers::SOI, 0xFF, markers::DQT];
j.extend_from_slice(&0u16.to_be_bytes()); j.push(0xFF);
j.push(markers::EOI);
let err = packetize(&j, 1400, QMode::Quality(50)).unwrap_err();
assert!(format!("{err}").contains("truncated DQT"));
}
#[test]
fn packetize_rejects_sos_with_underflowing_length() {
let mut j = vec![0xFF, markers::SOI];
j.push(0xFF);
j.push(markers::SOF0);
j.extend_from_slice(&17u16.to_be_bytes());
j.push(8); j.extend_from_slice(&16u16.to_be_bytes());
j.extend_from_slice(&16u16.to_be_bytes());
j.push(3);
j.extend_from_slice(&[1, 0x22, 0, 2, 0x11, 1, 3, 0x11, 1]);
j.push(0xFF);
j.push(markers::SOS);
j.extend_from_slice(&1u16.to_be_bytes());
j.push(0xFF);
j.push(markers::EOI);
let err = packetize(&j, 1400, QMode::Quality(50)).unwrap_err();
assert!(format!("{err}").contains("truncated SOS"));
}
#[test]
fn packetize_rejects_generic_segment_with_underflowing_length() {
let mut j = vec![0xFF, markers::SOI];
j.push(0xFF);
j.push(0xE0); j.extend_from_slice(&1u16.to_be_bytes()); j.push(0xFF);
j.push(markers::EOI);
let err = packetize(&j, 1400, QMode::Quality(50)).unwrap_err();
assert!(format!("{err}").contains("truncated/oversized segment"));
}
}