mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-08-28 17:17:02 +00:00
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:
co-authored by
Claude Fable 5
parent
1c2dd71891
commit
1ac684db0d
+57
-16
@@ -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" {
|
||||
|
||||
@@ -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>
|
||||
@@ -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(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
"name": "aom",
|
||||
"host": false
|
||||
},
|
||||
{
|
||||
"name": "svt-av1",
|
||||
"host": false,
|
||||
"platform": "!x86 & !arm32 & !uwp"
|
||||
},
|
||||
{
|
||||
"name": "cpu-features",
|
||||
"platform": "android"
|
||||
|
||||
Reference in New Issue
Block a user