videocall_codecs/encoder/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//! Codec-agnostic video encoder interface.
20//!
21//! The [`Encodable`] trait abstracts over concrete encoder backends so that
22//! consumers can encode I420 frames without depending on a particular codec or
23//! implementation. Two backends implement it today:
24//!
25//! - [`crate::vp9::Vp9Encoder`] — the pure-Rust VP9 encoder (always compiled,
26//! including on `wasm32`).
27//! - The legacy libvpx-backed `Vp9Encoder` in [`libvpx`], available only with
28//! the `libvpx` feature on native targets, used as a test oracle and as an
29//! opt-in encode backend.
30//!
31//! Both are interchangeable behind `Box<dyn Encodable>`.
32
33use anyhow::Result;
34
35// The legacy libvpx encoder is only available on native targets with the
36// `libvpx` feature. `Vp9Encoder` is re-exported under the same cfg so
37// `videocall_codecs::encoder::Vp9Encoder` keeps resolving for the native bins.
38#[cfg(all(feature = "libvpx", not(target_arch = "wasm32")))]
39pub mod libvpx;
40#[cfg(all(feature = "libvpx", not(target_arch = "wasm32")))]
41pub use libvpx::Vp9Encoder;
42
43/// Configuration for a video encoder, independent of the concrete backend.
44///
45/// Backends map these fields onto their native configuration. Fields that a
46/// given backend does not support are ignored.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct EncoderConfig {
49 /// Frame width in pixels. Must be even.
50 pub width: u32,
51 /// Frame height in pixels. Must be even.
52 pub height: u32,
53 /// Nominal frame rate in frames per second (the encoder time base is 1/framerate).
54 pub framerate: u32,
55 /// Target bitrate in kilobits per second.
56 pub bitrate_kbps: u32,
57 /// Maximum distance between keyframes, in frames.
58 pub keyframe_interval: u32,
59 /// Minimum quantizer (0-63); lower means higher quality.
60 pub min_quantizer: u32,
61 /// Maximum quantizer (0-63); higher means lower quality.
62 pub max_quantizer: u32,
63 /// Speed/quality trade-off (0 = slowest/best, 8 = fastest/worst).
64 pub cpu_used: u8,
65}
66
67impl Default for EncoderConfig {
68 fn default() -> Self {
69 Self {
70 width: 640,
71 height: 480,
72 framerate: 30,
73 bitrate_kbps: 500,
74 keyframe_interval: 150,
75 min_quantizer: 40,
76 max_quantizer: 60,
77 cpu_used: 7,
78 }
79 }
80}
81
82/// A single compressed frame produced by an encoder.
83#[derive(Debug, Clone)]
84pub struct EncodedFrame {
85 /// Compressed bitstream for this frame (owned; consumers copy immediately).
86 pub data: Vec<u8>,
87 /// Whether this frame is a keyframe (decodable without references).
88 pub is_keyframe: bool,
89 /// Presentation timestamp in time-base units (as supplied to [`Encodable::encode`]).
90 pub pts: i64,
91}
92
93/// A codec-agnostic video encoder.
94///
95/// Implementations accept planar I420 frames and emit at most one compressed
96/// frame per call. Because some backends buffer frames (e.g. `lag_in_frames`),
97/// [`encode`](Encodable::encode) may return `None` for a call that produced no
98/// output yet; callers must tolerate zero-frame results.
99pub trait Encodable {
100 /// Create a new encoder with the given configuration.
101 fn new(config: EncoderConfig) -> Result<Self>
102 where
103 Self: Sized;
104
105 /// Update the target bitrate at runtime.
106 fn update_bitrate_kbps(&mut self, kbps: u32) -> Result<()>;
107
108 /// Encode one planar I420 frame.
109 ///
110 /// `pts` is the presentation timestamp in time-base units. `i420` is the
111 /// full I420 buffer (`width * height * 3 / 2` bytes). Returns the compressed
112 /// frame if one is ready, or `None` if the encoder buffered it.
113 fn encode(&mut self, pts: i64, i420: &[u8]) -> Result<Option<EncodedFrame>>;
114}
115
116/// Construct an encoder for the given codec.
117///
118/// Currently only [`VideoCodec::Vp9Profile0Level10Bit8`](crate::decoder::VideoCodec::Vp9Profile0Level10Bit8)
119/// is supported, backed by the pure-Rust [`crate::vp9::Vp9Encoder`]. Any other
120/// codec returns an error.
121pub fn create_encoder(
122 codec: crate::decoder::VideoCodec,
123 cfg: EncoderConfig,
124) -> Result<Box<dyn Encodable + Send>> {
125 use crate::decoder::VideoCodec;
126 match codec {
127 VideoCodec::Vp9Profile0Level10Bit8 => Ok(Box::new(crate::vp9::Vp9Encoder::new(cfg)?)),
128 other => Err(anyhow::anyhow!(
129 "no pure-Rust encoder available for codec {other:?}"
130 )),
131 }
132}