videocall_codecs/vp9/mod.rs
1/*
2 * Copyright 2025 Security Union LLC
3 *
4 * Licensed under either of
5 *
6 * * Apache License, Version 2.0
7 * (http://www.apache.org/licenses/LICENSE-2.0)
8 * * MIT license
9 * (http://opensource.org/licenses/MIT)
10 *
11 * at your option.
12 *
13 * Unless you explicitly state otherwise, any contribution intentionally
14 * submitted for inclusion in the work by you, as defined in the Apache-2.0
15 * license, shall be dual licensed as above, without any additional terms or
16 * conditions.
17 */
18
19//! Pure-Rust VP9 encoder.
20//!
21//! This is a from-scratch, dependency-free VP9 encoder targeting a one-pass,
22//! realtime, error-resilient subset of VP9 Profile 0 (8-bit I420). It is a
23//! drop-in replacement for the C libvpx encoder used elsewhere in the project
24//! and compiles on every target, including `wasm32`.
25//!
26//! The implementation was built up behind an oracle-based TDD harness (encode
27//! with this encoder, decode with libvpx, assert PSNR/correctness); the
28//! milestones live in `tests/oracle_vp9.rs`. It now encodes keyframes and
29//! single-reference integer-pel inter frames, with one-pass VBR rate control and
30//! `cpu_used` speed presets.
31//!
32//! ## Planned module layout
33//!
34//! - `common/` — decoder-mandated, bit-exact machinery ported faithfully from
35//! libvpx `vp9/common/` and `vpx_dsp/`: the boolean arithmetic coder, default
36//! probability tables, context derivations, scan orders, dequant tables,
37//! inverse transforms, intra predictors, motion-compensation filters, and
38//! header syntax. Errors here silently corrupt the bitstream.
39//! - `enc/` — encoder-free choices that may be simplified aggressively: forward
40//! transforms, quantizer rounding, mode decision, motion search, rate control,
41//! and partitioning. Errors here only cost quality.
42//! - `debug/` — a minimal stream parser for round-trip validation plus IVF glue.
43
44use crate::encoder::{Encodable, EncodedFrame, EncoderConfig};
45use crate::vp9::common::frame_buffer::FrameBuffer;
46use crate::vp9::enc::encoder::{encode_inter_frame, encode_keyframe};
47use crate::vp9::enc::ratectrl::RateControl;
48use crate::vp9::enc::speed::SpeedFeatures;
49use anyhow::Result;
50
51pub(crate) mod common;
52#[cfg(any(test, feature = "test-utils"))]
53pub(crate) mod debug;
54pub mod dec;
55pub(crate) mod enc;
56
57/// Multiple of the per-frame keyframe target above which a keyframe is re-encoded
58/// once at a higher quantizer to rein in a gross overshoot.
59const KF_OVERSHOOT_MULT: i64 = 3;
60/// Quantizer step applied to the one keyframe overshoot re-encode.
61const KF_OVERSHOOT_QSTEP: i32 = 8;
62
63/// The pure-Rust VP9 encoder.
64///
65/// Construct via [`Encodable::new`]. The first frame (and every
66/// `keyframe_interval`-th frame, plus any frame after [`Vp9Encoder::force_keyframe`])
67/// is an intra keyframe; the rest are single-reference inter frames with
68/// integer-pel motion compensation. A one-pass VBR [`RateControl`] picks the
69/// per-frame quantizer within the app's `[min_quantizer, max_quantizer]` window;
70/// [`SpeedFeatures`] derived from `cpu_used` tune the motion search.
71pub struct Vp9Encoder {
72 config: EncoderConfig,
73 frame_count: u64,
74 /// Reconstruction of the most recently encoded frame with borders extended:
75 /// the LAST reference for the next inter frame. Also exposed (via the
76 /// cropped export) for the bit-exact oracle drift test.
77 reference: Option<FrameBuffer>,
78 /// One-pass VBR rate controller (target bitrate, qindex window, bit balance).
79 rc: RateControl,
80 /// `cpu_used`-derived speed knobs.
81 sf: SpeedFeatures,
82 /// Set by [`Vp9Encoder::force_keyframe`]; forces the next frame to be a
83 /// keyframe, then clears.
84 force_kf: bool,
85 /// Base qindex chosen for the most recently encoded frame (0 before the
86 /// first). Surfaced for rate-control drift tests via
87 /// [`Vp9Encoder::last_base_qindex`]; unread in non-test builds.
88 #[cfg_attr(not(any(test, feature = "test-utils")), allow(dead_code))]
89 last_qindex: u8,
90}
91
92impl Vp9Encoder {
93 /// The most recently encoded frame's reconstruction as a tight-packed I420
94 /// buffer at the cropped dimensions, or `None` before the first frame.
95 ///
96 /// This is exactly what a conformant decoder must reproduce, so tests can
97 /// assert bit-exactness against the libvpx oracle.
98 pub fn last_reconstruction_i420(&self) -> Option<Vec<u8>> {
99 self.reference.as_ref().map(|fb| fb.export_i420())
100 }
101
102 /// Base qindex the rate controller selected for the most recently encoded
103 /// frame. Exposed for tests that assert the quantizer varies across a
104 /// sequence (bit-exactness must hold under a changing per-frame qindex).
105 #[cfg(any(test, feature = "test-utils"))]
106 pub fn last_base_qindex(&self) -> u8 {
107 self.last_qindex
108 }
109
110 /// Request that the next encoded frame be a keyframe, regardless of the
111 /// keyframe interval. The flag clears after that frame is emitted. Useful
112 /// when the app needs a decodable refresh point (e.g. a new participant
113 /// joins).
114 pub fn force_keyframe(&mut self) {
115 self.force_kf = true;
116 }
117
118 /// Whether the next call to [`Encodable::encode`] will emit a keyframe (the
119 /// first frame, every `keyframe_interval`-th frame, or when forced).
120 fn is_keyframe(&self) -> bool {
121 let interval = self.config.keyframe_interval.max(1) as u64;
122 self.force_kf || self.reference.is_none() || self.frame_count.is_multiple_of(interval)
123 }
124
125 /// Encode `src` at `qindex` as a keyframe, or as an inter frame against the
126 /// current reference when one exists and `is_keyframe` is false.
127 fn encode_at(
128 &self,
129 src: &FrameBuffer,
130 is_keyframe: bool,
131 qindex: u8,
132 ) -> (Vec<u8>, FrameBuffer) {
133 match (is_keyframe, self.reference.as_ref()) {
134 (false, Some(reference)) => encode_inter_frame(src, reference, qindex, &self.sf),
135 _ => encode_keyframe(src, qindex),
136 }
137 }
138}
139
140impl Encodable for Vp9Encoder {
141 fn new(config: EncoderConfig) -> Result<Self> {
142 let rc = RateControl::new(&config);
143 let sf = SpeedFeatures::for_cpu_used(config.cpu_used);
144 Ok(Self {
145 config,
146 frame_count: 0,
147 reference: None,
148 rc,
149 sf,
150 force_kf: false,
151 last_qindex: 0,
152 })
153 }
154
155 fn update_bitrate_kbps(&mut self, kbps: u32) -> Result<()> {
156 self.config.bitrate_kbps = kbps;
157 self.rc.update_bitrate_kbps(kbps);
158 Ok(())
159 }
160
161 fn encode(&mut self, pts: i64, i420: &[u8]) -> Result<Option<EncodedFrame>> {
162 let (w, h) = (self.config.width, self.config.height);
163 let mut src = FrameBuffer::new(w, h);
164 src.import_i420(i420, w, h)
165 .map_err(|e| anyhow::anyhow!("i420 import failed: {e}"))?;
166
167 let is_keyframe = self.is_keyframe();
168 let target_bits = self.rc.frame_target_bits(is_keyframe);
169 let mut qindex = self.rc.select_qindex(is_keyframe, target_bits);
170 let (mut data, mut recon) = self.encode_at(&src, is_keyframe, qindex);
171
172 // Keyframe overshoot guard: a keyframe that blows far past its target is
173 // re-encoded once at a higher quantizer (the two-phase pipeline makes a
174 // one-shot re-encode cheap). Inter frames rely on the balance instead.
175 if is_keyframe {
176 let (_, qmax) = self.rc.qindex_window();
177 let actual_bits = (data.len() as i64) * 8;
178 if actual_bits > KF_OVERSHOOT_MULT * target_bits && (qindex as i32) < qmax {
179 let q2 = ((qindex as i32) + KF_OVERSHOOT_QSTEP).min(qmax) as u8;
180 let (d2, r2) = self.encode_at(&src, true, q2);
181 data = d2;
182 recon = r2;
183 qindex = q2;
184 }
185 }
186
187 let actual_bits = (data.len() as i64) * 8;
188 self.rc
189 .update_after_encode(is_keyframe, qindex, target_bits, actual_bits);
190 self.last_qindex = qindex;
191
192 // The reconstruction becomes the LAST reference; extend its borders so
193 // motion compensation of the next frame can read past the frame edge.
194 recon.extend_borders();
195 self.reference = Some(recon);
196 self.frame_count += 1;
197 self.force_kf = false;
198
199 Ok(Some(EncodedFrame {
200 data,
201 is_keyframe,
202 pts,
203 }))
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 fn cfg(keyframe_interval: u32) -> EncoderConfig {
212 EncoderConfig {
213 width: 64,
214 height: 64,
215 framerate: 30,
216 bitrate_kbps: 500,
217 keyframe_interval,
218 min_quantizer: 40,
219 max_quantizer: 60,
220 cpu_used: 7,
221 }
222 }
223
224 /// A flat mid-gray I420 frame of the config's dimensions.
225 fn gray(w: u32, h: u32) -> Vec<u8> {
226 vec![128u8; (w * h + 2 * (w.div_ceil(2)) * (h.div_ceil(2))) as usize]
227 }
228
229 fn is_kf(enc: &mut Vp9Encoder, pts: i64) -> bool {
230 enc.encode(pts, &gray(64, 64)).unwrap().unwrap().is_keyframe
231 }
232
233 #[test]
234 fn first_frame_is_keyframe_rest_are_inter() {
235 let mut enc = Vp9Encoder::new(cfg(150)).unwrap();
236 assert!(is_kf(&mut enc, 0), "frame 0 must be a keyframe");
237 for t in 1..10 {
238 assert!(!is_kf(&mut enc, t), "frame {t} must be inter");
239 }
240 }
241
242 #[test]
243 fn keyframe_cadence_follows_interval() {
244 let mut enc = Vp9Encoder::new(cfg(5)).unwrap();
245 // Keyframes at 0, 5, 10; inter elsewhere.
246 for t in 0..12i64 {
247 let key = is_kf(&mut enc, t);
248 let expect = t % 5 == 0;
249 assert_eq!(key, expect, "frame {t}: keyframe={key}, expected {expect}");
250 }
251 }
252
253 #[test]
254 fn force_keyframe_forces_next_frame_only() {
255 let mut enc = Vp9Encoder::new(cfg(150)).unwrap();
256 assert!(is_kf(&mut enc, 0)); // frame 0 keyframe
257 assert!(!is_kf(&mut enc, 1)); // frame 1 inter
258 enc.force_keyframe();
259 assert!(is_kf(&mut enc, 2), "forced frame must be a keyframe");
260 assert!(!is_kf(&mut enc, 3), "flag must clear after one frame");
261 }
262
263 #[test]
264 fn qindex_stays_within_configured_window() {
265 // q 40..60 → qindex window [160, 240].
266 let mut enc = Vp9Encoder::new(cfg(150)).unwrap();
267 for t in 0..8i64 {
268 enc.encode(t, &gray(64, 64)).unwrap();
269 let q = enc.last_base_qindex();
270 assert!((160..=240).contains(&q), "qindex {q} outside window");
271 }
272 }
273}