Skip to main content

videocall_codecs/
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::Result;
20use std::ffi::{c_int, CStr};
21use std::mem::MaybeUninit;
22use std::os::raw::c_char;
23use vpx_sys::*;
24
25const FPS: u64 = 30;
26
27// --- VP9 Encoder Implementation (inspired by videocall-rs) ---
28
29/// A safe wrapper for the vpx_codec_ctx_t
30pub struct Vp9Encoder {
31    ctx: vpx_codec_ctx_t,
32    pub width: u32,
33    pub height: u32,
34}
35
36// These are necessary because the context contains raw pointers.
37// We are guaranteeing that we are using the encoder in a thread-safe way.
38unsafe impl Send for Vp9Encoder {}
39unsafe impl Sync for Vp9Encoder {}
40
41/// Helper to convert C strings to Rust Strings for error messages.
42fn c_str_to_rust_str(c_str_ptr: *const c_char) -> String {
43    if c_str_ptr.is_null() {
44        return "Unknown error".to_string();
45    }
46    unsafe { CStr::from_ptr(c_str_ptr).to_string_lossy().into_owned() }
47}
48
49impl Vp9Encoder {
50    pub fn new(width: u32, height: u32, bitrate_kbps: u32) -> Result<Self> {
51        unsafe {
52            let mut cfg: vpx_codec_enc_cfg_t = MaybeUninit::zeroed().assume_init();
53            let ret = vpx_codec_enc_config_default(vpx_codec_vp9_cx(), &mut cfg, 0);
54            if ret != VPX_CODEC_OK {
55                anyhow::bail!("Failed to get default VP9 encoder config");
56            }
57
58            cfg.g_w = width;
59            cfg.g_h = height;
60            cfg.g_timebase.num = 1;
61            cfg.g_timebase.den = FPS as c_int;
62            cfg.rc_target_bitrate = bitrate_kbps;
63            cfg.rc_end_usage = vpx_rc_mode::VPX_VBR;
64            // Keyframe settings can be important for streaming
65            cfg.kf_max_dist = 150;
66            cfg.kf_min_dist = 150;
67            cfg.kf_mode = vpx_kf_mode::VPX_KF_AUTO;
68
69            let mut ctx: vpx_codec_ctx_t = MaybeUninit::zeroed().assume_init();
70            let ret = vpx_codec_enc_init_ver(
71                &mut ctx,
72                vpx_codec_vp9_cx(),
73                &cfg,
74                0,
75                VPX_ENCODER_ABI_VERSION as i32,
76            );
77            if ret != VPX_CODEC_OK {
78                let err_msg = c_str_to_rust_str(vpx_codec_err_to_string(ret));
79                anyhow::bail!("Failed to initialize VP9 encoder: {err_msg}");
80            }
81
82            Ok(Vp9Encoder { ctx, width, height })
83        }
84    }
85
86    pub fn encode(&mut self, frame_count: i64, yuv_data: Option<&[u8]>) -> Result<Frames<'_>> {
87        unsafe {
88            let image_ptr = if let Some(data) = yuv_data {
89                let mut image: vpx_image_t = MaybeUninit::zeroed().assume_init();
90                vpx_img_wrap(
91                    &mut image,
92                    vpx_img_fmt::VPX_IMG_FMT_I420,
93                    self.width,
94                    self.height,
95                    1,
96                    data.as_ptr() as *mut u8,
97                );
98                &image as *const _
99            } else {
100                // This is the flush call.
101                std::ptr::null()
102            };
103
104            let ret = vpx_codec_encode(
105                &mut self.ctx,
106                image_ptr,
107                frame_count, // pts
108                1,           // duration
109                0,
110                VPX_DL_REALTIME as u64,
111            );
112            if ret != VPX_CODEC_OK {
113                let err_msg = c_str_to_rust_str(vpx_codec_error(&mut self.ctx as *mut _));
114                anyhow::bail!("Failed to encode frame: {err_msg:?}");
115            }
116
117            Ok(Frames {
118                ctx: &mut self.ctx,
119                iter: std::ptr::null(),
120            })
121        }
122    }
123
124    pub fn width(&self) -> u32 {
125        self.width
126    }
127
128    pub fn height(&self) -> u32 {
129        self.height
130    }
131}
132
133impl Drop for Vp9Encoder {
134    fn drop(&mut self) {
135        unsafe {
136            vpx_codec_destroy(&mut self.ctx);
137        }
138    }
139}
140
141// --- Frame iterator structures (from videocall-cli) ---
142
143#[derive(Clone, Copy, Debug)]
144pub struct Frame<'a> {
145    /// Compressed data.
146    pub data: &'a [u8],
147    /// Whether the frame is a keyframe.
148    pub key: bool,
149    /// Presentation timestamp (in timebase units).
150    pub pts: i64,
151}
152
153pub struct Frames<'a> {
154    ctx: &'a mut vpx_codec_ctx_t,
155    iter: vpx_codec_iter_t,
156}
157
158impl<'a> Iterator for Frames<'a> {
159    type Item = Frame<'a>;
160    #[allow(clippy::unnecessary_cast)]
161    fn next(&mut self) -> Option<Self::Item> {
162        loop {
163            unsafe {
164                let pkt = vpx_codec_get_cx_data(self.ctx, &mut self.iter);
165                if pkt.is_null() {
166                    return None;
167                } else if (*pkt).kind == vpx_codec_cx_pkt_kind::VPX_CODEC_CX_FRAME_PKT {
168                    let f = &(*pkt).data.frame;
169                    return Some(Frame {
170                        data: std::slice::from_raw_parts(f.buf as _, f.sz as usize),
171                        key: (f.flags & VPX_FRAME_IS_KEY) != 0,
172                        pts: f.pts,
173                    });
174                }
175            }
176        }
177    }
178}