use oxideav_core::bits::BitReader;
use crate::gob::parse_gob_header;
use crate::mb::{decode_mba_diff, reconstruct_mv, MbContext};
use crate::picture::parse_picture_header;
use crate::start_code::{find_next_start_code_bits, iter_start_codes, GN_PICTURE};
use crate::tables::{
decode_tcoeff, decode_vlc, MtypeInfo, MvdSym, Prediction, TcoeffSym, CBP_TABLE, MTYPE_TABLE,
MVD_TABLE,
};
pub const HEADER_LEN: usize = 4;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct H261RtpHeader {
pub sbit: u8,
pub ebit: u8,
pub intra_only: bool,
pub motion_vectors: bool,
pub gobn: u8,
pub mbap: u8,
pub quant: u8,
pub hmvd: i8,
pub vmvd: i8,
}
impl H261RtpHeader {
pub fn gob_aligned(ebit: u8, intra_only: bool, motion_vectors: bool) -> Self {
Self {
sbit: 0,
ebit,
intra_only,
motion_vectors,
gobn: 0,
mbap: 0,
quant: 0,
hmvd: 0,
vmvd: 0,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RtpError {
ShortHeader,
BadBitOffset { field: &'static str, value: u8 },
ForbiddenMvd { field: &'static str },
FieldOverflow { field: &'static str, value: u32 },
EmptyPayload,
NoStartCodes,
MalformedStream {
detail: String,
},
FragmentTooLarge {
needed: usize,
max: usize,
},
}
impl core::fmt::Display for RtpError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
RtpError::ShortHeader => write!(f, "h261 RTP header < 4 bytes"),
RtpError::BadBitOffset { field, value } => {
write!(f, "h261 RTP: {field}={value} out of 0..=7")
}
RtpError::ForbiddenMvd { field } => {
write!(
f,
"h261 RTP: {field} encodes the forbidden -16 (RFC 4587 §4.1)"
)
}
RtpError::FieldOverflow { field, value } => {
write!(f, "h261 RTP: {field}={value} exceeds field width")
}
RtpError::EmptyPayload => write!(f, "h261 RTP: empty payload"),
RtpError::NoStartCodes => {
write!(f, "h261 RTP: depacketized stream contains no start codes")
}
RtpError::MalformedStream { detail } => {
write!(f, "h261 RTP: MB fragmenter VLC parse failed: {detail}")
}
RtpError::FragmentTooLarge { needed, max } => {
write!(
f,
"h261 RTP: smallest legal fragment needs {needed} bytes but the payload budget is {max}"
)
}
}
}
}
impl std::error::Error for RtpError {}
pub fn pack_header(h: &H261RtpHeader) -> Result<[u8; HEADER_LEN], RtpError> {
if h.sbit > 7 {
return Err(RtpError::BadBitOffset {
field: "SBIT",
value: h.sbit,
});
}
if h.ebit > 7 {
return Err(RtpError::BadBitOffset {
field: "EBIT",
value: h.ebit,
});
}
if h.gobn > 15 {
return Err(RtpError::FieldOverflow {
field: "GOBN",
value: h.gobn as u32,
});
}
if h.mbap > 31 {
return Err(RtpError::FieldOverflow {
field: "MBAP",
value: h.mbap as u32,
});
}
if h.quant > 31 {
return Err(RtpError::FieldOverflow {
field: "QUANT",
value: h.quant as u32,
});
}
if h.hmvd < -15 || h.hmvd > 15 {
return Err(RtpError::ForbiddenMvd { field: "HMVD" });
}
if h.vmvd < -15 || h.vmvd > 15 {
return Err(RtpError::ForbiddenMvd { field: "VMVD" });
}
let hmvd5 = (h.hmvd as i32 & 0x1F) as u32;
let vmvd5 = (h.vmvd as i32 & 0x1F) as u32;
let word: u32 = ((h.sbit as u32) << 29)
| ((h.ebit as u32) << 26)
| ((h.intra_only as u32) << 25)
| ((h.motion_vectors as u32) << 24)
| ((h.gobn as u32) << 20)
| ((h.mbap as u32) << 15)
| ((h.quant as u32) << 10)
| (hmvd5 << 5)
| vmvd5;
Ok(word.to_be_bytes())
}
pub fn unpack_header(buf: &[u8]) -> Result<(H261RtpHeader, &[u8]), RtpError> {
if buf.len() < HEADER_LEN {
return Err(RtpError::ShortHeader);
}
let word = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
let sbit = ((word >> 29) & 0x7) as u8;
let ebit = ((word >> 26) & 0x7) as u8;
let intra_only = ((word >> 25) & 0x1) != 0;
let motion_vectors = ((word >> 24) & 0x1) != 0;
let gobn = ((word >> 20) & 0xF) as u8;
let mbap = ((word >> 15) & 0x1F) as u8;
let quant = ((word >> 10) & 0x1F) as u8;
let hmvd5 = ((word >> 5) & 0x1F) as u8;
let vmvd5 = (word & 0x1F) as u8;
let sign_extend5 = |v: u8| -> i8 {
if v & 0x10 != 0 {
(v as i8) | !0x1F
} else {
v as i8
}
};
let hmvd = sign_extend5(hmvd5);
let vmvd = sign_extend5(vmvd5);
Ok((
H261RtpHeader {
sbit,
ebit,
intra_only,
motion_vectors,
gobn,
mbap,
quant,
hmvd,
vmvd,
},
&buf[HEADER_LEN..],
))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct H261RtpPayload {
pub header: H261RtpHeader,
pub bytes: Vec<u8>,
pub marker: bool,
}
impl H261RtpPayload {
pub fn data_len(&self) -> usize {
self.bytes.len().saturating_sub(HEADER_LEN)
}
}
pub fn packetize_gob_aligned(
data: &[u8],
max_payload: usize,
intra_only: bool,
motion_vectors: bool,
) -> Vec<H261RtpPayload> {
assert!(
max_payload > HEADER_LEN,
"max_payload must accommodate the 4-byte H.261 RTP header + at least 1 data byte"
);
let mut payloads = Vec::new();
let starts: Vec<usize> = iter_start_codes(data)
.filter_map(|sc| {
if sc.bit_pos % 8 == 0 {
Some(sc.byte_pos)
} else {
None
}
})
.collect();
if starts.is_empty() {
return payloads;
}
let psc_byte_positions: Vec<usize> = iter_start_codes(data)
.filter(|sc| sc.bit_pos % 8 == 0 && sc.gn == GN_PICTURE)
.map(|sc| sc.byte_pos)
.collect();
let max_data = max_payload - HEADER_LEN;
let total = data.len();
for (idx, &begin) in starts.iter().enumerate() {
let end = starts.get(idx + 1).copied().unwrap_or(total);
let mut p = begin;
let mut first_chunk_of_gob = true;
while p < end {
let chunk_len = (end - p).min(max_data);
let chunk_end = p + chunk_len;
let mut hdr = if first_chunk_of_gob {
H261RtpHeader::gob_aligned(0, intra_only, motion_vectors)
} else {
H261RtpHeader::gob_aligned(0, intra_only, motion_vectors)
};
if chunk_end == total {
hdr.ebit = 0;
}
let next_psc_after = psc_byte_positions.iter().copied().find(|&pos| pos > p);
let marker = match next_psc_after {
Some(np) => chunk_end == np || (idx + 1 == starts.len() && chunk_end == total),
None => chunk_end == total,
};
let mut bytes = Vec::with_capacity(HEADER_LEN + chunk_len);
bytes.extend_from_slice(&pack_header(&hdr).expect("hdr packs"));
bytes.extend_from_slice(&data[p..chunk_end]);
payloads.push(H261RtpPayload {
header: hdr,
bytes,
marker,
});
p = chunk_end;
first_chunk_of_gob = false;
}
}
payloads
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct FragCtx {
pub(crate) gobn: u8,
pub(crate) last_mba: u8,
pub(crate) quant: u8,
pub(crate) mv: (i32, i32),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct SplitPoint {
pub(crate) bit: u64,
pub(crate) is_psc: bool,
pub(crate) ctx: Option<FragCtx>,
}
fn walk_err(e: oxideav_core::Error) -> RtpError {
RtpError::MalformedStream {
detail: e.to_string(),
}
}
fn skip_ac_coeffs(
br: &mut BitReader<'_>,
start: usize,
is_first_inter: bool,
) -> oxideav_core::Result<()> {
let mut idx = start;
let mut first = is_first_inter;
loop {
let sym = decode_tcoeff(br, first)?;
first = false;
match sym {
TcoeffSym::Eob => return Ok(()),
TcoeffSym::RunLevel { run, .. } => {
let _sign = br.read_u1()?;
idx = idx.saturating_add(run as usize);
if idx > 63 {
return Err(oxideav_core::Error::invalid(format!(
"h261 block: AC run overflow (idx={idx}, run={run})"
)));
}
idx += 1;
}
TcoeffSym::Escape => {
let run = br.read_u32(6)? as u8;
let raw = br.read_u32(8)?;
if raw == 0 || raw == 0x80 {
return Err(oxideav_core::Error::invalid(
"h261 escape: forbidden level FLC",
));
}
idx = idx.saturating_add(run as usize);
if idx > 63 {
return Err(oxideav_core::Error::invalid(format!(
"h261 escape: run overflow (idx={idx}, run={run})"
)));
}
idx += 1;
}
}
}
}
fn skip_macroblock(
br: &mut BitReader<'_>,
mba: u8,
quant: &mut u32,
ctx: &mut MbContext,
) -> oxideav_core::Result<()> {
let mtype: MtypeInfo = decode_vlc(br, MTYPE_TABLE)?;
if mtype.mquant {
let q = br.read_u32(5)?;
if q == 0 {
return Err(oxideav_core::Error::invalid("h261 MB: MQUANT == 0"));
}
*quant = q;
}
let mut mv = (0i32, 0i32);
if mtype.mvd {
let pred = crate::mb::mvd_predictor(mba, ctx.prev_mba, ctx.prev_was_mc, ctx.mv);
let sym_x: MvdSym = decode_vlc(br, MVD_TABLE)?;
let sym_y: MvdSym = decode_vlc(br, MVD_TABLE)?;
mv = (reconstruct_mv(pred.0, sym_x), reconstruct_mv(pred.1, sym_y));
}
let cbp: u8 = if mtype.cbp {
decode_vlc(br, CBP_TABLE)?
} else if mtype.prediction == Prediction::Intra {
0b111111
} else {
0
};
if mtype.prediction == Prediction::Intra {
for _ in 0..6 {
let dc = br.read_u32(8)?;
if dc == 0x00 || dc == 0x80 {
return Err(oxideav_core::Error::invalid(
"h261 INTRA DC: forbidden bitstream value",
));
}
skip_ac_coeffs(br, 1, false)?;
}
ctx.mv = (0, 0);
ctx.prev_was_mc = false;
ctx.prev_mba = mba;
return Ok(());
}
let block_coded = [
(cbp >> 5) & 1 != 0,
(cbp >> 4) & 1 != 0,
(cbp >> 3) & 1 != 0,
(cbp >> 2) & 1 != 0,
(cbp >> 1) & 1 != 0,
cbp & 1 != 0,
];
for coded in block_coded {
if coded && mtype.tcoeff {
skip_ac_coeffs(br, 0, true)?;
}
}
ctx.mv = mv;
ctx.prev_was_mc = matches!(
mtype.prediction,
Prediction::InterMc | Prediction::InterMcFil
);
ctx.prev_mba = mba;
Ok(())
}
pub(crate) fn walk_mb_split_points(data: &[u8]) -> Result<Vec<SplitPoint>, RtpError> {
let mut br = BitReader::new(data);
let mut points = Vec::new();
loop {
let pos = br.bit_position();
let Some(sc) = find_next_start_code_bits(data, pos) else {
break;
};
if sc.bit_pos > pos {
br.skip((sc.bit_pos - pos) as u32).map_err(walk_err)?;
}
if sc.gn == GN_PICTURE {
points.push(SplitPoint {
bit: sc.bit_pos,
is_psc: true,
ctx: None,
});
parse_picture_header(&mut br).map_err(walk_err)?;
continue;
}
points.push(SplitPoint {
bit: sc.bit_pos,
is_psc: false,
ctx: None,
});
let gob_hdr = parse_gob_header(&mut br).map_err(walk_err)?;
let mut quant = gob_hdr.gquant as u32;
let mut ctx = MbContext::reset();
let mut current_mba: i32 = 0;
loop {
let remaining = br.bits_remaining();
if remaining == 0 {
break;
}
if remaining >= 16 && br.peek_u32(16).map_err(walk_err)? == 0x0001 {
break;
}
let diff = match decode_mba_diff(&mut br).map_err(walk_err)? {
Some(d) => d as i32,
None => break,
};
let new_mba = current_mba + diff;
if !(1..=33).contains(&new_mba) {
return Err(RtpError::MalformedStream {
detail: format!(
"MBA out of range {new_mba} (GN={}, prev_mba={current_mba})",
gob_hdr.gn
),
});
}
current_mba = new_mba;
skip_macroblock(&mut br, new_mba as u8, &mut quant, &mut ctx).map_err(walk_err)?;
if new_mba <= 32 {
points.push(SplitPoint {
bit: br.bit_position(),
is_psc: false,
ctx: Some(FragCtx {
gobn: gob_hdr.gn,
last_mba: new_mba as u8,
quant: quant as u8,
mv: if ctx.prev_was_mc { ctx.mv } else { (0, 0) },
}),
});
}
}
}
Ok(points)
}
pub fn packetize_mb_fragmented(
data: &[u8],
max_payload: usize,
intra_only: bool,
motion_vectors: bool,
) -> Result<Vec<H261RtpPayload>, RtpError> {
assert!(
max_payload > HEADER_LEN,
"max_payload must accommodate the 4-byte H.261 RTP header + at least 1 data byte"
);
let mut payloads = Vec::new();
let points = walk_mb_split_points(data)?;
if points.is_empty() {
return Ok(payloads);
}
let max_data = max_payload - HEADER_LEN;
let total_bits = (data.len() as u64) * 8;
let mut s = points[0].bit;
let mut s_ctx: Option<FragCtx> = None;
let mut search_from = 0usize;
loop {
let start_byte = (s / 8) as usize;
while search_from < points.len() && points[search_from].bit <= s {
search_from += 1;
}
let frame_end = points[search_from..]
.iter()
.find(|p| p.is_psc)
.map(|p| p.bit)
.unwrap_or(total_bits);
let mut chosen: Option<(u64, Option<FragCtx>)> = None;
let mut first_span: Option<usize> = None;
for p in points[search_from..]
.iter()
.take_while(|p| p.bit <= frame_end)
{
let span = (p.bit.div_ceil(8) as usize) - start_byte;
if first_span.is_none() {
first_span = Some(span);
}
if span > max_data {
break;
}
chosen = Some((p.bit, p.ctx));
}
if frame_end == total_bits {
let span = data.len() - start_byte;
if first_span.is_none() {
first_span = Some(span);
}
if span <= max_data {
chosen = Some((total_bits, None));
}
}
let Some((frag_end_bit, next_ctx)) = chosen else {
return Err(RtpError::FragmentTooLarge {
needed: first_span.unwrap_or(data.len() - start_byte),
max: max_data,
});
};
let hdr = H261RtpHeader {
sbit: (s % 8) as u8,
ebit: ((8 - (frag_end_bit % 8)) % 8) as u8,
intra_only,
motion_vectors,
gobn: s_ctx.map_or(0, |c| c.gobn),
mbap: s_ctx.map_or(0, |c| c.last_mba - 1),
quant: s_ctx.map_or(0, |c| c.quant),
hmvd: s_ctx.map_or(0, |c| c.mv.0 as i8),
vmvd: s_ctx.map_or(0, |c| c.mv.1 as i8),
};
let end_byte = frag_end_bit.div_ceil(8) as usize;
let mut bytes = Vec::with_capacity(HEADER_LEN + (end_byte - start_byte));
bytes.extend_from_slice(&pack_header(&hdr).expect("hdr packs"));
bytes.extend_from_slice(&data[start_byte..end_byte]);
payloads.push(H261RtpPayload {
header: hdr,
bytes,
marker: frag_end_bit == frame_end,
});
if frag_end_bit >= total_bits {
break;
}
s = frag_end_bit;
s_ctx = next_ctx;
}
Ok(payloads)
}
pub fn depacketize(payloads: &[H261RtpPayload]) -> Result<Vec<u8>, RtpError> {
let mut out_bits = BitBuf::new();
for p in payloads {
if p.bytes.len() < HEADER_LEN {
return Err(RtpError::ShortHeader);
}
let (hdr, data) = unpack_header(&p.bytes)?;
if data.is_empty() {
continue;
}
let total_bits = data.len() * 8;
if (hdr.sbit as usize) + (hdr.ebit as usize) >= total_bits {
return Err(RtpError::EmptyPayload);
}
let usable_bits = total_bits - hdr.sbit as usize - hdr.ebit as usize;
out_bits.append_msb_bits(data, hdr.sbit as usize, usable_bits);
}
let bytes = out_bits.finish();
if iter_start_codes(&bytes).next().is_none() {
return Err(RtpError::NoStartCodes);
}
Ok(bytes)
}
pub const RTP_FIXED_HEADER_LEN: usize = 12;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RtpPacket {
pub bytes: Vec<u8>,
pub marker: bool,
pub sequence_number: u16,
pub timestamp: u32,
pub ssrc: u32,
}
impl RtpPacket {
pub fn data_len(&self) -> usize {
self.bytes
.len()
.saturating_sub(RTP_FIXED_HEADER_LEN + HEADER_LEN)
}
}
fn write_rtp_fixed_header(buf: &mut Vec<u8>, pt: u8, marker: bool, seq: u16, ts: u32, ssrc: u32) {
buf.push(0x80);
buf.push(((marker as u8) << 7) | (pt & 0x7F));
buf.extend_from_slice(&seq.to_be_bytes());
buf.extend_from_slice(&ts.to_be_bytes());
buf.extend_from_slice(&ssrc.to_be_bytes());
}
#[derive(Debug, Clone)]
pub struct RtpPacketizer {
pt: u8,
ssrc: u32,
next_seq: u16,
max_payload: usize,
intra_only: bool,
motion_vectors: bool,
mb_fragmentation: bool,
packet_count: u32,
octet_count: u32,
last_rtp_timestamp: Option<u32>,
}
impl RtpPacketizer {
pub fn new(
payload_type: u8,
ssrc: u32,
initial_sequence_number: u16,
max_rtp_packet_size: usize,
) -> Self {
assert!(
max_rtp_packet_size > RTP_FIXED_HEADER_LEN + HEADER_LEN,
"max_rtp_packet_size must accommodate the 12-byte RTP fixed header + 4-byte H.261 header + ≥ 1 data byte"
);
Self {
pt: payload_type & 0x7F,
ssrc,
next_seq: initial_sequence_number,
max_payload: max_rtp_packet_size,
intra_only: false,
motion_vectors: true,
mb_fragmentation: false,
packet_count: 0,
octet_count: 0,
last_rtp_timestamp: None,
}
}
pub fn with_intra_only(mut self, intra_only: bool) -> Self {
self.intra_only = intra_only;
self
}
pub fn with_motion_vectors(mut self, motion_vectors: bool) -> Self {
self.motion_vectors = motion_vectors;
self
}
pub fn with_mb_fragmentation(mut self, mb_fragmentation: bool) -> Self {
self.mb_fragmentation = mb_fragmentation;
self
}
pub fn next_sequence_number(&self) -> u16 {
self.next_seq
}
pub fn ssrc(&self) -> u32 {
self.ssrc
}
pub fn payload_type(&self) -> u8 {
self.pt
}
pub fn pack_frame(&mut self, frame_bytes: &[u8], rtp_timestamp_90khz: u32) -> Vec<RtpPacket> {
let inner_budget = self.max_payload - RTP_FIXED_HEADER_LEN;
let h261_payloads = if self.mb_fragmentation {
packetize_mb_fragmented(
frame_bytes,
inner_budget,
self.intra_only,
self.motion_vectors,
)
.unwrap_or_else(|_| {
packetize_gob_aligned(
frame_bytes,
inner_budget,
self.intra_only,
self.motion_vectors,
)
})
} else {
packetize_gob_aligned(
frame_bytes,
inner_budget,
self.intra_only,
self.motion_vectors,
)
};
if h261_payloads.is_empty() {
return Vec::new();
}
let last_idx = h261_payloads.len() - 1;
let mut out = Vec::with_capacity(h261_payloads.len());
for (i, p) in h261_payloads.into_iter().enumerate() {
let marker = i == last_idx;
let seq = self.next_seq;
self.next_seq = self.next_seq.wrapping_add(1);
let mut bytes = Vec::with_capacity(RTP_FIXED_HEADER_LEN + p.bytes.len());
write_rtp_fixed_header(
&mut bytes,
self.pt,
marker,
seq,
rtp_timestamp_90khz,
self.ssrc,
);
bytes.extend_from_slice(&p.bytes);
self.packet_count = self.packet_count.wrapping_add(1);
self.octet_count = self.octet_count.wrapping_add(p.bytes.len() as u32);
out.push(RtpPacket {
bytes,
marker,
sequence_number: seq,
timestamp: rtp_timestamp_90khz,
ssrc: self.ssrc,
});
}
if !out.is_empty() {
self.last_rtp_timestamp = Some(rtp_timestamp_90khz);
}
out
}
pub fn packet_count(&self) -> u32 {
self.packet_count
}
pub fn octet_count(&self) -> u32 {
self.octet_count
}
pub fn sender_info(&self, ntp_timestamp: u64) -> crate::rtcp::SenderInfo {
crate::rtcp::SenderInfo {
ntp_timestamp,
rtp_timestamp: self.last_rtp_timestamp.unwrap_or(0),
packet_count: self.packet_count,
octet_count: self.octet_count,
}
}
pub fn sender_report(
&self,
ntp_timestamp: u64,
blocks: &[crate::rtcp::ReceptionReportBlock],
) -> Result<Vec<u8>, crate::rtcp::RtcpError> {
crate::rtcp::build_sender_report(self.ssrc, &self.sender_info(ntp_timestamp), blocks)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RtpFixedHeader {
pub version: u8,
pub padding: bool,
pub extension: bool,
pub csrc_count: u8,
pub marker: bool,
pub payload_type: u8,
pub sequence_number: u16,
pub timestamp: u32,
pub ssrc: u32,
}
pub fn parse_rtp_fixed_header(buf: &[u8]) -> Result<(RtpFixedHeader, &[u8]), RtpError> {
if buf.len() < RTP_FIXED_HEADER_LEN {
return Err(RtpError::ShortHeader);
}
let b0 = buf[0];
let b1 = buf[1];
let version = (b0 >> 6) & 0x3;
if version != 2 {
return Err(RtpError::FieldOverflow {
field: "RTP-V",
value: version as u32,
});
}
let padding = (b0 & 0x20) != 0;
let extension = (b0 & 0x10) != 0;
let csrc_count = b0 & 0x0F;
let marker = (b1 & 0x80) != 0;
let payload_type = b1 & 0x7F;
let sequence_number = u16::from_be_bytes([buf[2], buf[3]]);
let timestamp = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
let ssrc = u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]);
let csrc_bytes = (csrc_count as usize) * 4;
let need = RTP_FIXED_HEADER_LEN + csrc_bytes;
if buf.len() < need {
return Err(RtpError::ShortHeader);
}
Ok((
RtpFixedHeader {
version,
padding,
extension,
csrc_count,
marker,
payload_type,
sequence_number,
timestamp,
ssrc,
},
&buf[need..],
))
}
struct BitBuf {
bytes: Vec<u8>,
bit_len: usize,
}
impl BitBuf {
fn new() -> Self {
Self {
bytes: Vec::new(),
bit_len: 0,
}
}
fn append_msb_bits(&mut self, src: &[u8], src_skip_bits: usize, count: usize) {
if src_skip_bits == 0 && self.bit_len % 8 == 0 && count == src.len() * 8 {
self.bytes.extend_from_slice(src);
self.bit_len += count;
return;
}
for i in 0..count {
let bit_index = src_skip_bits + i;
let byte = src[bit_index / 8];
let shift = 7 - (bit_index % 8);
let bit = (byte >> shift) & 1;
self.push_bit(bit);
}
}
fn push_bit(&mut self, bit: u8) {
let byte_index = self.bit_len / 8;
let bit_index_in_byte = 7 - (self.bit_len % 8);
if byte_index == self.bytes.len() {
self.bytes.push(0);
}
self.bytes[byte_index] |= (bit & 1) << bit_index_in_byte;
self.bit_len += 1;
}
fn finish(self) -> Vec<u8> {
self.bytes
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pack_then_unpack_round_trip_zero_header() {
let h = H261RtpHeader::gob_aligned(0, false, false);
let bytes = pack_header(&h).unwrap();
assert_eq!(bytes, [0, 0, 0, 0]);
let (h2, rest) = unpack_header(&bytes).unwrap();
assert_eq!(h, h2);
assert!(rest.is_empty());
}
#[test]
fn pack_then_unpack_round_trip_typical_values() {
let h = H261RtpHeader {
sbit: 3,
ebit: 5,
intra_only: true,
motion_vectors: true,
gobn: 7,
mbap: 12,
quant: 17,
hmvd: -7,
vmvd: 11,
};
let bytes = pack_header(&h).unwrap();
assert_eq!((bytes[0] >> 5) & 0x7, 3);
let mut buf = vec![0u8; 4];
buf.copy_from_slice(&bytes);
buf.push(0xAB); let (h2, rest) = unpack_header(&buf).unwrap();
assert_eq!(h, h2);
assert_eq!(rest, &[0xAB]);
}
#[test]
fn pack_then_unpack_round_trip_negative_mvds() {
for hmvd in -15i8..=15 {
for vmvd in -15i8..=15 {
let h = H261RtpHeader {
sbit: 0,
ebit: 0,
intra_only: false,
motion_vectors: true,
gobn: 0,
mbap: 0,
quant: 0,
hmvd,
vmvd,
};
let bytes = pack_header(&h).unwrap();
let (h2, _) = unpack_header(&bytes).unwrap();
assert_eq!(h, h2, "round-trip failed for ({hmvd},{vmvd})");
}
}
}
#[test]
fn pack_rejects_forbidden_mvd_value() {
let mut h = H261RtpHeader::gob_aligned(0, false, true);
h.hmvd = -16;
assert_eq!(
pack_header(&h),
Err(RtpError::ForbiddenMvd { field: "HMVD" })
);
let mut h2 = H261RtpHeader::gob_aligned(0, false, true);
h2.vmvd = -16;
assert_eq!(
pack_header(&h2),
Err(RtpError::ForbiddenMvd { field: "VMVD" })
);
}
#[test]
fn pack_rejects_oversized_sbit_ebit() {
let mut h = H261RtpHeader::gob_aligned(0, false, false);
h.sbit = 8;
assert_eq!(
pack_header(&h),
Err(RtpError::BadBitOffset {
field: "SBIT",
value: 8
})
);
let mut h2 = H261RtpHeader::gob_aligned(0, false, false);
h2.ebit = 9;
assert_eq!(
pack_header(&h2),
Err(RtpError::BadBitOffset {
field: "EBIT",
value: 9
})
);
}
#[test]
fn pack_rejects_oversized_quant() {
let mut h = H261RtpHeader::gob_aligned(0, false, false);
h.quant = 32;
assert_eq!(
pack_header(&h),
Err(RtpError::FieldOverflow {
field: "QUANT",
value: 32
})
);
}
#[test]
fn unpack_rejects_short_header() {
for n in 0..HEADER_LEN {
let buf = vec![0u8; n];
assert_eq!(unpack_header(&buf), Err(RtpError::ShortHeader));
}
}
#[test]
fn unpack_field_widths_match_rfc4587_layout() {
let word: u32 = (7u32 << 29)
| (6 << 26)
| (1 << 25)
| (10 << 20)
| (24 << 15)
| (3 << 10)
| (15 << 5)
| 17;
let bytes = word.to_be_bytes();
let (h, rest) = unpack_header(&bytes).unwrap();
assert_eq!(h.sbit, 7);
assert_eq!(h.ebit, 6);
assert!(h.intra_only);
assert!(!h.motion_vectors);
assert_eq!(h.gobn, 10);
assert_eq!(h.mbap, 24);
assert_eq!(h.quant, 3);
assert_eq!(h.hmvd, 15);
assert_eq!(h.vmvd, -15);
assert!(rest.is_empty());
}
#[test]
fn packetize_returns_empty_for_input_with_no_start_codes() {
let data = vec![0xFFu8; 100];
let pkts = packetize_gob_aligned(&data, 256, true, false);
assert!(pkts.is_empty());
}
#[test]
fn packetize_splits_at_gob_boundaries_for_small_stream() {
let mut data = vec![0x00, 0x01, 0x00, 0xAA, 0xBB];
data.extend_from_slice(&[0x00, 0x01, 0x1F, 0xCC, 0xDD]);
let pkts = packetize_gob_aligned(&data, 1024, false, true);
assert_eq!(pkts.len(), 2, "expected one payload per start code");
assert_eq!(pkts[0].data_len(), 5);
assert_eq!(pkts[1].data_len(), 5);
assert!(pkts[1].marker);
}
#[test]
fn packetize_fragments_large_gob_at_byte_boundaries() {
let mut data = vec![0x00, 0x01, 0x1F];
data.extend(std::iter::repeat(0xAA).take(500 - 3));
let pkts = packetize_gob_aligned(&data, 100, false, true);
assert_eq!(pkts.len(), 500_usize.div_ceil(96));
for p in &pkts[..pkts.len() - 1] {
assert_eq!(p.data_len(), 96);
}
assert!(pkts.last().unwrap().marker);
}
#[test]
fn packetize_then_depacketize_round_trip() {
let mut data = vec![0x00, 0x01, 0x00];
data.extend(std::iter::repeat(0xA5).take(10));
data.extend_from_slice(&[0x00, 0x01, 0x1F]);
data.extend(std::iter::repeat(0x5A).take(10));
let pkts = packetize_gob_aligned(&data, 256, false, false);
let recovered = depacketize(&pkts).unwrap();
assert_eq!(recovered, data);
}
#[test]
fn packetize_then_depacketize_round_trip_with_fragmentation() {
let mut data = vec![0x00, 0x01, 0x00];
data.extend(std::iter::repeat(0xA5).take(50));
data.extend_from_slice(&[0x00, 0x01, 0x1F]);
data.extend(std::iter::repeat(0x5A).take(50));
let pkts = packetize_gob_aligned(&data, 20, false, false);
assert!(pkts.len() > 4, "expected fragmentation");
let recovered = depacketize(&pkts).unwrap();
assert_eq!(recovered, data);
}
#[test]
fn depacketize_rejects_short_header_payload() {
let bad = H261RtpPayload {
header: H261RtpHeader::gob_aligned(0, false, false),
bytes: vec![0u8; 2],
marker: true,
};
assert_eq!(depacketize(&[bad]), Err(RtpError::ShortHeader));
}
#[test]
fn depacketize_rejects_payload_with_no_start_codes() {
let hdr = pack_header(&H261RtpHeader::gob_aligned(0, false, false)).unwrap();
let mut bytes = Vec::new();
bytes.extend_from_slice(&hdr);
bytes.extend_from_slice(&[0xFFu8; 4]);
let p = H261RtpPayload {
header: H261RtpHeader::gob_aligned(0, false, false),
bytes,
marker: true,
};
assert_eq!(depacketize(&[p]), Err(RtpError::NoStartCodes));
}
#[test]
fn packetize_marker_bit_set_only_on_last_packet_of_frame() {
let mut data = vec![0x00, 0x01, 0x00];
data.extend(std::iter::repeat(0x11).take(5));
data.extend_from_slice(&[0x00, 0x01, 0x00]);
data.extend(std::iter::repeat(0x22).take(5));
let pkts = packetize_gob_aligned(&data, 256, false, false);
assert_eq!(pkts.len(), 2);
assert!(pkts[0].marker, "first packet should be tail of picture 1");
assert!(pkts[1].marker, "second packet should be tail of picture 2");
}
#[test]
fn empty_input_packetizes_to_empty_output() {
let pkts = packetize_gob_aligned(&[], 256, false, false);
assert!(pkts.is_empty());
}
#[test]
#[should_panic(expected = "max_payload must accommodate")]
fn packetize_panics_on_tiny_max_payload() {
let _ = packetize_gob_aligned(&[0x00, 0x01, 0x00], HEADER_LEN, false, false);
}
fn synthetic_two_picture_stream() -> Vec<u8> {
let mut data = vec![0x00, 0x01, 0x00];
data.extend(std::iter::repeat(0x11).take(5));
data.extend_from_slice(&[0x00, 0x01, 0x00]);
data.extend(std::iter::repeat(0x22).take(5));
data
}
fn synthetic_one_picture_stream() -> Vec<u8> {
let mut data = vec![0x00, 0x01, 0x00];
data.extend(std::iter::repeat(0xA5).take(20));
data
}
#[test]
fn rtp_packet_stamps_fixed_header_fields_and_marker_on_last() {
let mut pk = RtpPacketizer::new(96, 0xDEAD_BEEF, 1000, 1500);
let frame = synthetic_one_picture_stream();
let packets = pk.pack_frame(&frame, 90_000);
assert_eq!(packets.len(), 1);
let p = &packets[0];
assert_eq!(
p.bytes.len(),
RTP_FIXED_HEADER_LEN + HEADER_LEN + frame.len()
);
assert_eq!(p.sequence_number, 1000);
assert_eq!(p.timestamp, 90_000);
assert_eq!(p.ssrc, 0xDEAD_BEEF);
assert!(p.marker, "single-packet frame must carry M=1");
assert_eq!(p.bytes[1] & 0x80, 0x80);
assert_eq!(p.bytes[1] & 0x7F, 96);
assert_eq!(p.bytes[0], 0x80);
assert_eq!(pk.next_sequence_number(), 1001);
}
#[test]
fn rtp_packet_marker_set_only_on_last_of_frame_when_fragmented() {
let mut pk = RtpPacketizer::new(96, 1, 0, 20)
.with_intra_only(true)
.with_motion_vectors(false);
let frame = synthetic_one_picture_stream();
let packets = pk.pack_frame(&frame, 12345);
assert!(packets.len() >= 2, "expected fragmentation");
for (i, p) in packets.iter().enumerate() {
if i + 1 == packets.len() {
assert!(p.marker, "last packet must have M=1");
assert_eq!(p.bytes[1] & 0x80, 0x80);
} else {
assert!(!p.marker, "non-last packet must have M=0");
assert_eq!(p.bytes[1] & 0x80, 0);
}
assert_eq!(p.timestamp, 12345);
assert_eq!(p.ssrc, 1);
}
for w in packets.windows(2) {
assert_eq!(w[1].sequence_number, w[0].sequence_number.wrapping_add(1));
}
}
#[test]
fn rtp_packet_two_frames_share_timestamp_per_frame_only() {
let mut pk = RtpPacketizer::new(96, 7, 0, 1500);
let one = synthetic_one_picture_stream();
let pkts1 = pk.pack_frame(&one, 100);
let pkts2 = pk.pack_frame(&one, 200);
assert_eq!(pkts1.len(), 1);
assert_eq!(pkts2.len(), 1);
assert_eq!(pkts1[0].timestamp, 100);
assert_eq!(pkts2[0].timestamp, 200);
assert_eq!(pkts2[0].sequence_number, pkts1[0].sequence_number + 1);
assert!(pkts1[0].marker);
assert!(pkts2[0].marker);
}
#[test]
fn rtp_packet_two_gobs_in_one_frame_only_last_has_marker() {
let mut pk = RtpPacketizer::new(96, 1, 0, 1500);
let frame = synthetic_two_picture_stream();
let packets = pk.pack_frame(&frame, 90_000);
assert!(packets.len() >= 2, "expected ≥ 2 packets");
assert!(!packets[0].marker, "first packet must not be M=1");
assert!(packets.last().unwrap().marker, "last packet must be M=1");
}
#[test]
fn rtp_packet_payload_type_is_masked_to_7_bits() {
let mut pk = RtpPacketizer::new(0xFF, 0, 0, 1500);
let pkts = pk.pack_frame(&synthetic_one_picture_stream(), 1);
assert_eq!(pkts.len(), 1);
assert_eq!(pk.payload_type(), 0x7F);
let pt = pkts[0].bytes[1] & 0x7F;
assert_eq!(pt, 0x7F);
}
#[test]
fn rtp_packet_sequence_number_wraps() {
let mut pk = RtpPacketizer::new(96, 0, u16::MAX, 1500);
let p1 = pk.pack_frame(&synthetic_one_picture_stream(), 0);
let p2 = pk.pack_frame(&synthetic_one_picture_stream(), 1);
assert_eq!(p1[0].sequence_number, u16::MAX);
assert_eq!(p2[0].sequence_number, 0);
}
#[test]
fn rtp_packet_empty_input_emits_no_packets() {
let mut pk = RtpPacketizer::new(96, 0, 0, 1500);
let pkts = pk.pack_frame(&[], 0);
assert!(pkts.is_empty());
let pkts = pk.pack_frame(&[0xFFu8; 8], 0);
assert!(pkts.is_empty());
}
#[test]
#[should_panic(expected = "max_rtp_packet_size must accommodate")]
fn rtp_packetizer_panics_on_tiny_mtu() {
let _ = RtpPacketizer::new(96, 0, 0, RTP_FIXED_HEADER_LEN + HEADER_LEN);
}
#[test]
fn parse_rtp_fixed_header_round_trips_against_packetizer_output() {
let mut pk = RtpPacketizer::new(101, 0x1234_5678, 42, 1500);
let pkts = pk.pack_frame(&synthetic_one_picture_stream(), 0xCAFE_BABE);
let pkt = &pkts[0];
let (hdr, rest) = parse_rtp_fixed_header(&pkt.bytes).unwrap();
assert_eq!(hdr.version, 2);
assert!(!hdr.padding);
assert!(!hdr.extension);
assert_eq!(hdr.csrc_count, 0);
assert!(hdr.marker);
assert_eq!(hdr.payload_type, 101);
assert_eq!(hdr.sequence_number, 42);
assert_eq!(hdr.timestamp, 0xCAFE_BABE);
assert_eq!(hdr.ssrc, 0x1234_5678);
assert!(rest.len() >= HEADER_LEN);
let (h261_hdr, _payload) = unpack_header(rest).unwrap();
assert_eq!(h261_hdr.sbit, 0);
assert_eq!(h261_hdr.ebit, 0);
}
#[test]
fn parse_rtp_fixed_header_rejects_short_buffer() {
for n in 0..RTP_FIXED_HEADER_LEN {
let buf = vec![0x80u8; n]; assert_eq!(parse_rtp_fixed_header(&buf), Err(RtpError::ShortHeader));
}
}
#[test]
fn parse_rtp_fixed_header_rejects_wrong_version() {
let mut buf = vec![0u8; RTP_FIXED_HEADER_LEN];
buf[0] = 0x40;
assert_eq!(
parse_rtp_fixed_header(&buf),
Err(RtpError::FieldOverflow {
field: "RTP-V",
value: 1,
})
);
}
#[test]
fn parse_rtp_fixed_header_consumes_csrc_block() {
let mut buf = vec![0u8; RTP_FIXED_HEADER_LEN + 8 + 3];
buf[0] = 0x82;
buf[1] = 0x00; buf[12..16].copy_from_slice(&0xAABB_CCDDu32.to_be_bytes());
buf[16..20].copy_from_slice(&0x1122_3344u32.to_be_bytes());
buf[20] = 0xFE; buf[21] = 0xED;
buf[22] = 0xBE;
let (hdr, rest) = parse_rtp_fixed_header(&buf).unwrap();
assert_eq!(hdr.csrc_count, 2);
assert_eq!(rest, &[0xFE, 0xED, 0xBE]);
}
#[test]
fn parse_rtp_fixed_header_short_when_csrc_count_overruns() {
let mut buf = vec![0u8; RTP_FIXED_HEADER_LEN];
buf[0] = 0x8F; assert_eq!(parse_rtp_fixed_header(&buf), Err(RtpError::ShortHeader));
}
#[test]
fn packetizer_tracks_packet_and_octet_counts() {
use crate::rtcp::{parse_report, PT_SR};
let mut pk = RtpPacketizer::new(96, 0xABCD, 0, 1500);
assert_eq!(pk.packet_count(), 0);
assert_eq!(pk.octet_count(), 0);
let frame = synthetic_one_picture_stream();
let pkts = pk.pack_frame(&frame, 9000);
assert_eq!(pkts.len(), 1);
assert_eq!(pk.packet_count(), 1);
let expected_octets = (pkts[0].bytes.len() - RTP_FIXED_HEADER_LEN) as u32;
assert_eq!(pk.octet_count(), expected_octets);
let pkts2 = pk.pack_frame(&frame, 12000);
assert_eq!(pk.packet_count(), 2);
let expected2 = expected_octets + (pkts2[0].bytes.len() - RTP_FIXED_HEADER_LEN) as u32;
assert_eq!(pk.octet_count(), expected2);
let sr = pk
.sender_report(SenderInfoTestClock::NTP, &[])
.expect("sr builds");
let parsed = parse_report(&sr).unwrap();
assert_eq!(parsed.packet_type, PT_SR);
assert_eq!(parsed.ssrc, 0xABCD);
let info = parsed.sender_info.expect("SR carries sender info");
assert_eq!(info.packet_count, 2);
assert_eq!(info.octet_count, expected2);
assert_eq!(info.rtp_timestamp, 12000);
assert_eq!(info.ntp_timestamp, SenderInfoTestClock::NTP);
}
struct SenderInfoTestClock;
impl SenderInfoTestClock {
const NTP: u64 = 0xB44D_B705_2000_0000;
}
#[test]
fn sender_info_rtp_timestamp_zero_before_any_frame() {
let pk = RtpPacketizer::new(96, 1, 0, 1500);
let info = pk.sender_info(0);
assert_eq!(info.rtp_timestamp, 0);
assert_eq!(info.packet_count, 0);
assert_eq!(info.octet_count, 0);
}
#[test]
fn empty_pack_frame_does_not_advance_counters() {
let mut pk = RtpPacketizer::new(96, 1, 0, 1500);
let _ = pk.pack_frame(&[], 5);
let _ = pk.pack_frame(&[0xFFu8; 8], 5); assert_eq!(pk.packet_count(), 0);
assert_eq!(pk.octet_count(), 0);
assert_eq!(pk.sender_info(0).rtp_timestamp, 0);
}
#[test]
fn rtp_packet_round_trip_recovers_elementary_stream() {
let mut pk = RtpPacketizer::new(96, 0, 0, RTP_FIXED_HEADER_LEN + HEADER_LEN + 4);
let mut frame = vec![0x00, 0x01, 0x00];
frame.extend(std::iter::repeat(0xA5).take(30));
let pkts = pk.pack_frame(&frame, 0);
assert!(
pkts.len() >= 2,
"expected fragmentation, got {}",
pkts.len()
);
let mut inner = Vec::new();
for p in &pkts {
let (_h, rest) = parse_rtp_fixed_header(&p.bytes).unwrap();
inner.push(H261RtpPayload {
header: unpack_header(rest).unwrap().0,
bytes: rest.to_vec(),
marker: p.marker,
});
}
let recovered = depacketize(&inner).unwrap();
assert_eq!(recovered, frame);
}
fn textured_qcif() -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let (w, h) = (176usize, 144usize);
let mut y = vec![0u8; w * h];
let mut state = 0x2545_F491u32;
for j in 0..h {
for i in 0..w {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
let base = 40 + ((i * 160) / w + (j * 40) / h) as u32;
y[j * w + i] = (base + (state & 0x3F)).min(255) as u8;
}
}
let mut cb = vec![128u8; (w / 2) * (h / 2)];
let mut cr = vec![128u8; (w / 2) * (h / 2)];
for (idx, v) in cb.iter_mut().enumerate() {
*v = 100 + ((idx * 7) % 56) as u8;
}
for (idx, v) in cr.iter_mut().enumerate() {
*v = 110 + ((idx * 11) % 36) as u8;
}
(y, cb, cr)
}
fn encode_textured_qcif_intra(quant: u32) -> Vec<u8> {
let (y, cb, cr) = textured_qcif();
crate::encoder::encode_intra_picture(
crate::picture::SourceFormat::Qcif,
&y,
176,
&cb,
88,
&cr,
88,
quant,
0,
)
.expect("encode_intra_picture")
}
fn decoder_walk_oracle(data: &[u8]) -> Vec<SplitPoint> {
use crate::gob::{cif_gob_origin_luma, qcif_gob_origin_luma};
use crate::mb::{decode_macroblock, Picture};
use crate::picture::SourceFormat;
let mut br = BitReader::new(data);
let mut points = Vec::new();
let mut fmt = SourceFormat::Qcif;
let mut pic = Picture::new(176, 144);
loop {
let pos = br.bit_position();
let Some(sc) = find_next_start_code_bits(data, pos) else {
break;
};
if sc.bit_pos > pos {
br.skip((sc.bit_pos - pos) as u32).unwrap();
}
if sc.gn == GN_PICTURE {
points.push(SplitPoint {
bit: sc.bit_pos,
is_psc: true,
ctx: None,
});
let hdr = parse_picture_header(&mut br).unwrap();
fmt = hdr.source_format;
pic = Picture::new(hdr.width as usize, hdr.height as usize);
continue;
}
points.push(SplitPoint {
bit: sc.bit_pos,
is_psc: false,
ctx: None,
});
let gob_hdr = parse_gob_header(&mut br).unwrap();
let (gob_x, gob_y) = match fmt {
SourceFormat::Cif => cif_gob_origin_luma(gob_hdr.gn),
SourceFormat::Qcif => qcif_gob_origin_luma(gob_hdr.gn),
};
let mut quant = gob_hdr.gquant as u32;
let mut ctx = MbContext::reset();
let mut current_mba: i32 = 0;
loop {
let remaining = br.bits_remaining();
if remaining == 0 {
break;
}
if remaining >= 16 && br.peek_u32(16).unwrap() == 0x0001 {
break;
}
let Some(diff) = decode_mba_diff(&mut br).unwrap() else {
break;
};
let new_mba = current_mba + diff as i32;
current_mba = new_mba;
decode_macroblock(
&mut br,
new_mba as u8,
gob_x,
gob_y,
&mut quant,
&mut ctx,
&mut pic,
None,
)
.unwrap();
if new_mba <= 32 {
points.push(SplitPoint {
bit: br.bit_position(),
is_psc: false,
ctx: Some(FragCtx {
gobn: gob_hdr.gn,
last_mba: new_mba as u8,
quant: quant as u8,
mv: if ctx.prev_was_mc { ctx.mv } else { (0, 0) },
}),
});
}
}
}
points
}
#[test]
fn mb_walker_matches_real_decoder_bit_for_bit() {
for quant in [2u32, 8, 31] {
let bits = encode_textured_qcif_intra(quant);
let walked = walk_mb_split_points(&bits).expect("walk");
let oracle = decoder_walk_oracle(&bits);
assert_eq!(
walked, oracle,
"walker desynced from decoder at quant {quant}"
);
assert_eq!(walked.iter().filter(|p| p.is_psc).count(), 1);
assert_eq!(
walked
.iter()
.filter(|p| !p.is_psc && p.ctx.is_none())
.count(),
3,
"QCIF transmits GOBs 1, 3, 5"
);
assert_eq!(walked.iter().filter(|p| p.ctx.is_some()).count(), 3 * 32);
}
}
#[test]
fn mb_walker_matches_real_decoder_on_p_picture_with_motion() {
let (y, cb, cr) = textured_qcif();
let mut enc = crate::encoder::H261Encoder::new(crate::picture::SourceFormat::Qcif, 6);
let _i = enc.encode_frame(&y, 176, &cb, 88, &cr, 88).expect("I");
let mut y2 = vec![0u8; 176 * 144];
for j in 0..144usize {
for i in 0..176usize {
let sj = j.saturating_sub(2);
let si = i.saturating_sub(4);
y2[j * 176 + i] = y[sj * 176 + si];
}
}
let p_bits = enc.encode_frame(&y2, 176, &cb, 88, &cr, 88).expect("P");
let walked = walk_mb_split_points(&p_bits).expect("walk");
let oracle = decoder_walk_oracle(&p_bits);
assert_eq!(walked, oracle, "walker desynced from decoder on P-picture");
assert!(
walked.iter().any(|p| p.ctx.is_some_and(|c| c.mv != (0, 0))),
"shifted content should produce at least one MC-coded MB boundary"
);
}
#[test]
fn mb_fragment_round_trip_is_byte_exact_and_respects_budget() {
let bits = encode_textured_qcif_intra(8);
for &budget in &[128usize, 256, 512] {
let pkts = packetize_mb_fragmented(&bits, budget, true, false).expect("fragment");
assert!(
pkts.len() > 3,
"expected several packets at budget {budget}"
);
let mut saw_continuation = false;
for p in &pkts {
assert!(
p.bytes.len() <= budget,
"payload {} exceeds budget {budget}",
p.bytes.len()
);
assert!(p.bytes.len() > HEADER_LEN);
if p.header.gobn != 0 {
saw_continuation = true;
assert!((1..=12).contains(&p.header.gobn));
assert!((1..=31).contains(&p.header.quant));
assert!(p.header.mbap <= 31);
assert!((-15..=15).contains(&p.header.hmvd));
assert!((-15..=15).contains(&p.header.vmvd));
}
}
assert!(
saw_continuation,
"budget {budget} should force at least one mid-GOB packet"
);
assert_eq!(pkts.iter().filter(|p| p.marker).count(), 1);
assert!(pkts.last().unwrap().marker);
let recovered = depacketize(&pkts).expect("depacketize");
assert_eq!(recovered, bits, "round trip at budget {budget}");
}
}
#[test]
fn mb_fragment_boundaries_are_bit_contiguous() {
let bits = encode_textured_qcif_intra(8);
let pkts = packetize_mb_fragmented(&bits, 128, true, false).expect("fragment");
let mut misaligned_splits = 0;
for pair in pkts.windows(2) {
let (prev, next) = (&pair[0], &pair[1]);
assert_eq!(
next.header.sbit,
(8 - prev.header.ebit) % 8,
"SBIT/EBIT must describe the same split bit"
);
if next.header.sbit != 0 {
misaligned_splits += 1;
assert_eq!(
prev.bytes.last().unwrap(),
&next.bytes[HEADER_LEN],
"fragments must share the split byte"
);
}
}
assert!(
misaligned_splits > 0,
"expected at least one non-byte-aligned MB split"
);
}
#[test]
fn mb_fragment_continuation_context_matches_walker() {
let bits = encode_textured_qcif_intra(8);
let pkts = packetize_mb_fragmented(&bits, 128, true, false).expect("fragment");
let points = walk_mb_split_points(&bits).expect("walk");
let mut checked = 0;
for p in pkts.iter().filter(|p| p.header.gobn != 0) {
let matched = points.iter().any(|sp| {
sp.ctx.is_some_and(|c| {
c.gobn == p.header.gobn
&& c.last_mba == p.header.mbap + 1
&& c.quant == p.header.quant
&& c.mv == (p.header.hmvd as i32, p.header.vmvd as i32)
&& (sp.bit % 8) as u8 == p.header.sbit
})
});
assert!(
matched,
"continuation header {:?} matches no walker split point",
p.header
);
checked += 1;
}
assert!(checked > 0, "expected continuation packets to verify");
}
#[test]
fn mb_fragment_errors_when_no_split_fits() {
let bits = encode_textured_qcif_intra(8);
match packetize_mb_fragmented(&bits, HEADER_LEN + 1, true, false) {
Err(RtpError::FragmentTooLarge { needed, max }) => {
assert_eq!(max, 1);
assert!(needed > 1);
}
other => panic!("expected FragmentTooLarge, got {other:?}"),
}
}
#[test]
fn mb_fragment_emits_single_packet_when_frame_fits() {
let bits = encode_textured_qcif_intra(16);
let pkts =
packetize_mb_fragmented(&bits, bits.len() + HEADER_LEN, true, false).expect("fragment");
assert_eq!(pkts.len(), 1, "whole frame fits one packet");
let p = &pkts[0];
assert!(p.marker);
assert_eq!(p.header.sbit, 0);
assert_eq!(p.header.ebit, 0);
assert_eq!(p.header.gobn, 0);
assert_eq!(p.header.quant, 0);
assert_eq!(depacketize(&pkts).unwrap(), bits);
}
#[test]
fn mb_fragment_never_spans_a_psc() {
let (y, cb, cr) = textured_qcif();
let mut enc = crate::encoder::H261Encoder::new(crate::picture::SourceFormat::Qcif, 8);
let f0 = enc.encode_frame(&y, 176, &cb, 88, &cr, 88).expect("I");
let f1 = enc.encode_frame(&y, 176, &cb, 88, &cr, 88).expect("P");
let mut stream = f0.clone();
stream.extend_from_slice(&f1);
let pkts = packetize_mb_fragmented(&stream, 128, true, false).expect("fragment");
assert_eq!(
pkts.iter().filter(|p| p.marker).count(),
2,
"one marker per picture"
);
for pair in pkts.windows(2) {
if pair[0].marker {
assert_eq!(pair[1].header.gobn, 0);
assert_eq!(pair[1].header.sbit, 0);
}
}
assert_eq!(depacketize(&pkts).unwrap(), stream);
}
#[test]
fn rtp_packetizer_mb_fragmentation_emits_context_and_round_trips() {
let bits = encode_textured_qcif_intra(8);
let mut pk = RtpPacketizer::new(96, 0xABCD_EF01, 0, 200)
.with_intra_only(true)
.with_mb_fragmentation(true);
let packets = pk.pack_frame(&bits, 0);
assert!(!packets.is_empty());
let mut inner = Vec::new();
let mut saw_continuation = false;
for p in &packets {
assert!(p.bytes.len() <= 200);
let (_h, rest) = parse_rtp_fixed_header(&p.bytes).unwrap();
let (h261, _) = unpack_header(rest).unwrap();
if h261.gobn != 0 {
saw_continuation = true;
}
inner.push(H261RtpPayload {
header: h261,
bytes: rest.to_vec(),
marker: p.marker,
});
}
assert!(saw_continuation, "MTU 200 should force mid-GOB packets");
assert!(packets.last().unwrap().marker);
assert_eq!(packets.iter().filter(|p| p.marker).count(), 1);
assert_eq!(depacketize(&inner).unwrap(), bits);
}
#[test]
fn rtp_packetizer_mb_fragmentation_falls_back_when_unsplittable() {
let bits = encode_textured_qcif_intra(16);
let mtu = RTP_FIXED_HEADER_LEN + HEADER_LEN + 1;
let mut pk = RtpPacketizer::new(96, 1, 0, mtu).with_mb_fragmentation(true);
let packets = pk.pack_frame(&bits, 0);
assert!(!packets.is_empty(), "fallback must still emit packets");
let mut inner = Vec::new();
for p in &packets {
assert!(p.bytes.len() <= mtu);
let (_h, rest) = parse_rtp_fixed_header(&p.bytes).unwrap();
inner.push(H261RtpPayload {
header: unpack_header(rest).unwrap().0,
bytes: rest.to_vec(),
marker: p.marker,
});
}
assert_eq!(depacketize(&inner).unwrap(), bits);
}
}