v0.0.5
Other
- RFC 4587 §6.1.1/§6.2 video/H261 rtpmap + fmtp parameter mapping
- RFC 3550 §6.7 Application-Defined (APP, PT=204) packet
- RFC 3550 §6.5 SDES + §6.6 BYE + §6.1 compound packets
- RFC 3550 §6.4 Sender/Receiver Report builders + RtpPacketizer counters
- encoder-side RFC 3550 packetiser stamps RFC 4587 RTP packets
- implement RFC 4587 H.261 RTP payload-format wrap/unwrap
- implement §5.2 + Annex B HRD buffer model and §5.4.2 spec test
- implement BCH (511,493) forward error correction framing (§5.4)
Added
-
SDP media-type /
rtpmap/fmtpparameter mapping (oxideav_h261::sdp).
New module implementing the RFC 4587 §6.1.1video/H261media-type
registration and its §6.2 SDP mapping. Thea=rtpmapline is fixed —
encoding nameH261, clock rate90000,m=media namevideo(pinned by
ENCODING_NAME/CLOCK_RATE/MEDIA_NAME);format_rtpmap(pt)emits it
andparse_rtpmapreads it back, confirming the encoding name is H.261
(case-insensitively), tolerating the optional trailing channel field, and
rejecting other codecs / non-rtpmap lines.H261FmtpParams { cif, qcif, d }
models the three §6.1.1 optionala=fmtpparameters:CIF/QCIFcarry an
MPI integer 1..=4 ("max rate29.97 / valuefps"),Dsignals Annex D
still-image support (1/0).format_value/format_fmtpemit
CIF=2;QCIF=1;D=1(CIF before QCIF, the §6.2.1 example order; no line when
no parameters are set, per §6.2 "if any");parse_value/parse_fmtp
reverse it, enforcing the 1..=4 MPI range (MpiOutOfRange) andD ∈ {0,1}
(BadAnnexD), rejecting non-integer values / malformed tokens / duplicate
picture-size params, tolerating whitespace, matching parameter names
case-insensitively, and skipping unknown parameters forward-compatibly. The
§6.2.1 offer/answer helpers:validateenforces "SHALL specify at least one
supported picture size" (NoPictureSize),rfc2032_fallbackreturns the
§6.2.1 default (QCIF MPI=1) for a peer that omits picture-size params, and
max_frame_rate(fmt)returns the exact29.97 / MPIbound as an integer
rational ((2997, 100 * MPI)) so the §6.2.1 "≤ 15 fps for CIF=2" bound is
computed without floating-point round-off. The SDP offer/answer state machine
and the rest of the session description (v=/o=/c=/t=) remain
caller-side — this module owns only the H.261-specificrtpmap/fmtpwire
format. The RFC 2032 H.261-specific RTCP control packets (FIR / NACK) are
deliberately not implemented: RFC 4587 §7.1 mandates new implementations
SHALL ignore them and SHALL NOT use them. 36 new unit tests cover the
spec-example round trip, both line builders/parsers (with and without the
a=prefix), the full 1..=4 MPI range, all six error variants, the
forward-compatible unknown-parameter skip, case-insensitive name matching,
the RFC-2032 fallback, the frame-rate rational, and a full two-line session-
description round trip. -
RTCP APP (Application-Defined) packet (
oxideav_h261::rtcp). Builder and
parser for RFC 3550 §6.7 (PT = 204).build_app(subtype, ssrc, name, data)
emits the standard 4-byte RTCP header (with the 5-bit RC slot reused as the
§6.7 subtype) + SSRC + 4-octet ASCII name + application-dependent data;
parse_appreverses it. The builder enforces three §6.7 invariants —
subtype≤ 31 (5-bit field,AppSubtypeOutOfRange),nameexactly 4
octets (AppNameWrongLength), anddata.len() % 4 == 0
(AppDataNotAligned, "must be a multiple of 32 bits long"). The parser
rejects truncated buffers, V != 2, PT != 204, and length-field-smaller-than-
mandatory-header. APP packets now round-trip throughparse_compoundas a
typedRtcpPacket::App(AppPacket)variant rather than falling into the
catch-allOther; unknown PTs (e.g. RFC 4585 RTPFB = 205) still surface as
Other. §6.7 mandates names be case-sensitive ("uppercase and lowercase
characters treated as distinct"), so the parser surfaces the four bytes
verbatim without case folding. The §6.2 transmission-interval scheduler and
the §A.1 / §A.3 / §A.8 loss-fraction / jitter estimators remain caller-side
(out of scope for the codec). 14 new unit tests cover the header layout for
empty + data-bearing packets, subtype-0 / subtype-31 boundaries, 1024-byte
payload round-trip, byte-exact (non-case-folded) name preservation,
short-header / bad-version / wrong-PT / truncated-by-length / past-stated-
length rejection paths, all three builder validation errors (subtype-32,
name-of-every-non-4-length, data-of-every-non-aligned-length 1..=9), and a
compound RR + SDES + APP round-trip that pulls the App variant back typed.
The pre-existing "compound_preserves_unknown_app_packet" test was updated to
use PT = 205 (a stand-in for an RFC 4585 RTPFB packet this module doesn't
model) since APP is no longer "unknown." -
RTCP SDES + BYE + compound packets (
oxideav_h261::rtcp). Rounds out
the control channel beyond SR/RR (RFC 3550 §6.5 / §6.6 / §6.1).
build_sdes/parse_sdes(PT=202, §6.5) handle Source Description
packets: 0..=31 chunks, each binding an SSRC/CSRC to a list ofSdesItems
—Cname(§6.5.1, mandatory),Name,Email,Phone,Loc,Tool,
Note, andPriv(§6.5.8 prefix/value) — independently 32-bit-aligned
with a trailing END (item-type-0) byte and null padding.build_cname_sdes
is the one-call helper for the minimal "SSRC → CNAME" chunk §6.1 requires
in every compound packet.build_bye/parse_bye(PT=203, §6.6) carry
0..=31 leaving SSRC/CSRC identifiers plus an optional 8-bit-length-prefixed,
null-padded free-text reason.compoundconcatenates pre-built sub-packets
into one datagram body;parse_compoundwalks a received datagram back into
typedRtcpPackets (Report/Sdes/Bye/Otherfor unmodelled PTs
such as APP=204), advancing via each sub-packet's self-delimitinglength
field. Item text / reason strings are validated against the 255-octet 8-bit
length limit (TextTooLong/PrivTooLong); the SC field is capped at 31
(TooManySources); parsers decode text UTF-8-lossily so a malformed
datagram never panics, skip unknown SDES item types forward-compatibly, and
reject truncated / wrong-PT / wrong-version input. Scheduling (§6.2) and the
§A.1/§A.3/§A.8 loss/jitter estimators remain caller-side (out of scope for
the codec). 19 new unit tests cover header/alignment, all-item-type and
multi-chunk round-trips, max-length and empty-chunk edges, the 31/255 caps,
unknown-item skipping, and three compound-packet round-trips (RR+SDES+BYE,
SR-with-block+SDES, RR+unmodelled-APP) plus truncation rejection. -
RTCP Sender / Receiver Report builders (
oxideav_h261::rtcp). The
control-channel companions to the RTP data path (RFC 3550 §6.4).
build_sender_report(PT=200, §6.4.1) emits the 8-byte RTCP header +
20-byte sender-info section (NTP + RTP timestamps, sender's packet &
octet counts) + 0..=31 reception report blocks;build_receiver_report
(PT=201, §6.4.2) is the same minus the sender-info section, with an
empty RR (RC=0) as the canonical "nothing to report" packet.
ReceptionReportBlock(24 bytes: SSRC, 8-bit fraction lost, 24-bit
two's-complement cumulative lost, extended highest sequence number,
jitter, LSR, DLSR) andSenderInforound-trip throughparse_report,
which validates V=2, the SR/RR PT, and the §6.4.1lengthfield
(32-bit words minus one).RtpPacketizernow tracks the session's
running packet/octet counts and the last frame's RTP timestamp, exposed
viapacket_count()/octet_count()/sender_info()and a
sender_report()convenience that drops a conformant SR straight out of
the packetiser state. Scheduling (§6.2), SDES/CNAME/BYE, and the
§A.1/§A.3/§A.8 loss/jitter estimators remain caller-side (out of scope
for the codec). Wired through end-to-end tests that encode QCIF
I-pictures, packetize them, build an SR from the packetiser counters,
and round-trip both SR and RR through the parser. -
Encoder-side RTP packetiser (
RtpPacketizer). Higher-level glue
betweenH261Encoderand the RTP wire format. Construct with
RtpPacketizer::new(payload_type, ssrc, initial_sequence_number, max_rtp_packet_size); callpack_frame(frame_bytes, rtp_timestamp_90khz)once per coded picture. Returns a sequence of
RtpPackets whosebytesfield is a complete RFC 3550 §5.1 fixed
header (V=2, P=0, X=0, CC=0, M, PT, seq, ts, SSRC) followed by the
RFC 4587 §4.1 4-byte H.261 header and the GOB-aligned payload slice.
The marker bit is set on the LAST packet of each frame per RFC 4587
§4.1 ("MUST be set to one in the last packet of a video frame;
otherwise, it MUST be zero"); sequence numbers auto-advance mod
2^16 across frames; the same RTP timestamp is stamped on every
packet of one frame (§4.1). The 7-bit payload type is masked
internally so callers passing au8with the high bit set don't
corrupt the M bit.parse_rtp_fixed_headerparses RFC 3550 §5.1
headers (including any CSRC list) for the receiver side. Wired
through an end-to-end test that drivesH261Encoder.encode_frame()
for an I + P pair, packets them, parses the RTP fixed headers,
reusesdepacketizeon the inner payloads, and decodes the result
back into video frames. -
RTP payload format (RFC 4587). New
oxideav_h261::rtpmodule
implements the H.261 RTP payload-format §4.1 4-byte header (SBIT,
EBIT, I, V, GOBN, MBAP, QUANT, HMVD, VMVD) with bit-exact
pack_header/unpack_header, plus the GOB-aligned cheap
packetizer (packetize_gob_aligned) anddepacketizereassembler
from §4.2. The packetizer splits at byte-aligned PSC / GBSC
boundaries, fragments oversized GOBs at byte boundaries (SBIT/EBIT
stay zero), and sets the RTP marker-bit hint on the last payload of
each frame. Round-trips are byte-exact againstencode_intra_picture
output and the recovered stream still decodes through the regular
H261Decoder. RFC 4587 §4.1's explicit "no BCH on the RTP path" rule
is documented in the module's intro; thebchandrtpmodules are
mutually exclusive consumers of an elementary stream.pack_header
enforces the-16MVD prohibition (5-bit field'10000'is
forbidden by §4.1). -
Hypothetical Reference Decoder buffer model (§5.2 + Annex B). New
oxideav_h261::hrdmodule exposes the §5.2 per-picture cap
(64 kbitsQCIF,256 kbitsCIF, excluding §5.4 FEC framing) and
the Annex B buffer-occupancy walk.HrdParams::new(R_max)derives
B = 4 * R_max / 29.97and the receiver buffer sizeB + 256 kbits
via integer-rational arithmetic so long sequences don't drift on
floating-point round-off.walk_buffer(pictures, N, params)returns
the post-removal occupancy after every picture and the first underflow
index (if any);check_overflow(pictures, N, params)flags the dual
pre-removal-overflow case. The HRD is a coder-side compliance check
only — no on-wire changes. -
Spec §5.4.2 worked-example regression test for
bch::parity18.
The ITU-T H.261 (03/93) spec publishes a single validation vector
for the BCH parity routine — for the 493-bit input0followed by
492 ones, the parity is exactly011011010100011011₂ =0x1B51B.
The newparity_matches_spec_5_4_2_worked_exampletest feeds that
input throughparity18and asserts equality with the spec value,
pinning the implementation to the spec's own published test data.
Tests added
hrd::tests::*(12 unit tests insrc/hrd.rs):- Per-picture cap returns 64 / 256 kbits for QCIF / CIF.
check_picture_capreturnsOkat-or-below the cap,Overflow
above it with bothactual_bitsandcap_bitspopulated.HrdParams::newderivesBcorrectly at 64 kbit/s and 2 Mbit/s
channel rates (integer-rational truncation matches the spec
fraction4 * R * 10000 / 299700).walk_bufferat matched rate ⇒ buffer drains to exactly 0;
smaller pictures accumulate monotonically; oversized first picture
triggersfirst_underflow = Some(0); skip factorN = 2doubles
per-interval arrival as expected.check_overflowis silent under normal drain, trips at the
correct frame index when pictures are tiny relative to arrival.
tests/hrd_e2e.rs(4 integration tests):- Real QCIF I-pictures at quant=8 / quant=2 fit the §5.2 cap.
- 10 real I-pictures at quant=12 fail the HRD walk at N=1 / 29.97 fps
over 64 kbit/s (each picture far larger than per-interval arrival)
but succeed at N=10 (≈3 fps); confirms the HRD correctly identifies
both regimes. - 30 real I-pictures matched against a 64 kbit/s / N=4 channel pass
the overflow check (matched-rate drain).
bch::tests::parity_matches_spec_5_4_2_worked_example(1 new unit
test) — verifiesparity18against the §5.4.2 worked example.
Changed
-
lib.rsmodule-docstring scope: HRD §5.2 + Annex B added to
in-scope items. -
README feature matrix:
HRD buffer model (§5.2 + Annex B)row
added (yes / yes). -
BCH (511, 493) forward error correction framing (§5.4). New
oxideav_h261::bchmodule wraps and unwraps the outer multiframe
FEC layer H.261 prescribes for noisyp × 64 kbit/schannels. The
module computes the 18-bit BCH parity over the 493-bitFi || data
field via the spec generator polynomial
g(x) = (x^9 + x^4 + 1)(x^9 + x^6 + x^4 + x^3 + 1) = x^18 + x^15 + x^12 + x^10 + x^8 + x^7 + x^6 + x^3 + 1
(0x495C9in 19-bit form), assembles 8-frame multiframes carrying
the alignment patternS1..S8 = 0 0 0 1 1 0 1 1, and surfaces the
per-frame BCH syndrome as a corruption diagnostic.parity18(data: &[u8]) -> u32— long-division shift-register
implementation, 19-bit register XORed withGEN_POLYwhenever
the bit-18 sentinel is set.syndrome18(data, parity) -> u32— zero means the codeword
matchesg(x), non-zero means at least one bit error.encode_multiframe(coded, bits)— packs an arbitrary
inner-bitstream payload into 512-byte multiframes, emitting
Fi=0 stuffing frames to round up to a multiframe boundary.decode_multiframe(framed)— requires 3 consecutive complete
alignment patterns (24 framing bits ≡ 3 multiframes) for lock
per §5.4.4; reportscorrupted_frames(non-zero-syndrome count),
fill_frames(Fi=0 frames skipped), and the recovered inner data.
The BCH layer is transport-level — neither the public
H261Decoder
nor the encoder change shape. Callers that need framed output for
a raw bit-serial link wrap their bytes; callers receiving a framed
stream (e.g. RFC 4587 §6.2 historical deployments) recover the
inner stream.
Tests added
bch::tests::*(12 unit tests insrc/bch.rs):- Generator polynomial factors
(0x211)*(0x259) == 0x495C9over
GF(2). - All-zero input ⇒ zero parity.
- All-ones 493-bit input round-trips through
parity18/
syndrome18with zero residue. - Single-bit flip in either data or parity is detected by the
syndrome. - Round-trip across 1 / 3 / 6 whole multiframes plus a
< 1-multiframe payload that exercises the fill-frame path. - Data corruption surfaces as
corrupted_frames >= 1without
breaking lock. - Lock acquired when the framed stream is preceded by 4 junk bits.
- All-ones noise input fails to obtain frame lock (no false
positive on alignment).
- Generator polynomial factors
tests/bch_e2e.rs(3 integration tests):- End-to-end QCIF I-picture encode → BCH wrap → BCH unwrap → H.261
decode round-trip, PSNR ≥ 32 dB. - Single-bit error in the FEC payload is flagged via syndrome but
data is still passed through. - Two concatenated pictures BCH-wrapped separately survive the
unwrap intact.
- End-to-end QCIF I-picture encode → BCH wrap → BCH unwrap → H.261
Changed
lib.rsmodule-docstring "Out of scope" entry for BCH §5.4 replaced
with the in-scope description (single-bit correction of corrupted
codewords is the only remaining out-of-scope item).- README feature matrix:
BCH forward error correction (§5.4)row
flipped fromno / notoyes / yes.