NetDesk 1.5.0 — source release
This commit is contained in:
commit
40318aafdd
227 files changed
+80798
No files matched your search
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "netdesk-vpx"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
description = "VP8/VP9 (libvpx) encode/decode for NetDesk — the constrained-link codec. Developed by Akronlogic."
|
||||
links = "vpx"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "=0.65.1"
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Link the vendored static libvpx (UCRT MinGW ABI) + generate FFI bindings
|
||||
//! from the vendored vpx headers via bindgen (libclang). RustDesk does the same
|
||||
//! (its vpx.rs include!s an OUT_DIR/vpx_ffi.rs). We vendor a UCRT-ABI libvpx.a
|
||||
//! so there is no vcpkg/MSYS2 or runtime DLL dependency.
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let tp = manifest.join("..").join("third_party").join("ucrt64");
|
||||
let lib = tp.join("lib");
|
||||
let inc = tp.join("include");
|
||||
|
||||
println!("cargo:rustc-link-search=native={}", lib.display());
|
||||
println!("cargo:rustc-link-lib=static=vpx");
|
||||
println!("cargo:rustc-link-lib=pthread");
|
||||
println!("cargo:rerun-if-changed=wrapper.h");
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed={}", lib.join("libvpx.a").display());
|
||||
|
||||
// bindgen needs libclang; point at the installed LLVM if the env is unset.
|
||||
if std::env::var("LIBCLANG_PATH").is_err() {
|
||||
for p in ["C:/Program Files/LLVM/bin", "C:/Program Files (x86)/LLVM/bin"] {
|
||||
if PathBuf::from(p).join("libclang.dll").exists() {
|
||||
std::env::set_var("LIBCLANG_PATH", p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Based on RustDesk's proven bindgen config (libs/scrap/build.rs): a single
|
||||
// `^[vV].*` allowlist for types/vars/functions, rustified enums.
|
||||
//
|
||||
// vpx_codec_enc_cfg and vpx_codec_dec_cfg are forward-declared (by pointer)
|
||||
// in vpx_codec.h's iface struct BEFORE vpx_encoder.h/vpx_decoder.h define
|
||||
// them. libclang 22 then reports them to bindgen as incomplete, so bindgen
|
||||
// emits them as opaque `_address: u8` blobs with no fields — making g_w /
|
||||
// rc_target_bitrate / threads unsettable. (bindgen version, trust_clang_
|
||||
// mangling, rustified_enum, and a by-value use all failed to change this.)
|
||||
// So we blocklist both here and hand-write their exact #[repr(C)] layouts
|
||||
// in src/lib.rs (ffi module) from the vendored headers. Everything else
|
||||
// (vpx_image, cx_pkt, ctx, enums, functions) still comes from bindgen.
|
||||
let regex = "^[vV].*";
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("wrapper.h")
|
||||
.clang_arg(format!("-I{}", inc.display()))
|
||||
.allowlist_type(regex)
|
||||
.allowlist_var(regex)
|
||||
.allowlist_function(regex)
|
||||
.rustified_enum(regex)
|
||||
.blocklist_type("vpx_codec_enc_cfg")
|
||||
.blocklist_type("vpx_codec_enc_cfg_t")
|
||||
.blocklist_type("vpx_codec_dec_cfg")
|
||||
.blocklist_type("vpx_codec_dec_cfg_t")
|
||||
.trust_clang_mangling(false)
|
||||
.layout_tests(false)
|
||||
.generate_comments(false)
|
||||
.generate()
|
||||
.expect("bindgen libvpx");
|
||||
bindings
|
||||
.write_to_file(PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("vpx_ffi.rs"))
|
||||
.expect("write vpx_ffi.rs");
|
||||
}
|
||||
@@ -0,0 +1,742 @@
|
||||
//! VP8/VP9 software codec over the vendored static libvpx (UCRT MinGW).
|
||||
//!
|
||||
//! The raw FFI is generated by bindgen from the vendored headers (build.rs),
|
||||
//! EXCEPT for `vpx_codec_enc_cfg` / `vpx_codec_dec_cfg`: those are forward-
|
||||
//! declared by pointer in vpx_codec.h before their definitions, which libclang
|
||||
//! reports to bindgen as opaque (`_address: u8`) blobs with no settable fields.
|
||||
//! They are blocklisted in build.rs and hand-written below with the exact
|
||||
//! `#[repr(C)]` layout from the vendored headers (ABI-identical: C `enum` and
|
||||
//! `unsigned int` are both 4 bytes, `struct vpx_rational` is two i32, and
|
||||
//! `vpx_fixed_buf_t` is `{void*, size_t}`).
|
||||
//!
|
||||
//! The safe Encoder/Decoder are ported from RustDesk's libs/scrap vpxcodec.rs
|
||||
//! (VP9 CBR realtime config, cpu-used 7, row-mt, 4 tile columns). Color
|
||||
//! conversions (BGRA<->I420, BT.601) are pure-Rust so there is no libyuv dep.
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use std::os::raw::{c_int, c_uint};
|
||||
|
||||
pub mod ffi {
|
||||
#![allow(
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
non_upper_case_globals,
|
||||
dead_code,
|
||||
improper_ctypes
|
||||
)]
|
||||
use std::os::raw::{c_int, c_uint, c_void};
|
||||
|
||||
// ---- hand-written config structs (blocklisted from bindgen) ----
|
||||
// Distinct helper names (Nd*) so they never collide with bindgen's own
|
||||
// vpx_rational / vpx_fixed_buf while staying ABI-identical.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct NdVpxRational {
|
||||
pub num: c_int,
|
||||
pub den: c_int,
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct NdVpxFixedBuf {
|
||||
pub buf: *mut c_void,
|
||||
pub sz: usize,
|
||||
}
|
||||
|
||||
/// Mirror of `vpx_codec_enc_cfg_t` (vpx_encoder.h) — 61 fields, in order.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct vpx_codec_enc_cfg {
|
||||
pub g_usage: c_uint,
|
||||
pub g_threads: c_uint,
|
||||
pub g_profile: c_uint,
|
||||
pub g_w: c_uint,
|
||||
pub g_h: c_uint,
|
||||
pub g_bit_depth: c_uint, // enum vpx_bit_depth
|
||||
pub g_input_bit_depth: c_uint,
|
||||
pub g_timebase: NdVpxRational,
|
||||
pub g_error_resilient: c_uint, // vpx_codec_er_flags_t
|
||||
pub g_pass: c_uint, // enum vpx_enc_pass
|
||||
pub g_lag_in_frames: c_uint,
|
||||
pub rc_dropframe_thresh: c_uint,
|
||||
pub rc_resize_allowed: c_uint,
|
||||
pub rc_scaled_width: c_uint,
|
||||
pub rc_scaled_height: c_uint,
|
||||
pub rc_resize_up_thresh: c_uint,
|
||||
pub rc_resize_down_thresh: c_uint,
|
||||
pub rc_end_usage: c_uint, // enum vpx_rc_mode
|
||||
pub rc_twopass_stats_in: NdVpxFixedBuf,
|
||||
pub rc_firstpass_mb_stats_in: NdVpxFixedBuf,
|
||||
pub rc_target_bitrate: c_uint,
|
||||
pub rc_min_quantizer: c_uint,
|
||||
pub rc_max_quantizer: c_uint,
|
||||
pub rc_undershoot_pct: c_uint,
|
||||
pub rc_overshoot_pct: c_uint,
|
||||
pub rc_buf_sz: c_uint,
|
||||
pub rc_buf_initial_sz: c_uint,
|
||||
pub rc_buf_optimal_sz: c_uint,
|
||||
pub rc_2pass_vbr_bias_pct: c_uint,
|
||||
pub rc_2pass_vbr_minsection_pct: c_uint,
|
||||
pub rc_2pass_vbr_maxsection_pct: c_uint,
|
||||
pub rc_2pass_vbr_corpus_complexity: c_uint,
|
||||
pub kf_mode: c_uint, // enum vpx_kf_mode
|
||||
pub kf_min_dist: c_uint,
|
||||
pub kf_max_dist: c_uint,
|
||||
pub ss_number_layers: c_uint,
|
||||
pub ss_enable_auto_alt_ref: [c_int; 5], // VPX_SS_MAX_LAYERS
|
||||
pub ss_target_bitrate: [c_uint; 5],
|
||||
pub ts_number_layers: c_uint,
|
||||
pub ts_target_bitrate: [c_uint; 5], // VPX_TS_MAX_LAYERS
|
||||
pub ts_rate_decimator: [c_uint; 5],
|
||||
pub ts_periodicity: c_uint,
|
||||
pub ts_layer_id: [c_uint; 16], // VPX_TS_MAX_PERIODICITY
|
||||
pub layer_target_bitrate: [c_uint; 12], // VPX_MAX_LAYERS
|
||||
pub temporal_layering_mode: c_int,
|
||||
pub use_vizier_rc_params: c_int,
|
||||
pub active_wq_factor: NdVpxRational,
|
||||
pub err_per_mb_factor: NdVpxRational,
|
||||
pub sr_default_decay_limit: NdVpxRational,
|
||||
pub sr_diff_factor: NdVpxRational,
|
||||
pub kf_err_per_mb_factor: NdVpxRational,
|
||||
pub kf_frame_min_boost_factor: NdVpxRational,
|
||||
pub kf_frame_max_boost_first_factor: NdVpxRational,
|
||||
pub kf_frame_max_boost_subs_factor: NdVpxRational,
|
||||
pub kf_max_total_boost_factor: NdVpxRational,
|
||||
pub gf_max_total_boost_factor: NdVpxRational,
|
||||
pub gf_frame_max_boost_factor: NdVpxRational,
|
||||
pub zm_factor: NdVpxRational,
|
||||
pub rd_mult_inter_qp_fac: NdVpxRational,
|
||||
pub rd_mult_arf_qp_fac: NdVpxRational,
|
||||
pub rd_mult_key_qp_fac: NdVpxRational,
|
||||
}
|
||||
pub type vpx_codec_enc_cfg_t = vpx_codec_enc_cfg;
|
||||
|
||||
/// Mirror of `vpx_codec_dec_cfg_t` (vpx_decoder.h).
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct vpx_codec_dec_cfg {
|
||||
pub threads: c_uint,
|
||||
pub w: c_uint,
|
||||
pub h: c_uint,
|
||||
}
|
||||
pub type vpx_codec_dec_cfg_t = vpx_codec_dec_cfg;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/vpx_ffi.rs"));
|
||||
}
|
||||
|
||||
use std::ffi::CStr;
|
||||
|
||||
/// libvpx version string, e.g. "v1.17.0".
|
||||
pub fn version() -> String {
|
||||
unsafe {
|
||||
let p = ffi::vpx_codec_version_str();
|
||||
if p.is_null() {
|
||||
return String::new();
|
||||
}
|
||||
CStr::from_ptr(p).to_string_lossy().into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// Which libvpx codec to use. VP9 is the constrained-link default (far better
|
||||
/// per-bitrate than H.264); VP8 is a fallback for very old decoders.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum VpxCodec {
|
||||
Vp8,
|
||||
Vp9,
|
||||
}
|
||||
|
||||
/// One compressed output packet.
|
||||
pub struct EncodedFrame {
|
||||
pub data: Vec<u8>,
|
||||
pub key: bool,
|
||||
}
|
||||
|
||||
/// One decoded frame, converted to packed BGRA (8-bit, straight alpha = 255).
|
||||
pub struct DecodedFrame {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub bgra: Vec<u8>,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn ck(code: i32, what: &str) -> Result<()> {
|
||||
if code == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("{what}: vpx error {code}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_num() -> c_uint {
|
||||
std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(4)
|
||||
.min(16) as c_uint
|
||||
}
|
||||
|
||||
/// VP8/VP9 encoder (software, libvpx). Input is packed BGRA; dimensions must be
|
||||
/// even. One keyframe is emitted on the first frame (or on demand).
|
||||
pub struct Encoder {
|
||||
ctx: ffi::vpx_codec_ctx_t,
|
||||
width: u32,
|
||||
height: u32,
|
||||
i420: Vec<u8>, // reusable packed plane scratch (I420 Y|U|V, or I444 Y|U|V)
|
||||
i444: bool, // true => VP9 profile-1 4:4:4 chroma (crisp text); else I420
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
/// `bitrate_kbps` is the CBR target in kilobits/sec. I420 (4:2:0) chroma.
|
||||
pub fn new(codec: VpxCodec, width: u32, height: u32, fps: u32, bitrate_kbps: u32) -> Result<Self> {
|
||||
Self::new_i444(codec, width, height, fps, bitrate_kbps, false)
|
||||
}
|
||||
|
||||
/// Like [`Encoder::new`], but `i444` selects VP9 profile-1 4:4:4 chroma
|
||||
/// (full-resolution U/V) for crisp text and saturated 1px edges that 4:2:0
|
||||
/// blurs. VP8 has no such profile, so it is ignored there (stays I420).
|
||||
pub fn new_i444(
|
||||
codec: VpxCodec,
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: u32,
|
||||
bitrate_kbps: u32,
|
||||
i444: bool,
|
||||
) -> Result<Self> {
|
||||
if width == 0 || height == 0 || width % 2 != 0 || height % 2 != 0 {
|
||||
bail!("vpx encoder needs non-zero even dimensions, got {width}x{height}");
|
||||
}
|
||||
let _ = fps; // reserved for future rc tuning; ms timebase is fps-independent
|
||||
unsafe {
|
||||
let iface = match codec {
|
||||
VpxCodec::Vp8 => ffi::vpx_codec_vp8_cx(),
|
||||
VpxCodec::Vp9 => ffi::vpx_codec_vp9_cx(),
|
||||
};
|
||||
if iface.is_null() {
|
||||
bail!("vpx codec iface null");
|
||||
}
|
||||
let mut cfg: ffi::vpx_codec_enc_cfg_t = std::mem::zeroed();
|
||||
ck(
|
||||
ffi::vpx_codec_enc_config_default(iface, &mut cfg, 0) as i32,
|
||||
"enc_config_default",
|
||||
)?;
|
||||
|
||||
// Match RustDesk's VP9 realtime CBR config.
|
||||
cfg.g_w = width;
|
||||
cfg.g_h = height;
|
||||
cfg.g_timebase.num = 1;
|
||||
cfg.g_timebase.den = 1000; // ms timestamps
|
||||
cfg.rc_undershoot_pct = 95;
|
||||
cfg.rc_dropframe_thresh = 25;
|
||||
cfg.g_threads = thread_num();
|
||||
cfg.g_lag_in_frames = 0; // realtime: no lookahead, emit every frame immediately
|
||||
cfg.g_error_resilient = 1; // VPX_ERROR_RESILIENT_DEFAULT
|
||||
cfg.rc_end_usage = 1; // VPX_CBR
|
||||
cfg.kf_mode = 0; // VPX_KF_DISABLED (we force keyframes on demand)
|
||||
cfg.rc_min_quantizer = 0;
|
||||
cfg.rc_max_quantizer = 63;
|
||||
cfg.rc_target_bitrate = bitrate_kbps.max(50);
|
||||
// VP9 profile 1 carries 8-bit 4:2:2/4:4:4; VP8 has no such profile.
|
||||
let use_i444 = i444 && codec == VpxCodec::Vp9;
|
||||
cfg.g_profile = if use_i444 { 1 } else { 0 };
|
||||
|
||||
let mut ctx: ffi::vpx_codec_ctx_t = std::mem::zeroed();
|
||||
ck(
|
||||
ffi::vpx_codec_enc_init_ver(
|
||||
&mut ctx,
|
||||
iface,
|
||||
&cfg,
|
||||
0,
|
||||
ffi::VPX_ENCODER_ABI_VERSION as c_int,
|
||||
) as i32,
|
||||
"enc_init",
|
||||
)?;
|
||||
|
||||
if codec == VpxCodec::Vp9 {
|
||||
ck(
|
||||
ffi::vpx_codec_control_(
|
||||
&mut ctx,
|
||||
ffi::vp8e_enc_control_id::VP8E_SET_CPUUSED as c_int,
|
||||
7 as c_int,
|
||||
) as i32,
|
||||
"set_cpuused",
|
||||
)?;
|
||||
ck(
|
||||
ffi::vpx_codec_control_(
|
||||
&mut ctx,
|
||||
ffi::vp8e_enc_control_id::VP9E_SET_ROW_MT as c_int,
|
||||
1 as c_int,
|
||||
) as i32,
|
||||
"set_row_mt",
|
||||
)?;
|
||||
ck(
|
||||
ffi::vpx_codec_control_(
|
||||
&mut ctx,
|
||||
ffi::vp8e_enc_control_id::VP9E_SET_TILE_COLUMNS as c_int,
|
||||
4 as c_int,
|
||||
) as i32,
|
||||
"set_tile_columns",
|
||||
)?;
|
||||
} else {
|
||||
ck(
|
||||
ffi::vpx_codec_control_(
|
||||
&mut ctx,
|
||||
ffi::vp8e_enc_control_id::VP8E_SET_CPUUSED as c_int,
|
||||
12 as c_int,
|
||||
) as i32,
|
||||
"set_cpuused_vp8",
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
ctx,
|
||||
width,
|
||||
height,
|
||||
i420: Vec::new(),
|
||||
i444: use_i444,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode one packed-BGRA frame at `pts_ms`. Returns any compressed packets
|
||||
/// the encoder produced (usually one). `force_key` requests a keyframe.
|
||||
pub fn encode_bgra(&mut self, bgra: &[u8], pts_ms: i64, force_key: bool) -> Result<Vec<EncodedFrame>> {
|
||||
let (w, h) = (self.width as usize, self.height as usize);
|
||||
if bgra.len() != w * h * 4 {
|
||||
bail!("bgra len {} != {}", bgra.len(), w * h * 4);
|
||||
}
|
||||
let fmt = if self.i444 {
|
||||
bgra_to_i444(bgra, w, h, &mut self.i420);
|
||||
ffi::vpx_img_fmt::VPX_IMG_FMT_I444
|
||||
} else {
|
||||
bgra_to_i420(bgra, w, h, &mut self.i420);
|
||||
ffi::vpx_img_fmt::VPX_IMG_FMT_I420
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let mut img: ffi::vpx_image_t = std::mem::zeroed();
|
||||
let wrapped = ffi::vpx_img_wrap(
|
||||
&mut img,
|
||||
fmt,
|
||||
self.width as c_uint,
|
||||
self.height as c_uint,
|
||||
1,
|
||||
self.i420.as_ptr() as *mut u8,
|
||||
);
|
||||
if wrapped.is_null() {
|
||||
bail!("vpx_img_wrap failed");
|
||||
}
|
||||
let flags: ffi::vpx_enc_frame_flags_t = if force_key {
|
||||
ffi::VPX_EFLAG_FORCE_KF as ffi::vpx_enc_frame_flags_t
|
||||
} else {
|
||||
0
|
||||
};
|
||||
ck(
|
||||
ffi::vpx_codec_encode(
|
||||
&mut self.ctx,
|
||||
&img,
|
||||
pts_ms as ffi::vpx_codec_pts_t,
|
||||
1,
|
||||
flags,
|
||||
ffi::VPX_DL_REALTIME as _,
|
||||
) as i32,
|
||||
"encode",
|
||||
)?;
|
||||
Ok(self.drain())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn drain(&mut self) -> Vec<EncodedFrame> {
|
||||
let mut iter: ffi::vpx_codec_iter_t = std::ptr::null();
|
||||
let mut out = Vec::new();
|
||||
loop {
|
||||
let pkt = ffi::vpx_codec_get_cx_data(&mut self.ctx, &mut iter);
|
||||
if pkt.is_null() {
|
||||
break;
|
||||
}
|
||||
if (*pkt).kind == ffi::vpx_codec_cx_pkt_kind::VPX_CODEC_CX_FRAME_PKT {
|
||||
let f = &(*pkt).data.frame;
|
||||
let data = std::slice::from_raw_parts(f.buf as *const u8, f.sz as usize).to_vec();
|
||||
let key = (f.flags & ffi::VPX_FRAME_IS_KEY) != 0;
|
||||
out.push(EncodedFrame { data, key });
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Encoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
ffi::vpx_codec_destroy(&mut self.ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A libvpx context holds raw pointers, so it is not auto-Send. It is never
|
||||
// shared (no Sync), but a single owner may move it between threads — the host
|
||||
// capture/encode thread owns the Encoder, the viewer network thread owns the
|
||||
// Decoder. This matches RustDesk's VpxEncoder/VpxDecoder.
|
||||
unsafe impl Send for Encoder {}
|
||||
unsafe impl Send for Decoder {}
|
||||
|
||||
/// VP8/VP9 decoder (software, libvpx). Produces packed BGRA frames.
|
||||
pub struct Decoder {
|
||||
ctx: ffi::vpx_codec_ctx_t,
|
||||
}
|
||||
|
||||
impl Decoder {
|
||||
pub fn new(codec: VpxCodec) -> Result<Self> {
|
||||
unsafe {
|
||||
let iface = match codec {
|
||||
VpxCodec::Vp8 => ffi::vpx_codec_vp8_dx(),
|
||||
VpxCodec::Vp9 => ffi::vpx_codec_vp9_dx(),
|
||||
};
|
||||
if iface.is_null() {
|
||||
bail!("vpx decoder iface null");
|
||||
}
|
||||
let cfg = ffi::vpx_codec_dec_cfg_t {
|
||||
threads: thread_num(),
|
||||
w: 0,
|
||||
h: 0,
|
||||
};
|
||||
let mut ctx: ffi::vpx_codec_ctx_t = std::mem::zeroed();
|
||||
ck(
|
||||
ffi::vpx_codec_dec_init_ver(
|
||||
&mut ctx,
|
||||
iface,
|
||||
&cfg,
|
||||
0,
|
||||
ffi::VPX_DECODER_ABI_VERSION as c_int,
|
||||
) as i32,
|
||||
"dec_init",
|
||||
)?;
|
||||
Ok(Self { ctx })
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode one compressed packet into zero or more BGRA frames.
|
||||
pub fn decode_to_bgra(&mut self, data: &[u8]) -> Result<Vec<DecodedFrame>> {
|
||||
unsafe {
|
||||
ck(
|
||||
ffi::vpx_codec_decode(
|
||||
&mut self.ctx,
|
||||
data.as_ptr(),
|
||||
data.len() as c_uint,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
) as i32,
|
||||
"decode",
|
||||
)?;
|
||||
let mut iter: ffi::vpx_codec_iter_t = std::ptr::null();
|
||||
let mut out = Vec::new();
|
||||
loop {
|
||||
let img = ffi::vpx_codec_get_frame(&mut self.ctx, &mut iter);
|
||||
if img.is_null() {
|
||||
break;
|
||||
}
|
||||
let im = &*img;
|
||||
let w = im.d_w as usize;
|
||||
let h = im.d_h as usize;
|
||||
// The VP9 decoder adapts to the stream's profile: profile-1
|
||||
// frames arrive as I444 (full-res chroma), profile-0 as I420.
|
||||
// One Decoder therefore handles either without reconfiguration.
|
||||
let bgra = if im.fmt == ffi::vpx_img_fmt::VPX_IMG_FMT_I444 {
|
||||
i444_to_bgra(
|
||||
im.planes[0] as *const u8,
|
||||
im.planes[1] as *const u8,
|
||||
im.planes[2] as *const u8,
|
||||
im.stride[0] as usize,
|
||||
im.stride[1] as usize,
|
||||
w,
|
||||
h,
|
||||
)
|
||||
} else {
|
||||
i420_to_bgra(
|
||||
im.planes[0] as *const u8,
|
||||
im.planes[1] as *const u8,
|
||||
im.planes[2] as *const u8,
|
||||
im.stride[0] as usize,
|
||||
im.stride[1] as usize,
|
||||
w,
|
||||
h,
|
||||
)
|
||||
};
|
||||
out.push(DecodedFrame {
|
||||
width: im.d_w,
|
||||
height: im.d_h,
|
||||
bgra,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Decoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
ffi::vpx_codec_destroy(&mut self.ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- pure-Rust BT.601 color conversion (no libyuv) ----
|
||||
|
||||
/// Packed BGRA -> packed I420 (Y plane w*h, then U, then V at cw*ch). `w`,`h`
|
||||
/// must be even. Output is laid out exactly as vpx_img_wrap(I420, align=1)
|
||||
/// expects for even dimensions.
|
||||
fn bgra_to_i420(bgra: &[u8], w: usize, h: usize, out: &mut Vec<u8>) {
|
||||
let cw = w / 2;
|
||||
let ch = h / 2;
|
||||
let y_size = w * h;
|
||||
let c_size = cw * ch;
|
||||
out.clear();
|
||||
out.resize(y_size + 2 * c_size, 0);
|
||||
let (y_plane, rest) = out.split_at_mut(y_size);
|
||||
let (u_plane, v_plane) = rest.split_at_mut(c_size);
|
||||
|
||||
for j in 0..h {
|
||||
for i in 0..w {
|
||||
let p = (j * w + i) * 4;
|
||||
let b = bgra[p] as f32;
|
||||
let g = bgra[p + 1] as f32;
|
||||
let r = bgra[p + 2] as f32;
|
||||
let y = 0.257 * r + 0.504 * g + 0.098 * b + 16.0;
|
||||
y_plane[j * w + i] = clamp8(y);
|
||||
}
|
||||
}
|
||||
for cj in 0..ch {
|
||||
for ci in 0..cw {
|
||||
let mut rs = 0.0f32;
|
||||
let mut gs = 0.0f32;
|
||||
let mut bs = 0.0f32;
|
||||
for dy in 0..2 {
|
||||
for dx in 0..2 {
|
||||
let p = ((cj * 2 + dy) * w + (ci * 2 + dx)) * 4;
|
||||
bs += bgra[p] as f32;
|
||||
gs += bgra[p + 1] as f32;
|
||||
rs += bgra[p + 2] as f32;
|
||||
}
|
||||
}
|
||||
let (r, g, b) = (rs / 4.0, gs / 4.0, bs / 4.0);
|
||||
let u = -0.148 * r - 0.291 * g + 0.439 * b + 128.0;
|
||||
let v = 0.439 * r - 0.368 * g - 0.071 * b + 128.0;
|
||||
u_plane[cj * cw + ci] = clamp8(u);
|
||||
v_plane[cj * cw + ci] = clamp8(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// I420 planes (with strides) -> packed BGRA.
|
||||
unsafe fn i420_to_bgra(
|
||||
y: *const u8,
|
||||
u: *const u8,
|
||||
v: *const u8,
|
||||
ys: usize,
|
||||
cs: usize,
|
||||
w: usize,
|
||||
h: usize,
|
||||
) -> Vec<u8> {
|
||||
let mut out = vec![0u8; w * h * 4];
|
||||
for j in 0..h {
|
||||
for i in 0..w {
|
||||
let yv = *y.add(j * ys + i) as f32;
|
||||
let uu = *u.add((j / 2) * cs + i / 2) as f32;
|
||||
let vv = *v.add((j / 2) * cs + i / 2) as f32;
|
||||
let c = yv - 16.0;
|
||||
let d = uu - 128.0;
|
||||
let e = vv - 128.0;
|
||||
let r = 1.164 * c + 1.596 * e;
|
||||
let g = 1.164 * c - 0.391 * d - 0.813 * e;
|
||||
let b = 1.164 * c + 2.018 * d;
|
||||
let p = (j * w + i) * 4;
|
||||
out[p] = clamp8(b);
|
||||
out[p + 1] = clamp8(g);
|
||||
out[p + 2] = clamp8(r);
|
||||
out[p + 3] = 255;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Packed BGRA -> packed I444 (Y|U|V, each w*h at full resolution). Unlike
|
||||
/// I420 the chroma planes keep every pixel's own U/V, so 4:4:4 preserves 1px
|
||||
/// colored text and hard edges that 4:2:0 averages away. Layout matches
|
||||
/// vpx_img_wrap(I444, align=1).
|
||||
fn bgra_to_i444(bgra: &[u8], w: usize, h: usize, out: &mut Vec<u8>) {
|
||||
let n = w * h;
|
||||
out.clear();
|
||||
out.resize(3 * n, 0);
|
||||
let (y_plane, rest) = out.split_at_mut(n);
|
||||
let (u_plane, v_plane) = rest.split_at_mut(n);
|
||||
for idx in 0..n {
|
||||
let p = idx * 4;
|
||||
let b = bgra[p] as f32;
|
||||
let g = bgra[p + 1] as f32;
|
||||
let r = bgra[p + 2] as f32;
|
||||
y_plane[idx] = clamp8(0.257 * r + 0.504 * g + 0.098 * b + 16.0);
|
||||
u_plane[idx] = clamp8(-0.148 * r - 0.291 * g + 0.439 * b + 128.0);
|
||||
v_plane[idx] = clamp8(0.439 * r - 0.368 * g - 0.071 * b + 128.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// I444 planes (full-res chroma, with strides) -> packed BGRA.
|
||||
unsafe fn i444_to_bgra(
|
||||
y: *const u8,
|
||||
u: *const u8,
|
||||
v: *const u8,
|
||||
ys: usize,
|
||||
cs: usize,
|
||||
w: usize,
|
||||
h: usize,
|
||||
) -> Vec<u8> {
|
||||
let mut out = vec![0u8; w * h * 4];
|
||||
for j in 0..h {
|
||||
for i in 0..w {
|
||||
let yv = *y.add(j * ys + i) as f32;
|
||||
let uu = *u.add(j * cs + i) as f32;
|
||||
let vv = *v.add(j * cs + i) as f32;
|
||||
let c = yv - 16.0;
|
||||
let d = uu - 128.0;
|
||||
let e = vv - 128.0;
|
||||
let p = (j * w + i) * 4;
|
||||
out[p] = clamp8(1.164 * c + 2.018 * d);
|
||||
out[p + 1] = clamp8(1.164 * c - 0.391 * d - 0.813 * e);
|
||||
out[p + 2] = clamp8(1.164 * c + 1.596 * e);
|
||||
out[p + 3] = 255;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clamp8(v: f32) -> u8 {
|
||||
v.round().clamp(0.0, 255.0) as u8
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bindgen_links_libvpx() {
|
||||
let v = version();
|
||||
assert!(v.starts_with('v'), "libvpx version via bindgen FFI: {v}");
|
||||
let iface = unsafe { ffi::vpx_codec_vp9_cx() };
|
||||
assert!(!iface.is_null(), "vp9 encoder iface");
|
||||
println!("VPX_BINDGEN_OK {v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vp9_roundtrip_bgra() {
|
||||
let (w, h) = (64usize, 64usize);
|
||||
let mut bgra = vec![0u8; w * h * 4];
|
||||
// four solid quadrants (BGRA order)
|
||||
for j in 0..h {
|
||||
for i in 0..w {
|
||||
let p = (j * w + i) * 4;
|
||||
let (b, g, r) = if j < h / 2 && i < w / 2 {
|
||||
(30u8, 30, 200) // red
|
||||
} else if j < h / 2 {
|
||||
(30, 200, 30) // green
|
||||
} else if i < w / 2 {
|
||||
(200, 30, 30) // blue
|
||||
} else {
|
||||
(200, 200, 200) // gray
|
||||
};
|
||||
bgra[p] = b;
|
||||
bgra[p + 1] = g;
|
||||
bgra[p + 2] = r;
|
||||
bgra[p + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
let mut enc = Encoder::new(VpxCodec::Vp9, w as u32, h as u32, 30, 2000).unwrap();
|
||||
let frames = enc.encode_bgra(&bgra, 0, true).unwrap();
|
||||
assert!(!frames.is_empty(), "encoder produced no packets");
|
||||
assert!(frames[0].key, "first frame must be a keyframe");
|
||||
|
||||
let mut dec = Decoder::new(VpxCodec::Vp9).unwrap();
|
||||
let mut got = Vec::new();
|
||||
for f in &frames {
|
||||
got.extend(dec.decode_to_bgra(&f.data).unwrap());
|
||||
}
|
||||
assert_eq!(got.len(), 1, "expected exactly one decoded frame");
|
||||
let img = &got[0];
|
||||
assert_eq!((img.width, img.height), (w as u32, h as u32));
|
||||
|
||||
let sample = |x: usize, y: usize| {
|
||||
let p = (y * w + x) * 4;
|
||||
(img.bgra[p], img.bgra[p + 1], img.bgra[p + 2])
|
||||
};
|
||||
let approx = |a: u8, b: u8| (a as i32 - b as i32).abs() <= 48;
|
||||
|
||||
let (b, g, r) = sample(w / 4, h / 4); // red quadrant center
|
||||
assert!(
|
||||
approx(r, 200) && approx(g, 30) && approx(b, 30),
|
||||
"red quad decoded as b{b} g{g} r{r}"
|
||||
);
|
||||
let (b, g, r) = sample(3 * w / 4, 3 * h / 4); // gray quadrant center
|
||||
assert!(
|
||||
approx(r, 200) && approx(g, 200) && approx(b, 200),
|
||||
"gray quad decoded as b{b} g{g} r{r}"
|
||||
);
|
||||
|
||||
println!(
|
||||
"VPX_ROUNDTRIP_OK vp9 {w}x{h} key_bytes={}",
|
||||
frames[0].data.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vp9_i444_beats_i420_on_1px_chroma() {
|
||||
// 1px-wide vertical stripes of two DIFFERENT-chroma colors (magenta /
|
||||
// green). 4:2:0 stores one chroma sample per 2x2 block, so it CANNOT
|
||||
// represent per-column chroma no matter the bitrate; 4:4:4 keeps it.
|
||||
let (w, h) = (64usize, 64usize);
|
||||
let mut bgra = vec![0u8; w * h * 4];
|
||||
for j in 0..h {
|
||||
for i in 0..w {
|
||||
let p = (j * w + i) * 4;
|
||||
let (b, g, r) = if i % 2 == 0 {
|
||||
(200u8, 0, 200) // magenta
|
||||
} else {
|
||||
(0, 170, 0) // green
|
||||
};
|
||||
bgra[p] = b;
|
||||
bgra[p + 1] = g;
|
||||
bgra[p + 2] = r;
|
||||
bgra[p + 3] = 255;
|
||||
}
|
||||
}
|
||||
let mae = |i444: bool| -> f64 {
|
||||
let mut enc =
|
||||
Encoder::new_i444(VpxCodec::Vp9, w as u32, h as u32, 30, 4000, i444).unwrap();
|
||||
let frames = enc.encode_bgra(&bgra, 0, true).unwrap();
|
||||
let mut dec = Decoder::new(VpxCodec::Vp9).unwrap();
|
||||
let mut got = Vec::new();
|
||||
for f in &frames {
|
||||
got.extend(dec.decode_to_bgra(&f.data).unwrap());
|
||||
}
|
||||
assert_eq!(got.len(), 1);
|
||||
let img = &got[0];
|
||||
let mut e = 0f64;
|
||||
for idx in 0..w * h {
|
||||
let p = idx * 4;
|
||||
e += (img.bgra[p] as i32 - bgra[p] as i32).abs() as f64;
|
||||
e += (img.bgra[p + 1] as i32 - bgra[p + 1] as i32).abs() as f64;
|
||||
e += (img.bgra[p + 2] as i32 - bgra[p + 2] as i32).abs() as f64;
|
||||
}
|
||||
e / (3 * w * h) as f64
|
||||
};
|
||||
let e420 = mae(false);
|
||||
let e444 = mae(true);
|
||||
println!("VPX_I444_OK e420={e420:.2} e444={e444:.2}");
|
||||
assert!(
|
||||
e444 + 1.0 < e420,
|
||||
"i444 MAE {e444:.2} should clearly beat i420 {e420:.2} on 1px chroma"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#include <vpx/vp8.h>
|
||||
#include <vpx/vp8cx.h>
|
||||
#include <vpx/vp8dx.h>
|
||||
#include <vpx/vpx_codec.h>
|
||||
#include <vpx/vpx_decoder.h>
|
||||
#include <vpx/vpx_encoder.h>
|
||||
#include <vpx/vpx_frame_buffer.h>
|
||||
#include <vpx/vpx_image.h>
|
||||
#include <vpx/vpx_integer.h>
|
||||
Reference in new issue
Block a user