feat(codec): use SVT-AV1 for AV1 encoding on 64-bit targets

Encode AV1 with SVT-AV1 v4.2.0 (rtc + low delay + CBR) instead of libaom
on all 64-bit targets; 32-bit targets keep aom. Decoding is unchanged and
the wire format is still AV1, so old peers are unaffected.

- vcpkg overlay port pinned to v4.2.0: static build, LTO off (mixed
  toolchain safety), MSVC >= 1950 inline workaround, no forced llvm
  binutils
- low delay contract: one packet out per picture in, blocking get_packet
- bitrate and frame rate change on the fly via RATE_CHANGE_EVENT /
  FRAME_RATE_CHANGE_EVENT; VideoQoS pushes its pacing rate through the
  new EncoderApi::set_fps, applied only after a successful send
- fall back to aom for i444 (svt-av1 is 4:2:0 only), for unsupported
  resolutions and on encoder init failure
- test_av1 now gates auto AV1 on the encoder actually used, the cached
  verdict is re-keyed (SVT-Y/SVT-N) so upgrades re-measure
- unit tests: per-frame packet contract, aom cross-decode of the
  bitstream, on-the-fly event acceptance

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wf7v1KJM6mbhiYxWSz4PH
This commit is contained in:
rustdesk
2026-07-23 01:50:57 +08:00
co-authored by Claude Fable 5
parent 1c2dd71891
commit 1ac684db0d
11 changed files with 770 additions and 29 deletions
+57 -16
View File
@@ -4,19 +4,48 @@ use std::{
println,
};
/// The static library name may differ from the package name.
fn link_lib_name(package: &str) -> &str {
match package {
"svt-av1" => "SvtAv1Enc",
_ => package.trim_start_matches("lib"),
}
}
#[cfg(all(target_os = "linux", feature = "linux-pkg-config"))]
fn link_pkg_config(name: &str) -> Vec<PathBuf> {
// sometimes an override is needed
let pc_name = match name {
"libvpx" => "vpx",
"svt-av1" => "SvtAv1Enc",
_ => name,
};
let lib = pkg_config::probe_library(pc_name)
let mut pc = pkg_config::Config::new();
if name == "svt-av1" {
// svt_av1.rs requires the 4.x API (rtc, PredStructure, on-the-fly rate events)
pc.atleast_version("4.2");
}
let pkg_hint = match name {
"svt-av1" => "libsvt-av1-dev (needs svt-av1 >= 4.2)".to_owned(),
_ => format!("{pc_name}-dev"),
};
let lib = pc.probe(pc_name)
.expect(format!(
"unable to find '{pc_name}' development headers with pkg-config (feature linux-pkg-config is enabled).
try installing '{pc_name}-dev' from your system package manager.").as_str());
try installing '{pkg_hint}' from your system package manager.").as_str());
lib.include_paths
let mut include_paths = lib.include_paths;
// SvtAv1Enc.pc's Cflags is -I${includedir}/svt-av1, but svt_av1_ffi.h uses
// #include <svt-av1/...>, so the parent include root is needed too.
if name == "svt-av1" {
let parents: Vec<PathBuf> = include_paths
.iter()
.filter(|p| p.ends_with("svt-av1"))
.filter_map(|p| p.parent().map(|p| p.to_path_buf()))
.collect();
include_paths.extend(parents);
}
include_paths
}
#[cfg(not(all(target_os = "linux", feature = "linux-pkg-config")))]
fn link_pkg_config(_name: &str) -> Vec<PathBuf> {
@@ -61,10 +90,7 @@ fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf {
path.push("installed");
}
path.push(target);
println!(
"cargo:rustc-link-lib=static={}",
name.trim_start_matches("lib")
);
println!("cargo:rustc-link-lib=static={}", link_lib_name(name));
println!(
"cargo:rustc-link-search={}",
path.join("lib").to_str().unwrap()
@@ -103,11 +129,13 @@ fn link_homebrew_m1(name: &str) -> PathBuf {
);
}
path.push(directories.pop().unwrap());
// Link the library.
println!(
"cargo:rustc-link-lib=static={}",
name.trim_start_matches("lib")
);
// Link the library. Homebrew's svt-av1 bottle only ships a dylib.
if name == "svt-av1" {
println!("cargo:rustc-link-lib={}", link_lib_name(name));
println!("cargo:warning=svt-av1 is linked dynamically from homebrew, the binary needs homebrew's svt-av1 at runtime and is not relocatable; set VCPKG_ROOT for a static build");
} else {
println!("cargo:rustc-link-lib=static={}", link_lib_name(name));
}
// Add the library path.
println!(
"cargo:rustc-link-search={}",
@@ -120,8 +148,9 @@ fn link_homebrew_m1(name: &str) -> PathBuf {
}
/// Find package. By default, it will try to find vcpkg first, then homebrew(currently only for Mac M1).
/// If building for linux and feature "linux-pkg-config" is enabled, will try to use pkg-config
/// unless check fails (e.g. NO_PKG_CONFIG_libyuv=1)
/// If building on a linux host and feature "linux-pkg-config" is enabled, will try to use pkg-config
/// unless check fails (e.g. NO_PKG_CONFIG_libyuv=1). Note the cfg! below sees the build host, not
/// the target, so cross builds (e.g. linux -> android) must not enable linux-pkg-config.
fn find_package(name: &str) -> Vec<PathBuf> {
let no_pkg_config_var_name = format!("NO_PKG_CONFIG_{name}");
println!("cargo:rerun-if-env-changed={no_pkg_config_var_name}");
@@ -170,9 +199,9 @@ fn gen_vcpkg_package(package: &str, ffi_header: &str, generated: &str, regex: &s
let out_dir = Path::new(&out_dir);
let ffi_header = src_dir.join("src").join("bindings").join(ffi_header);
println!("rerun-if-changed={}", ffi_header.display());
println!("cargo:rerun-if-changed={}", ffi_header.display());
for dir in &includes {
println!("rerun-if-changed={}", dir.display());
println!("cargo:rerun-if-changed={}", dir.display());
}
let ffi_rs = out_dir.join(generated);
@@ -248,6 +277,18 @@ fn main() {
gen_vcpkg_package("libvpx", "vpx_ffi.h", "vpx_ffi.rs", "^[vV].*");
gen_vcpkg_package("aom", "aom_ffi.h", "aom_ffi.rs", "^(aom|AOM|OBU|AV1).*");
gen_vcpkg_package("libyuv", "yuv_ffi.h", "yuv_ffi.rs", ".*");
// Skipped on 32-bit targets: svt-av1 does not support them, the vcpkg
// manifest does not install it there and disable_av1() never uses AV1 on
// them anyway. Must stay in sync with the cfg gates in
// mod.rs/codec.rs/video_service.rs.
if env::var("CARGO_CFG_TARGET_POINTER_WIDTH").as_deref() == Ok("64") {
gen_vcpkg_package(
"svt-av1",
"svt_av1_ffi.h",
"svt_av1_ffi.rs",
"^(svt_av1_|Svt|SVT_|Eb|EB_|PredStructure|PrivDataType).*",
);
}
// ffmpeg();
if target_os == "ios" {
+5
View File
@@ -0,0 +1,5 @@
#include <svt-av1/EbSvtAv1.h>
#include <svt-av1/EbSvtAv1Enc.h>
#include <svt-av1/EbSvtAv1ErrorCodes.h>
#include <svt-av1/EbSvtAv1Formats.h>
#include <svt-av1/EbSvtAv1Metadata.h>
+80 -7
View File
@@ -9,6 +9,8 @@ use std::{
use crate::hwcodec::*;
#[cfg(feature = "mediacodec")]
use crate::mediacodec::{MediaCodecDecoder, H264_DECODER_SUPPORT, H265_DECODER_SUPPORT};
#[cfg(target_pointer_width = "64")]
use crate::svt_av1::{SvtAv1Encoder, SvtAv1EncoderConfig};
#[cfg(feature = "vram")]
use crate::vram::*;
use crate::{
@@ -51,6 +53,8 @@ pub const ENCODE_NEED_SWITCH: &'static str = "ENCODE_NEED_SWITCH";
pub enum EncoderCfg {
VPX(VpxEncoderConfig),
AOM(AomEncoderConfig),
#[cfg(target_pointer_width = "64")]
SVTAV1(SvtAv1EncoderConfig),
#[cfg(feature = "hwcodec")]
HWRAM(HwRamEncoderConfig),
#[cfg(feature = "vram")]
@@ -71,6 +75,11 @@ pub trait EncoderApi {
fn set_quality(&mut self, ratio: f32) -> ResultType<()>;
/// Inform the encoder of the current pacing rate. Only rate controls that
/// budget per frame from a configured frame rate need to override the
/// default no-op.
fn set_fps(&mut self, _fps: u32) {}
fn bitrate(&self) -> u32;
fn support_changing_quality(&self) -> bool;
@@ -140,6 +149,25 @@ impl Encoder {
EncoderCfg::AOM(_) => Ok(Encoder {
codec: Box::new(AomEncoder::new(config, i444)?),
}),
#[cfg(target_pointer_width = "64")]
EncoderCfg::SVTAV1(svt) => match SvtAv1Encoder::new(EncoderCfg::SVTAV1(svt), i444) {
Ok(codec) => Ok(Encoder {
codec: Box::new(codec),
}),
Err(e) => {
// Same wire format, aom keeps the negotiated AV1 session alive.
log::error!("new svt-av1 encoder failed: {e:?}, fallback to aom");
let aom = EncoderCfg::AOM(AomEncoderConfig {
width: svt.width,
height: svt.height,
quality: svt.quality,
keyframe_interval: svt.keyframe_interval,
});
Ok(Encoder {
codec: Box::new(AomEncoder::new(aom, i444)?),
})
}
},
#[cfg(feature = "hwcodec")]
EncoderCfg::HWRAM(_) => match HwRamEncoder::new(config, i444) {
@@ -269,7 +297,10 @@ impl Encoder {
let preference = most_frequent.enum_value_or(PreferCodec::Auto);
// auto: h265 > h264 > av1/vp9/vp8
let av1_test = Config::get_option(hbb_common::config::keys::OPTION_AV1_TEST) != "N";
let av1_test = {
let v = Config::get_option(hbb_common::config::keys::OPTION_AV1_TEST);
v != "N" && v != "SVT-N"
};
let mut auto_codec = if av1_useable && av1_test {
CodecFormat::AV1
} else {
@@ -365,6 +396,8 @@ impl Encoder {
VpxVideoCodecId::VP9 => CodecFormat::VP9,
},
EncoderCfg::AOM(_) => CodecFormat::AV1,
#[cfg(target_pointer_width = "64")]
EncoderCfg::SVTAV1(_) => CodecFormat::AV1,
#[cfg(feature = "hwcodec")]
EncoderCfg::HWRAM(hw) => {
let name = hw.name.to_lowercase();
@@ -411,6 +444,10 @@ impl Encoder {
VpxVideoCodecId::VP9 => decodings.iter().all(|d| d.1.i444.vp9),
},
EncoderCfg::AOM(_) => decodings.iter().all(|d| d.1.i444.av1),
// Mirrors AOM so an i444 preference change still triggers the encoder
// rebuild, the rebuild then selects aom for the actual i444 encoding.
#[cfg(target_pointer_width = "64")]
EncoderCfg::SVTAV1(_) => decodings.iter().all(|d| d.1.i444.av1),
#[cfg(feature = "hwcodec")]
EncoderCfg::HWRAM(_) => false,
#[cfg(feature = "vram")]
@@ -1042,7 +1079,15 @@ pub fn test_av1() {
use hbb_common::rand::Rng;
use std::{sync::Once, time::Duration};
if disable_av1() || !Config::get_option(OPTION_AV1_TEST).is_empty() {
// The verdict is tied to the encoder generation that produced it, so
// switching the AV1 encoder (aom <-> svt-av1) re-runs the test.
#[cfg(target_pointer_width = "64")]
const AV1_TEST_RESULT: (&str, &str) = ("SVT-Y", "SVT-N");
#[cfg(not(target_pointer_width = "64"))]
const AV1_TEST_RESULT: (&str, &str) = ("Y", "N");
let cached = Config::get_option(OPTION_AV1_TEST);
if disable_av1() || cached == AV1_TEST_RESULT.0 || cached == AV1_TEST_RESULT.1 {
log::info!("skip test av1");
return;
}
@@ -1101,15 +1146,38 @@ pub fn test_av1() {
}
Ok(dst)
};
let Ok(mut av1) = AomEncoder::new(
EncoderCfg::AOM(AomEncoderConfig {
let aom = || {
AomEncoder::new(
EncoderCfg::AOM(AomEncoderConfig {
width,
height,
quality,
keyframe_interval,
}),
i444,
)
.map(|e| Box::new(e) as Box<dyn EncoderApi>)
};
// Test the encoder actually used for AV1, svt-av1 on 64-bit targets, with
// the same fallback to aom as Encoder::new so an svt init failure
// does not brand a working aom as unusable. Note that i444 (true
// color) sessions and svt-unsupported resolutions still run aom,
// whose speed is then not covered by this gate.
#[cfg(target_pointer_width = "64")]
let av1 = crate::svt_av1::SvtAv1Encoder::new(
EncoderCfg::SVTAV1(crate::svt_av1::SvtAv1EncoderConfig {
width,
height,
quality,
keyframe_interval,
}),
i444,
) else {
)
.map(|e| Box::new(e) as Box<dyn EncoderApi>)
.or_else(|_| aom());
#[cfg(not(target_pointer_width = "64"))]
let av1 = aom();
let Ok(mut av1) = av1 else {
return false;
};
let mut key_frame_time = Duration::ZERO;
@@ -1122,7 +1190,7 @@ pub fn test_av1() {
};
let start = Instant::now();
if av1
.encode(pts.elapsed().as_millis() as _, &yuv, super::STRIDE_ALIGN)
.encode_to_message(EncodeInput::YUV(&yuv), pts.elapsed().as_millis() as _)
.is_err()
{
log::debug!("av1 encode failed");
@@ -1150,7 +1218,12 @@ pub fn test_av1() {
let v = f();
Config::set_option(
OPTION_AV1_TEST.to_string(),
if v { "Y" } else { "N" }.to_string(),
if v {
AV1_TEST_RESULT.0
} else {
AV1_TEST_RESULT.1
}
.to_string(),
);
});
});
+2
View File
@@ -52,6 +52,8 @@ pub mod aom;
#[cfg(not(any(target_os = "ios")))]
pub mod camera;
pub mod record;
#[cfg(target_pointer_width = "64")]
pub mod svt_av1;
mod vpx;
#[repr(usize)]
+500
View File
@@ -0,0 +1,500 @@
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(improper_ctypes)]
#![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/svt_av1_ffi.rs"));
use crate::codec::{base_bitrate, codec_thread_num, EncoderApi, EncoderCfg};
use crate::{EncodeInput, EncodeYuvFormat, Pixfmt, STRIDE_ALIGN};
use hbb_common::{
anyhow::Context,
bail,
bytes::Bytes,
log,
message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame},
ResultType,
};
use std::{mem::MaybeUninit, os::raw::c_void, ptr, slice};
// SVT-AV1 rejects on-the-fly target bitrates above 100_000 kbps.
const MAX_TARGET_BITRATE_KBPS: u32 = 100_000;
// CBR budgets bits per frame from the configured frame rate, VideoQoS pushes
// its authoritative pacing rate in via set_fps.
const DEFAULT_FPS: u32 = 30;
#[derive(Clone, Copy, Debug)]
pub struct SvtAv1EncoderConfig {
pub width: u32,
pub height: u32,
pub quality: f32,
pub keyframe_interval: Option<usize>,
}
pub struct SvtAv1Encoder {
handle: *mut EbComponentType,
width: usize,
height: usize,
yuvfmt: EncodeYuvFormat,
// current target bitrate in kbps, same unit as aom's rc_target_bitrate
bitrate: u32,
// bitrate change (kbps) to apply with the next picture via RATE_CHANGE_EVENT
pending_bitrate: Option<u32>,
// frame rate currently configured in the encoder
fps: u32,
// frame rate change to apply with the next picture via FRAME_RATE_CHANGE_EVENT
pending_fps: Option<u32>,
}
// The handle is only used behind &mut self, SVT-AV1 synchronizes internally.
unsafe impl Send for SvtAv1Encoder {}
impl EncoderApi for SvtAv1Encoder {
fn new(cfg: EncoderCfg, i444: bool) -> ResultType<Self>
where
Self: Sized,
{
match cfg {
EncoderCfg::SVTAV1(config) => {
if i444 {
// SVT-AV1 only supports 4:2:0 input, callers must fall back to aom for i444.
bail!("svt-av1 encoder does not support I444");
}
if !Self::support(config.width, config.height) {
bail!(
"svt-av1 encoder does not support resolution {}x{}",
config.width,
config.height
);
}
let mut handle: *mut EbComponentType = ptr::null_mut();
let mut c: MaybeUninit<EbSvtAv1EncConfiguration> = MaybeUninit::zeroed();
let res = unsafe { svt_av1_enc_init_handle(&mut handle, c.as_mut_ptr()) };
if res != EbErrorType::EB_ErrorNone || handle.is_null() {
bail!("svt_av1_enc_init_handle failed: {res:?}");
}
// c is loaded with the library defaults now, only override what we need.
let mut c = unsafe { c.assume_init() };
let bitrate = Self::bitrate(config.width, config.height, config.quality)
.min(MAX_TARGET_BITRATE_KBPS);
Self::apply_config(&mut c, &config, bitrate);
let mut res = unsafe { svt_av1_enc_set_parameter(handle, &mut c) };
if res == EbErrorType::EB_ErrorNone {
res = unsafe { svt_av1_enc_init(handle) };
}
if res != EbErrorType::EB_ErrorNone {
unsafe {
svt_av1_enc_deinit_handle(handle);
}
bail!("failed to init svt-av1 encoder: {res:?}");
}
Ok(Self {
handle,
width: config.width as _,
height: config.height as _,
yuvfmt: Self::get_yuvfmt(config.width, config.height),
bitrate,
pending_bitrate: None,
fps: DEFAULT_FPS,
pending_fps: None,
})
}
_ => bail!("encoder type mismatch"),
}
}
fn encode_to_message(&mut self, input: EncodeInput, ms: i64) -> ResultType<VideoFrame> {
let frames = self
.encode(ms, input.yuv()?)
.with_context(|| "Failed to encode")?;
if frames.len() > 0 {
Ok(Self::create_video_frame(frames))
} else {
bail!("no valid frame");
}
}
fn yuvfmt(&self) -> EncodeYuvFormat {
self.yuvfmt.clone()
}
#[cfg(feature = "vram")]
fn input_texture(&self) -> bool {
false
}
fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
let bitrate =
Self::bitrate(self.width as _, self.height as _, ratio).min(MAX_TARGET_BITRATE_KBPS);
if bitrate > 0 && bitrate != self.bitrate {
self.bitrate = bitrate;
self.pending_bitrate = Some(bitrate);
}
Ok(())
}
fn set_fps(&mut self, fps: u32) {
if fps == 0 {
return;
}
// also cancels a queued change that a later call made moot again
if fps == self.fps {
self.pending_fps = None;
} else {
self.pending_fps = Some(fps);
}
}
fn bitrate(&self) -> u32 {
self.bitrate
}
fn support_changing_quality(&self) -> bool {
true
}
fn latency_free(&self) -> bool {
true
}
fn is_hardware(&self) -> bool {
false
}
fn disable(&self) {}
}
impl SvtAv1Encoder {
pub fn support(width: u32, height: u32) -> bool {
width >= 64
&& height >= 64
&& width <= 16384
&& height <= 8704
&& width % 2 == 0
&& height % 2 == 0
}
fn apply_config(c: &mut EbSvtAv1EncConfiguration, cfg: &SvtAv1EncoderConfig, bitrate: u32) {
c.enc_mode = Self::preset(cfg.width, cfg.height);
c.source_width = cfg.width;
c.source_height = cfg.height;
// CBR budgets bits per frame from this rate, set_fps adjusts it on the
// fly with FRAME_RATE_CHANGE_EVENT.
c.frame_rate_numerator = DEFAULT_FPS;
c.frame_rate_denominator = 1;
c.encoder_bit_depth = 8;
c.encoder_color_format = EbColorFormat::EB_YUV420;
c.profile = EbAv1SeqProfile::MAIN_PROFILE;
// Low delay + rtc + CBR: one packet out per picture in, svt_av1_enc_get_packet
// blocks until the packet for the sent picture is ready, and rate/keyframe
// changes on the fly are allowed.
c.pred_structure = PredStructure::LOW_DELAY;
c.rtc = true;
c.rate_control_mode = SvtAv1RcMode::SVT_AV1_RC_MODE_CBR as _;
c.target_bit_rate = bitrate.min(MAX_TARGET_BITRATE_KBPS) * 1000;
// Full envelope of aom's calc_q_values so later bitrate changes are not clipped.
c.min_qp_allowed = 5;
c.max_qp_allowed = 45;
let (q_min, q_max) = Self::calc_q_values(cfg.quality);
c.qp = (q_min + q_max) / 2;
c.look_ahead_distance = 0;
c.recode_loop = 0; // DISALLOW_RECODE
c.scene_change_detection = 0;
c.screen_content_mode = 1;
c.tune = 1; // PSNR, low delay does not support tune 0
c.level_of_parallelism = Self::parallelism();
c.intra_refresh_type = SvtAv1IntraRefreshType::SVT_AV1_KF_REFRESH; // closed GOP
c.intra_period_length = match cfg.keyframe_interval {
Some(keyframe_interval) => keyframe_interval.saturating_sub(1) as _,
None => -1, // no periodic intra refresh, keyframes only on demand
};
// pic_type is left to the encoder and keyframes come from encoder
// restarts, the force_key_frames flag must stay off for low delay CBR
// (the library resets it with a warning).
c.force_key_frames = false;
}
fn preset(width: u32, height: u32) -> i8 {
// Mirrors aom's get_cpu_speed buckets, M9-M13 are the rtc presets.
if width * height <= 320 * 180 {
9
} else if width * height <= 640 * 360 {
10
} else {
11
}
}
fn parallelism() -> u32 {
// level_of_parallelism is a level from 1 to 6, not a thread count.
match codec_thread_num(64) {
n if n >= 32 => 6,
n if n >= 16 => 5,
n if n >= 8 => 4,
n if n >= 4 => 3,
n if n >= 2 => 2,
_ => 1,
}
}
fn encode(&mut self, ms: i64, data: &[u8]) -> ResultType<Vec<EncodedVideoFrame>> {
let fmt = &self.yuvfmt;
let chroma_height = (fmt.h + 1) / 2;
let len = fmt.v + fmt.stride[2] * chroma_height;
if data.len() < len {
bail!("len not enough: {} < {}", data.len(), len);
}
let mut io = EbSvtIOFormat {
luma: data.as_ptr() as *mut u8,
cb: data[fmt.u..].as_ptr() as *mut u8,
cr: data[fmt.v..].as_ptr() as *mut u8,
y_stride: fmt.stride[0] as _,
cr_stride: fmt.stride[2] as _,
cb_stride: fmt.stride[1] as _,
};
let mut hdr: EbBufferHeaderType = unsafe { std::mem::zeroed() };
hdr.size = std::mem::size_of::<EbBufferHeaderType>() as _;
hdr.p_buffer = &mut io as *mut EbSvtIOFormat as *mut u8;
hdr.n_filled_len = len as _;
hdr.pts = ms;
hdr.pic_type = EbAv1PictureType::EB_AV1_INVALID_PICTURE; // encoder decides
// send_picture copies the private data list, stack lifetime is fine.
// The pending changes are only cleared after a successful send, so a
// failed send retries them with the next picture.
let pending_bitrate = self.pending_bitrate;
let pending_fps = self.pending_fps;
let mut rate_info = SvtAv1RateInfo {
seq_qp: 0,
target_bit_rate: pending_bitrate.unwrap_or(0).min(MAX_TARGET_BITRATE_KBPS) * 1000,
};
let mut fps_info = SvtAv1FrameRateInfo {
frame_rate_numerator: pending_fps.unwrap_or(0),
frame_rate_denominator: 1,
};
let mut rate_node = EbPrivDataNode {
node_type: PrivDataType::RATE_CHANGE_EVENT,
data: &mut rate_info as *mut SvtAv1RateInfo as *mut c_void,
size: std::mem::size_of::<SvtAv1RateInfo>() as _,
next: ptr::null_mut(),
};
let mut fps_node = EbPrivDataNode {
node_type: PrivDataType::FRAME_RATE_CHANGE_EVENT,
data: &mut fps_info as *mut SvtAv1FrameRateInfo as *mut c_void,
size: std::mem::size_of::<SvtAv1FrameRateInfo>() as _,
next: ptr::null_mut(),
};
let mut list: *mut EbPrivDataNode = ptr::null_mut();
if pending_bitrate.is_some() {
list = &mut rate_node;
}
if pending_fps.is_some() {
fps_node.next = list;
list = &mut fps_node;
}
hdr.p_app_private = list as *mut c_void;
// The input YUV planes are copied inside send_picture.
let res = unsafe { svt_av1_enc_send_picture(self.handle, &mut hdr) };
if res != EbErrorType::EB_ErrorNone {
bail!("svt_av1_enc_send_picture failed: {res:?}");
}
self.pending_bitrate = None;
self.pending_fps = None;
if let Some(fps) = pending_fps {
self.fps = fps;
}
// In low delay mode get_packet blocks until the packet for the picture just
// sent is ready, and each picture produces exactly one packet. Do not call
// it again without sending another picture, it would block forever.
let mut frames = Vec::new();
let mut pkt: *mut EbBufferHeaderType = ptr::null_mut();
let res = unsafe { svt_av1_enc_get_packet(self.handle, &mut pkt, 0) };
match res {
EbErrorType::EB_ErrorNone if !pkt.is_null() => unsafe {
let h = &*pkt;
frames.push(EncodedVideoFrame {
data: Bytes::from(
slice::from_raw_parts(h.p_buffer, h.n_filled_len as usize).to_vec(),
),
key: h.pic_type == EbAv1PictureType::EB_AV1_KEY_PICTURE,
pts: h.pts,
..Default::default()
});
svt_av1_enc_release_out_buffer(&mut pkt);
},
EbErrorType::EB_NoErrorEmptyQueue => {}
_ => {
if !pkt.is_null() {
unsafe {
svt_av1_enc_release_out_buffer(&mut pkt);
}
}
bail!("svt_av1_enc_get_packet failed: {res:?}");
}
}
Ok(frames)
}
#[inline]
fn create_video_frame(frames: Vec<EncodedVideoFrame>) -> VideoFrame {
let mut vf = VideoFrame::new();
let av1s = EncodedVideoFrames {
frames: frames.into(),
..Default::default()
};
vf.set_av1s(av1s);
vf
}
fn bitrate(width: u32, height: u32, ratio: f32) -> u32 {
let bitrate = base_bitrate(width, height) as f32;
(bitrate * ratio) as u32
}
// Same mapping as AomEncoder::calc_q_values, only used to seed the start qp.
#[inline]
fn calc_q_values(ratio: f32) -> (u32, u32) {
let b = (ratio * 100.0) as u32;
let b = std::cmp::min(b, 200);
let q_min1 = 24;
let q_min2 = 5;
let q_max1 = 45;
let q_max2 = 25;
let t = b as f32 / 200.0;
let mut q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32;
let mut q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32;
q_min = q_min.clamp(q_min2, q_min1);
q_max = q_max.clamp(q_max2, q_max1);
(q_min, q_max)
}
fn get_yuvfmt(width: u32, height: u32) -> EncodeYuvFormat {
let w = width as usize;
let h = height as usize;
let align = |x: usize| (x + STRIDE_ALIGN - 1) & !(STRIDE_ALIGN - 1);
let stride_y = align(w);
let stride_uv = align((w + 1) / 2);
let u = stride_y * h;
let v = u + stride_uv * ((h + 1) / 2);
EncodeYuvFormat {
pixfmt: Pixfmt::I420,
w,
h,
stride: vec![stride_y, stride_uv, stride_uv],
u,
v,
}
}
}
impl Drop for SvtAv1Encoder {
fn drop(&mut self) {
unsafe {
if self.handle.is_null() {
return;
}
// svt_av1_enc_deinit drains the pipeline itself, but logs an error
// when EOS was not sent first.
let mut hdr: EbBufferHeaderType = std::mem::zeroed();
hdr.size = std::mem::size_of::<EbBufferHeaderType>() as _;
hdr.pic_type = EbAv1PictureType::EB_AV1_INVALID_PICTURE;
hdr.flags = EB_BUFFERFLAG_EOS;
let _ = svt_av1_enc_send_picture(self.handle, &mut hdr);
let res = svt_av1_enc_deinit(self.handle);
if res != EbErrorType::EB_ErrorNone {
log::error!("svt_av1_enc_deinit failed: {res:?}");
}
let res = svt_av1_enc_deinit_handle(self.handle);
if res != EbErrorType::EB_ErrorNone {
log::error!("svt_av1_enc_deinit_handle failed: {res:?}");
}
self.handle = ptr::null_mut();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::GoogleImage;
fn fake_i420(fmt: &EncodeYuvFormat, index: usize) -> Vec<u8> {
let chroma_height = (fmt.h + 1) / 2;
let len = fmt.v + fmt.stride[2] * chroma_height;
let mut yuv = vec![128u8; len];
// moving gradient so every frame differs
for y in 0..fmt.h {
let row = &mut yuv[y * fmt.stride[0]..y * fmt.stride[0] + fmt.w];
for (x, px) in row.iter_mut().enumerate() {
*px = ((x + y + index * 17) % 256) as u8;
}
}
yuv
}
#[test]
fn test_encode_and_decode_with_aom() {
let (width, height) = (640u32, 480u32);
let mut enc = SvtAv1Encoder::new(
crate::codec::EncoderCfg::SVTAV1(SvtAv1EncoderConfig {
width,
height,
quality: 1.0,
keyframe_interval: None,
}),
false,
)
.unwrap();
let fmt = enc.yuvfmt();
let mut dec = crate::aom::AomDecoder::new().unwrap();
for i in 0..20usize {
if i == 5 {
// exercises the FRAME_RATE_CHANGE_EVENT path on the next frame
enc.set_fps(60);
}
if i == 10 {
// exercises the RATE_CHANGE_EVENT path on the next frame
enc.set_quality(0.5).unwrap();
}
let yuv = fake_i420(&fmt, i);
let frames = enc.encode(i as i64 * 33, &yuv).unwrap();
// one packet out per picture in, this is the low delay contract
assert_eq!(frames.len(), 1, "no packet for frame {i}");
assert_eq!(frames[0].key, i == 0, "unexpected key flag for frame {i}");
assert!(!frames[0].data.is_empty());
let mut decoded = 0;
for f in &frames {
for img in dec.decode(&f.data).unwrap() {
assert_eq!(img.width(), width as usize);
assert_eq!(img.height(), height as usize);
decoded += 1;
}
}
for img in dec.flush().unwrap() {
assert_eq!(img.width(), width as usize);
decoded += 1;
}
assert!(decoded >= 1, "aom failed to decode svt-av1 frame {i}");
}
}
#[test]
fn test_unsupported_resolution() {
assert!(SvtAv1Encoder::new(
crate::codec::EncoderCfg::SVTAV1(SvtAv1EncoderConfig {
width: 62,
height: 62,
quality: 1.0,
keyframe_interval: None,
}),
false,
)
.is_err());
}
}
+13
View File
@@ -0,0 +1,13 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c0c9767..93ec320 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -192,7 +192,7 @@ set(PC_LIBS_PRIVATE)
set(PC_REQUIRES_PRIVATE)
#Clang support, required to build static with LTO
-if(CMAKE_C_COMPILER_ID MATCHES "Clang" AND UNIX AND NOT APPLE)
+if(FALSE)
find_program(LLVM_LD_EXE llvm-ld)
find_program(LLVM_AR_EXE llvm-ar)
find_program(LLVM_RANLIB_EXE llvm-ranlib)
@@ -0,0 +1,19 @@
diff --git a/Source/Lib/ASM_AVX2/synonyms_avx2.h b/Source/Lib/ASM_AVX2/synonyms_avx2.h
index f615e2569..9a7667724 100644
--- a/Source/Lib/ASM_AVX2/synonyms_avx2.h
+++ b/Source/Lib/ASM_AVX2/synonyms_avx2.h
@@ -75,7 +75,13 @@ static INLINE __m256i yy_loadu2_128(const void *hi, const void *lo) {
return yy_set_m128i(mhi, mlo);
}
-static INLINE __m256i yy_unpacklo_epi128(const __m256i in0, const __m256i in1) {
+#if defined(_MSC_VER) && _MSC_VER >= 1950
+#define MAYBE_INLINE NOINLINE
+#else
+#define MAYBE_INLINE INLINE
+#endif
+
+static MAYBE_INLINE __m256i yy_unpacklo_epi128(const __m256i in0, const __m256i in1) {
return _mm256_inserti128_si256(in0, _mm256_castsi256_si128(in1), 1);
}
+45
View File
@@ -0,0 +1,45 @@
vcpkg_from_gitlab(
GITLAB_URL https://gitlab.com
OUT_SOURCE_PATH SOURCE_PATH
REPO AOMediaCodec/SVT-AV1
REF v${VERSION}
SHA512 37df96559af179d28acae10cdff98c94ee2c4ee086b2446ab362b57be9adf566d6f2adc28faa30648798d9bc7d18eaeb2d0665e7292982652496b605342e1c4b
PATCHES
# upstream forces llvm-ld/llvm-ar/llvm-ranlib for clang static builds on
# non-Apple unix, which breaks toolchains without the llvm binutils
no-force-llvm.diff
# MSVC >= 1950 miscompiles the inlined yy_unpacklo_epi128
no-inline-yy_unpacklo_epi128.diff
)
if(VCPKG_TARGET_ARCHITECTURE MATCHES "^(x86|x64)$")
# NASM is required to build the x86/x64 assembly
vcpkg_find_acquire_program(NASM)
set(SIMD_OPTIONS -DCOMPILE_C_ONLY=OFF "-DCMAKE_ASM_NASM_COMPILER=${NASM}")
elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "^(arm64|arm64ec)$" AND NOT VCPKG_TARGET_IS_WINDOWS)
set(SIMD_OPTIONS -DCOMPILE_C_ONLY=OFF)
else()
set(SIMD_OPTIONS -DCOMPILE_C_ONLY=ON)
endif()
vcpkg_cmake_configure(
SOURCE_PATH "${SOURCE_PATH}"
OPTIONS
${SIMD_OPTIONS}
-DBUILD_APPS=OFF
-DBUILD_TESTING=OFF
-DREPRODUCIBLE_BUILDS=ON
# SVT-AV1 defaults LTO to ON for gcc>=9/clang>=12, which puts LTO
# bitcode into the static archive and breaks linking with a different
# compiler version
-DSVT_AV1_LTO=OFF
)
vcpkg_cmake_install()
vcpkg_cmake_config_fixup(PACKAGE_NAME SVT-AV1 CONFIG_PATH lib/cmake/SVT-AV1)
vcpkg_copy_pdbs()
vcpkg_fixup_pkgconfig()
file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include")
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.md" "${SOURCE_PATH}/PATENTS.md")
+19
View File
@@ -0,0 +1,19 @@
{
"name": "svt-av1",
"version-semver": "4.2.0",
"port-version": 0,
"description": "Scalable Video Technology AV1 software video encoder library",
"homepage": "https://gitlab.com/AOMediaCodec/SVT-AV1",
"license": "BSD-3-Clause-Clear",
"supports": "!x86 & !arm32 & !uwp",
"dependencies": [
{
"name": "vcpkg-cmake",
"host": true
},
{
"name": "vcpkg-cmake-config",
"host": true
}
]
}
+25 -6
View File
@@ -1023,12 +1023,28 @@ fn get_encoder_config(
},
keyframe_interval,
}),
CodecFormat::AV1 => EncoderCfg::AOM(AomEncoderConfig {
width: c.width as _,
height: c.height as _,
quality,
keyframe_interval,
}),
CodecFormat::AV1 => {
let aom = EncoderCfg::AOM(AomEncoderConfig {
width: c.width as _,
height: c.height as _,
quality,
keyframe_interval,
});
// Prefer svt-av1 for AV1 encoding, keep aom for i444 (svt-av1 is 4:2:0
// only) and for resolutions svt-av1 cannot handle.
#[cfg(target_pointer_width = "64")]
if scrap::svt_av1::SvtAv1Encoder::support(c.width as _, c.height as _)
&& !Encoder::use_i444(&aom)
{
return EncoderCfg::SVTAV1(scrap::svt_av1::SvtAv1EncoderConfig {
width: c.width as _,
height: c.height as _,
quality,
keyframe_interval,
});
}
aom
}
_ => EncoderCfg::VPX(VpxEncoderConfig {
width: c.width as _,
height: c.height as _,
@@ -1326,6 +1342,9 @@ fn check_qos(
) -> ResultType<()> {
let mut video_qos = VIDEO_QOS.lock().unwrap();
*spf = video_qos.spf();
// encoders whose rate control budgets per frame need the real pacing rate,
// implementations no-op when the value is unchanged
encoder.set_fps(video_qos.fps());
if *ratio != video_qos.ratio() {
*ratio = video_qos.ratio();
if encoder.support_changing_quality() {
+5
View File
@@ -8,6 +8,11 @@
"name": "aom",
"host": false
},
{
"name": "svt-av1",
"host": false,
"platform": "!x86 & !arm32 & !uwp"
},
{
"name": "cpu-features",
"platform": "android"