oxideav_h261/lib.rs
1//! Pure-Rust ITU-T H.261 video codec (decoder + encoder).
2//!
3//! Scope (ITU-T Rec. H.261 03/93):
4//! * Picture header — PSC (20 bits: `0000 0000 0000 0001 0000`), TR (5),
5//! PTYPE (6) with `source format` bit (0 = QCIF, 1 = CIF), PEI/PSPARE loop.
6//! * GOB header — GBSC (16 bits), GN (4), GQUANT (5), GEI/GSPARE loop.
7//! * Macroblock layer — MBA VLC (Table 1), MTYPE VLC (Table 2), MQUANT (5),
8//! MVD VLC (Table 3), CBP VLC (Table 4).
9//! * Block layer — TCOEFF VLC (Table 5) + EOB + 20-bit `(escape | run | level)`
10//! escape, zigzag scan per Figure 12, dequantisation per §4.2.4, 8x8 IDCT,
11//! INTRA DC as 8-bit FLC per Table 6.
12//! * INTRA / INTER / INTER+MC / INTER+MC+FIL macroblock types.
13//! * Integer-pel motion compensation (range +-15 pels). Chroma MVs are the
14//! luma MV halved then truncated toward zero (§3.2.2).
15//! * Loop filter (FIL bit of MTYPE) — separable 1/4, 1/2, 1/4 filter, §3.2.3.
16//! * Output is YUV 4:2:0 in an `oxideav_core::VideoFrame`. Two picture sizes:
17//! QCIF 176x144 and CIF 352x288.
18//! * Encoder: I + P pictures, integer-pel MC (spiral+diamond ME), per-GOB
19//! MQUANT rate control, FIL loop-filter RDO, full §4.2.3.4 MV-pred.
20//! * BCH (511,493) error-correction framing (§5.4): the [`bch`] module
21//! wraps / unwraps the outer FEC multiframe layer (alignment pattern,
22//! `Fi` bit, BCH parity computation and per-frame syndrome check), and
23//! implements the spec-mandated `t = 1` single-bit error correction
24//! via [`bch::locate_single_error`] /
25//! [`bch::decode_multiframe_with_correction`]. Sweep-tested across all
26//! 511 protected bit positions per frame.
27//! * Hypothetical Reference Decoder buffer model (§5.2 + Annex B): the
28//! [`hrd`] module exposes the per-picture bit cap (256 kbits CIF /
29//! 64 kbits QCIF) and the HRD buffer-occupancy walk used to verify
30//! a coded sequence won't underflow a conforming decoder's receiving
31//! buffer at a given channel rate.
32//! * RTP payload-format wrapping (RFC 4587): the [`rtp`] module packs an
33//! elementary stream into a sequence of GOB-aligned H.261 RTP payloads
34//! (4-byte header + bitstream slice) and unpacks them back, plus the
35//! §4.2 RECOMMENDED MB-level fragmentation (`packetize_mb_fragmented`)
36//! that parses the Huffman layer to split oversize GOBs on macroblock
37//! boundaries with the full §4.1 GOBN/MBAP/QUANT/HMVD/VMVD context.
38//! The `RtpPacketizer` glue wraps each payload in a full RFC 3550 §5.1
39//! RTP fixed header (V/P/X/CC/M/PT/seq/timestamp/SSRC) so callers can
40//! hand `pack_frame` output straight to UDP / DTLS / SRTP.
41//! * RTCP report packets (RFC 3550 §6.4): the [`rtcp`] module builds and
42//! parses Sender Report (SR, PT=200) and Receiver Report (RR, PT=201)
43//! packets — the control-channel companions to the [`rtp`] data path.
44//! `RtpPacketizer` tracks the sender's running packet / octet counts so a
45//! conformant SR can be emitted directly from its session state.
46//! * RTCP SDES / BYE / APP packets (RFC 3550 §6.5 / §6.6 / §6.7): the
47//! [`rtcp`] module also builds and parses Source Description (SDES,
48//! PT=202), Goodbye (BYE, PT=203), and Application-Defined (APP, PT=204)
49//! packets, plus compound-packet (§6.1) concatenation and walking. APP
50//! carries a 4-byte ASCII name + 32-bit-aligned application-dependent
51//! data for application-layer extensions that do not warrant their own
52//! IANA-registered packet type.
53//! * SDP media-type signalling (RFC 4587 §6.1.1 / §6.2): the [`sdp`] module
54//! maps the `video/H261` media type and its three optional parameters
55//! (`CIF`, `QCIF`, `D`) to the SDP `a=rtpmap` / `a=fmtp` attribute lines,
56//! with the §6.2.1 offer/answer helpers (per-format MPI frame-rate bound,
57//! the "at least one picture size" invariant, the RFC-2032 QCIF fallback).
58//! * Annex D still-image transmission (§D.2 + §D.3): the [`annex_d`] module
59//! exposes the [`annex_d::SubImageIndex`] mapping to / from the 5-bit `TR`
60//! field, the still-image dimensions (4× the currently transmitted video
61//! format), and the Figure D.1 2:1 × 2:1 sub-sample / reassemble transform
62//! between a full-resolution still image and its four sub-images.
63//!
64//! Out of scope:
65//! * Multi-bit (weight ≥ 2) BCH (511,493) error correction. The
66//! generator polynomial has minimum distance `d = 3`, so the spec's
67//! `t = (d − 1) / 2 = 1` correction capability is the upper bound;
68//! weight-2 or denser error patterns are flagged
69//! (`uncorrectable_frames` in [`bch::DecodedMultiframe`]) but cannot
70//! be uniquely resolved by this code on its own — the inner H.261
71//! video VLC layer handles those via GOB resync.
72//!
73//! No runtime dependencies beyond `oxideav-core`, `oxideav-codec`, and
74//! `oxideav-pixfmt`.
75
76#![allow(clippy::needless_range_loop)]
77#![allow(clippy::too_many_arguments)]
78#![allow(clippy::unusual_byte_groupings)]
79#![allow(clippy::unnecessary_cast)]
80
81pub mod annex_d;
82pub mod bch;
83pub mod block;
84pub mod decoder;
85pub mod encoder;
86pub mod fdct;
87pub mod gob;
88pub mod hrd;
89pub mod idct;
90pub mod mb;
91pub mod picture;
92pub mod quant;
93pub mod rtcp;
94pub mod rtp;
95pub mod sdp;
96pub mod start_code;
97pub mod tables;
98
99use oxideav_core::{CodecCapabilities, CodecId, CodecTag};
100use oxideav_core::{CodecInfo, CodecRegistry, RuntimeContext};
101
102/// The canonical oxideav codec id for ITU-T H.261 video.
103///
104/// AVI FourCC `H261` maps to this id; raw `.h261` elementary-stream files
105/// probe to it as well.
106pub const CODEC_ID_STR: &str = "h261";
107
108/// Register the H.261 decoder and encoder with a codec registry.
109pub fn register_codecs(reg: &mut CodecRegistry) {
110 let caps = CodecCapabilities::video("h261_sw")
111 .with_lossy(true)
112 .with_intra_only(false)
113 .with_max_size(352, 288);
114 reg.register(
115 CodecInfo::new(CodecId::new(CODEC_ID_STR))
116 .capabilities(caps)
117 .decoder(decoder::make_decoder)
118 .encoder(encoder::make_encoder)
119 .tags([CodecTag::fourcc(b"H261"), CodecTag::fourcc(b"h261")]),
120 );
121}
122
123/// Unified registration entry point: install the H.261 codec factories
124/// into the codec sub-registry of a [`RuntimeContext`].
125///
126/// This is the preferred entry point for new code — it matches the
127/// convention every sibling crate now follows. Direct callers that need
128/// only the codec sub-registry can keep using [`register_codecs`].
129///
130/// Also wired into [`oxideav_meta::register_all`] via the
131/// [`oxideav_core::register!`] macro below.
132pub fn register(ctx: &mut RuntimeContext) {
133 register_codecs(&mut ctx.codecs);
134}
135
136oxideav_core::register!("h261", register);
137
138#[cfg(test)]
139mod register_tests {
140 use super::*;
141 use oxideav_core::{CodecId, CodecParameters, RuntimeContext};
142
143 #[test]
144 fn register_via_runtime_context_installs_codec_factory() {
145 let mut ctx = RuntimeContext::new();
146 register(&mut ctx);
147 let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
148 let dec = ctx
149 .codecs
150 .first_decoder(¶ms)
151 .expect("h261 decoder factory");
152 assert_eq!(dec.codec_id().as_str(), CODEC_ID_STR);
153 }
154
155 #[test]
156 fn register_via_runtime_context_installs_encoder_factory() {
157 let mut ctx = RuntimeContext::new();
158 register(&mut ctx);
159 let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
160 let enc = ctx
161 .codecs
162 .first_encoder(¶ms)
163 .expect("h261 encoder factory");
164 assert_eq!(enc.codec_id().as_str(), CODEC_ID_STR);
165 }
166
167 #[test]
168 fn encoder_factory_qcif_defaults() {
169 let mut ctx = RuntimeContext::new();
170 register(&mut ctx);
171 let mut params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
172 params.width = Some(176);
173 params.height = Some(144);
174 let enc = ctx
175 .codecs
176 .first_encoder(¶ms)
177 .expect("h261 encoder factory qcif");
178 assert_eq!(enc.codec_id().as_str(), CODEC_ID_STR);
179 let out = enc.output_params();
180 assert_eq!(out.width, Some(176));
181 assert_eq!(out.height, Some(144));
182 }
183
184 #[test]
185 fn encoder_factory_cif() {
186 let mut ctx = RuntimeContext::new();
187 register(&mut ctx);
188 let mut params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
189 params.width = Some(352);
190 params.height = Some(288);
191 let enc = ctx
192 .codecs
193 .first_encoder(¶ms)
194 .expect("h261 encoder factory cif");
195 assert_eq!(enc.codec_id().as_str(), CODEC_ID_STR);
196 let out = enc.output_params();
197 assert_eq!(out.width, Some(352));
198 assert_eq!(out.height, Some(288));
199 }
200
201 #[test]
202 fn encoder_factory_rejects_bad_dimensions() {
203 let mut ctx = RuntimeContext::new();
204 register(&mut ctx);
205 let mut params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
206 params.width = Some(320);
207 params.height = Some(240);
208 assert!(
209 ctx.codecs.first_encoder(¶ms).is_err(),
210 "should reject 320x240"
211 );
212 }
213}