Skip to content

v0.0.5

Choose a tag to compare

@MagicalTux MagicalTux released this 24 May 16:06
· 31 commits to master since this release
09f616b

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 / fmtp parameter mapping (oxideav_h261::sdp).
    New module implementing the RFC 4587 §6.1.1 video/H261 media-type
    registration and its §6.2 SDP mapping. The a=rtpmap line is fixed —
    encoding name H261, clock rate 90000, m= media name video (pinned by
    ENCODING_NAME / CLOCK_RATE / MEDIA_NAME); format_rtpmap(pt) emits it
    and parse_rtpmap reads 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 optional a=fmtp parameters: CIF / QCIF carry an
    MPI integer 1..=4 ("max rate 29.97 / value fps"), D signals Annex D
    still-image support (1/0). format_value / format_fmtp emit
    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) and D ∈ {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: validate enforces "SHALL specify at least one
    supported picture size" (NoPictureSize), rfc2032_fallback returns the
    §6.2.1 default (QCIF MPI=1) for a peer that omits picture-size params, and
    max_frame_rate(fmt) returns the exact 29.97 / MPI bound 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-specific rtpmap / fmtp wire
    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_app reverses it. The builder enforces three §6.7 invariants —
    subtype ≤ 31 (5-bit field, AppSubtypeOutOfRange), name exactly 4
    octets (AppNameWrongLength), and data.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 through parse_compound as a
    typed RtcpPacket::App(AppPacket) variant rather than falling into the
    catch-all Other; 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 of SdesItems
    Cname (§6.5.1, mandatory), Name, Email, Phone, Loc, Tool,
    Note, and Priv (§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. compound concatenates pre-built sub-packets
    into one datagram body; parse_compound walks a received datagram back into
    typed RtcpPackets (Report / Sdes / Bye / Other for unmodelled PTs
    such as APP=204), advancing via each sub-packet's self-delimiting length
    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) and SenderInfo round-trip through parse_report,
    which validates V=2, the SR/RR PT, and the §6.4.1 length field
    (32-bit words minus one). RtpPacketizer now tracks the session's
    running packet/octet counts and the last frame's RTP timestamp, exposed
    via packet_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
    between H261Encoder and the RTP wire format. Construct with
    RtpPacketizer::new(payload_type, ssrc, initial_sequence_number, max_rtp_packet_size); call pack_frame(frame_bytes, rtp_timestamp_90khz) once per coded picture. Returns a sequence of
    RtpPackets whose bytes field 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 a u8 with the high bit set don't
    corrupt the M bit. parse_rtp_fixed_header parses RFC 3550 §5.1
    headers (including any CSRC list) for the receiver side. Wired
    through an end-to-end test that drives H261Encoder.encode_frame()
    for an I + P pair, packets them, parses the RTP fixed headers,
    reuses depacketize on the inner payloads, and decodes the result
    back into video frames.

  • RTP payload format (RFC 4587). New oxideav_h261::rtp module
    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) and depacketize reassembler
    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 against encode_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; the bch and rtp modules are
    mutually exclusive consumers of an elementary stream. pack_header
    enforces the -16 MVD prohibition (5-bit field '10000' is
    forbidden by §4.1).

  • Hypothetical Reference Decoder buffer model (§5.2 + Annex B). New
    oxideav_h261::hrd module exposes the §5.2 per-picture cap
    (64 kbits QCIF, 256 kbits CIF, excluding §5.4 FEC framing) and
    the Annex B buffer-occupancy walk. HrdParams::new(R_max) derives
    B = 4 * R_max / 29.97 and the receiver buffer size B + 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 input 0 followed by
    492 ones, the parity is exactly 011011010100011011₂ = 0x1B51B.
    The new parity_matches_spec_5_4_2_worked_example test feeds that
    input through parity18 and asserts equality with the spec value,
    pinning the implementation to the spec's own published test data.

Tests added

  • hrd::tests::* (12 unit tests in src/hrd.rs):
    • Per-picture cap returns 64 / 256 kbits for QCIF / CIF.
    • check_picture_cap returns Ok at-or-below the cap, Overflow
      above it with both actual_bits and cap_bits populated.
    • HrdParams::new derives B correctly at 64 kbit/s and 2 Mbit/s
      channel rates (integer-rational truncation matches the spec
      fraction 4 * R * 10000 / 299700).
    • walk_buffer at matched rate ⇒ buffer drains to exactly 0;
      smaller pictures accumulate monotonically; oversized first picture
      triggers first_underflow = Some(0); skip factor N = 2 doubles
      per-interval arrival as expected.
    • check_overflow is 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) — verifies parity18 against the §5.4.2 worked example.

Changed

  • lib.rs module-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::bch module wraps and unwraps the outer multiframe
    FEC layer H.261 prescribes for noisy p × 64 kbit/s channels. The
    module computes the 18-bit BCH parity over the 493-bit Fi || 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
    (0x495C9 in 19-bit form), assembles 8-frame multiframes carrying
    the alignment pattern S1..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 with GEN_POLY whenever
      the bit-18 sentinel is set.
    • syndrome18(data, parity) -> u32 — zero means the codeword
      matches g(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; reports corrupted_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 in src/bch.rs):
    • Generator polynomial factors (0x211)*(0x259) == 0x495C9 over
      GF(2).
    • All-zero input ⇒ zero parity.
    • All-ones 493-bit input round-trips through parity18 /
      syndrome18 with 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 >= 1 without
      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).
  • 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.

Changed

  • lib.rs module-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 from no / no to yes / yes.