Skip to main content

videocall_cli/
video_encoder.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
19use anyhow::{anyhow, Result};
20use std::mem::MaybeUninit;
21use std::os::raw::{c_int, c_ulong};
22use vpx_sys::*;
23
24macro_rules! vpx {
25    ($f:expr) => {{
26        let res = unsafe { $f };
27        let res_int = unsafe { std::mem::transmute::<vpx_sys::vpx_codec_err_t, i32>(res) };
28        if res_int != 0 {
29            return Err(anyhow!("vpx function error code ({}).", res_int));
30        }
31        res
32    }};
33}
34
35macro_rules! vpx_ptr {
36    ($f:expr) => {{
37        let res = unsafe { $f };
38        if res.is_null() {
39            return Err(anyhow!("vpx function returned null pointer."));
40        }
41        res
42    }};
43}
44
45pub struct VideoEncoderBuilder {
46    pub min_quantizer: u32,
47    pub max_quantizer: u32,
48    pub bitrate_kbps: u32,
49    pub fps: u32,
50    pub resolution: (u32, u32),
51    pub cpu_used: u32,
52    pub profile: u32,
53}
54
55impl VideoEncoderBuilder {
56    pub fn new(fps: u32, cpu_used: u8) -> Self {
57        Self {
58            bitrate_kbps: 500,
59            max_quantizer: 60,
60            min_quantizer: 40,
61            resolution: (640, 480),
62            fps,
63            cpu_used: cpu_used as u32,
64            profile: 0,
65        }
66    }
67}
68
69impl VideoEncoderBuilder {
70    pub fn set_resolution(mut self, width: u32, height: u32) -> Self {
71        self.resolution = (width, height);
72        self
73    }
74
75    pub fn build(&self) -> Result<VideoEncoder> {
76        let (width, height) = self.resolution;
77        if width % 2 != 0 || width == 0 {
78            return Err(anyhow!("Width must be divisible by 2"));
79        }
80        if height % 2 != 0 || height == 0 {
81            return Err(anyhow!("Height must be divisible by 2"));
82        }
83        let cfg_ptr = vpx_ptr!(vpx_codec_vp9_cx());
84        let mut cfg = unsafe { MaybeUninit::zeroed().assume_init() };
85        vpx!(vpx_codec_enc_config_default(cfg_ptr, &mut cfg, 0));
86
87        cfg.g_w = width;
88        cfg.g_h = height;
89        cfg.g_timebase.num = 1;
90        cfg.g_timebase.den = self.fps as c_int;
91        cfg.rc_target_bitrate = self.bitrate_kbps;
92        cfg.rc_min_quantizer = self.min_quantizer;
93        cfg.rc_max_quantizer = self.max_quantizer;
94        cfg.g_threads = 2;
95        cfg.g_lag_in_frames = 1;
96        cfg.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT;
97        cfg.g_pass = vpx_enc_pass::VPX_RC_ONE_PASS;
98        cfg.g_profile = self.profile;
99        cfg.rc_end_usage = vpx_rc_mode::VPX_VBR;
100        cfg.kf_max_dist = 150;
101        cfg.kf_min_dist = 150;
102        cfg.kf_mode = vpx_kf_mode::VPX_KF_AUTO;
103
104        let ctx = MaybeUninit::zeroed();
105        let mut ctx = unsafe { ctx.assume_init() };
106
107        vpx!(vpx_codec_enc_init_ver(
108            &mut ctx,
109            cfg_ptr,
110            &cfg,
111            0,
112            VPX_ENCODER_ABI_VERSION as i32
113        ));
114        unsafe {
115            vpx_codec_control_(&mut ctx, vp8e_enc_control_id::VP8E_SET_CPUUSED as c_int, 5);
116            vpx_codec_control_(
117                &mut ctx,
118                vp8e_enc_control_id::VP9E_SET_TILE_COLUMNS as c_int,
119                4,
120            );
121            vpx_codec_control_(&mut ctx, vp8e_enc_control_id::VP9E_SET_ROW_MT as c_int, 1);
122            vpx_codec_control_(
123                &mut ctx,
124                vp8e_enc_control_id::VP9E_SET_FRAME_PARALLEL_DECODING as c_int,
125                1,
126            );
127            // vpx_codec_control_(&mut ctx, vp8e_enc_control_id::VP9E_SET_AQ_MODE as c_int, 3);
128        }
129        Ok(VideoEncoder {
130            ctx,
131            cfg,
132            width: self.resolution.0,
133            height: self.resolution.1,
134        })
135    }
136}
137
138pub struct VideoEncoder {
139    ctx: vpx_codec_ctx_t,
140    cfg: vpx_codec_enc_cfg_t,
141    width: u32,
142    height: u32,
143}
144
145impl VideoEncoder {
146    pub fn update_bitrate_kbps(&mut self, bitrate: u32) -> anyhow::Result<()> {
147        self.cfg.rc_target_bitrate = bitrate;
148        vpx!(vpx_codec_enc_config_set(&mut self.ctx, &self.cfg));
149        Ok(())
150    }
151
152    pub fn encode(&mut self, pts: i64, data: &[u8]) -> anyhow::Result<Frames<'_>> {
153        let image = MaybeUninit::zeroed();
154        let mut image = unsafe { image.assume_init() };
155
156        vpx_ptr!(vpx_img_wrap(
157            &mut image,
158            vpx_img_fmt::VPX_IMG_FMT_I420,
159            self.width as _,
160            self.height as _,
161            1,
162            data.as_ptr() as _,
163        ));
164
165        let flags: i64 = 0;
166
167        vpx!(vpx_codec_encode(
168            &mut self.ctx,
169            &image,
170            pts,
171            1,     // Duration
172            flags, // Flags
173            VPX_DL_REALTIME as c_ulong,
174        ));
175
176        Ok(Frames {
177            ctx: &mut self.ctx,
178            iter: std::ptr::null(),
179        })
180    }
181}
182
183#[derive(Clone, Copy, Debug)]
184pub struct Frame<'a> {
185    /// Compressed data.
186    pub data: &'a [u8],
187    /// Whether the frame is a keyframe.
188    pub key: bool,
189    /// Presentation timestamp (in timebase units).
190    pub pts: i64,
191}
192
193pub struct Frames<'a> {
194    ctx: &'a mut vpx_codec_ctx_t,
195    iter: vpx_codec_iter_t,
196}
197
198impl<'a> Iterator for Frames<'a> {
199    type Item = Frame<'a>;
200    #[allow(clippy::unnecessary_cast)]
201    fn next(&mut self) -> Option<Self::Item> {
202        loop {
203            unsafe {
204                let pkt = vpx_codec_get_cx_data(self.ctx, &mut self.iter);
205                if pkt.is_null() {
206                    return None;
207                } else if (*pkt).kind == vpx_codec_cx_pkt_kind::VPX_CODEC_CX_FRAME_PKT {
208                    let f = &(*pkt).data.frame;
209                    return Some(Frame {
210                        data: std::slice::from_raw_parts(f.buf as _, f.sz as usize),
211                        key: (f.flags & VPX_FRAME_IS_KEY) != 0,
212                        pts: f.pts,
213                    });
214                }
215            }
216        }
217    }
218}