diff --git a/libs/scrap/build.rs b/libs/scrap/build.rs index cdee4d5ec..7f08472e5 100644 --- a/libs/scrap/build.rs +++ b/libs/scrap/build.rs @@ -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 { // 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 , so the parent include root is needed too. + if name == "svt-av1" { + let parents: Vec = 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 { @@ -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 { 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" { diff --git a/libs/scrap/src/bindings/svt_av1_ffi.h b/libs/scrap/src/bindings/svt_av1_ffi.h new file mode 100644 index 000000000..d16aec3e8 --- /dev/null +++ b/libs/scrap/src/bindings/svt_av1_ffi.h @@ -0,0 +1,5 @@ +#include +#include +#include +#include +#include diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 9b072e1bd..08b4ed8d8 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -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) + }; + // 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) + .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(), ); }); }); diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 2d74caa0d..4adcae102 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -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)] diff --git a/libs/scrap/src/common/svt_av1.rs b/libs/scrap/src/common/svt_av1.rs new file mode 100644 index 000000000..86572eba7 --- /dev/null +++ b/libs/scrap/src/common/svt_av1.rs @@ -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, +} + +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, + // 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, +} + +// 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 + 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 = 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 { + 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> { + 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::() 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::() 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::() 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) -> 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::() 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 { + 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()); + } +} diff --git a/res/vcpkg/svt-av1/no-force-llvm.diff b/res/vcpkg/svt-av1/no-force-llvm.diff new file mode 100644 index 000000000..eb3a868bc --- /dev/null +++ b/res/vcpkg/svt-av1/no-force-llvm.diff @@ -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) diff --git a/res/vcpkg/svt-av1/no-inline-yy_unpacklo_epi128.diff b/res/vcpkg/svt-av1/no-inline-yy_unpacklo_epi128.diff new file mode 100644 index 000000000..3c23c08bd --- /dev/null +++ b/res/vcpkg/svt-av1/no-inline-yy_unpacklo_epi128.diff @@ -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); + } + diff --git a/res/vcpkg/svt-av1/portfile.cmake b/res/vcpkg/svt-av1/portfile.cmake new file mode 100644 index 000000000..e8eeb16e7 --- /dev/null +++ b/res/vcpkg/svt-av1/portfile.cmake @@ -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") diff --git a/res/vcpkg/svt-av1/vcpkg.json b/res/vcpkg/svt-av1/vcpkg.json new file mode 100644 index 000000000..7e3b24330 --- /dev/null +++ b/res/vcpkg/svt-av1/vcpkg.json @@ -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 + } + ] +} diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 9d97b1ce9..7bd7b66f0 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -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() { diff --git a/vcpkg.json b/vcpkg.json index cd282fc1c..aaa08c984 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -8,6 +8,11 @@ "name": "aom", "host": false }, + { + "name": "svt-av1", + "host": false, + "platform": "!x86 & !arm32 & !uwp" + }, { "name": "cpu-features", "platform": "android"