diff --git a/src/config/schema/stream.rs b/src/config/schema/stream.rs index 25908e4c..f1acb752 100644 --- a/src/config/schema/stream.rs +++ b/src/config/schema/stream.rs @@ -102,8 +102,8 @@ pub enum EncoderType { Qsv, Amf, Rkmpp, + #[serde(alias = "amlogic")] V4l2m2m, - Amlogic, } impl EncoderType { @@ -117,7 +117,6 @@ impl EncoderType { EncoderType::Amf => "AMD AMF", EncoderType::Rkmpp => "Rockchip MPP", EncoderType::V4l2m2m => "V4L2 M2M", - EncoderType::Amlogic => "AMLENC", } } } diff --git a/src/stream_encoder.rs b/src/stream_encoder.rs index 4bae805a..ce0360f5 100644 --- a/src/stream_encoder.rs +++ b/src/stream_encoder.rs @@ -14,29 +14,5 @@ pub fn encoder_type_to_backend(encoder: EncoderType) -> Option { EncoderType::Amf => Some(EncoderBackend::Amf), EncoderType::Rkmpp => Some(EncoderBackend::Rkmpp), EncoderType::V4l2m2m => Some(EncoderBackend::V4l2m2m), - EncoderType::Amlogic => Some(EncoderBackend::Amlogic), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn maps_amlogic_config_to_backend() { - assert_eq!( - encoder_type_to_backend(EncoderType::Amlogic), - Some(EncoderBackend::Amlogic) - ); - } - - #[test] - fn amlogic_config_json_round_trip() { - let json = serde_json::to_string(&EncoderType::Amlogic).unwrap(); - assert_eq!(json, "\"amlogic\""); - assert_eq!( - serde_json::from_str::(&json).unwrap(), - EncoderType::Amlogic - ); } } diff --git a/src/video/codec/amlenc.rs b/src/video/codec/amlenc.rs deleted file mode 100644 index 4ee9b0c5..00000000 --- a/src/video/codec/amlenc.rs +++ /dev/null @@ -1,989 +0,0 @@ -//! Native Amlogic AMLENC bindings for the S912/GXM vendor Linux 4.9 stack. -//! -//! The vendor libraries are deliberately loaded at runtime. They must be built -//! with the One-KVM ABI v1 patch from the standalone `amlenc` repository; -//! unpatched 0.4 libraries -//! are rejected before any device access is attempted. - -use std::env; -use std::ffi::{c_int, c_long, c_uchar, c_uint, OsStr}; -use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::{Duration, Instant}; - -use bytes::Bytes; -use libloading::Library; -use tracing::{debug, warn}; - -use crate::error::{AppError, Result}; -use crate::video::format::Resolution; - -pub const AMLENC_ABI_VERSION: c_int = 1; -pub const AMLENC_H264_CODEC_NAME: &str = "h264_amlenc"; -pub const AMLENC_H265_CODEC_NAME: &str = "hevc_amlenc"; -pub const AMLENC_H264_DEFAULT_LIBRARY: &str = "libvpcodec.so"; -pub const AMLENC_H265_DEFAULT_LIBRARY: &str = "libvphevcodec.so"; - -const AMLENC_MAX_WIDTH: u32 = 1920; -const AMLENC_MAX_HEIGHT: u32 = 1080; -const AMLENC_MAX_FPS: u32 = 60; -const MIN_OUTPUT_BUFFER_SIZE: usize = 1024 * 1024; -const OUTPUT_STALL_TIMEOUT: Duration = Duration::from_secs(1); -const CODEC_ID_H264: c_int = 4; -const CODEC_ID_H265: c_int = 5; -const IMG_FMT_NV12: c_int = 1; -const FRAME_TYPE_AUTO: c_int = 1; -const FRAME_TYPE_IDR: c_int = 2; -const H264_NV12_FORMAT: c_int = 0; -const H265_NV12_FORMAT: c_int = 1; - -type AbiVersionFn = unsafe extern "C" fn() -> c_int; -type H264InitFn = unsafe extern "C" fn(c_int, c_int, c_int, c_int, c_int, c_int, c_int) -> c_long; -type H265InitFn = unsafe extern "C" fn(c_int, c_int, c_int, c_int, c_int, c_int) -> c_long; -type H264EncodeFn = - unsafe extern "C" fn(c_long, c_int, *mut c_uchar, c_int, *mut c_uchar, c_int) -> c_int; -type H265EncodeFn = - unsafe extern "C" fn(c_long, c_int, *mut c_uchar, c_uint, *mut c_uchar, c_int) -> c_int; -type DestroyFn = unsafe extern "C" fn(c_long) -> c_int; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AmlencCodec { - H264, - H265, -} - -impl AmlencCodec { - pub fn codec_name(self) -> &'static str { - match self { - Self::H264 => AMLENC_H264_CODEC_NAME, - Self::H265 => AMLENC_H265_CODEC_NAME, - } - } - - pub fn default_library(self) -> &'static str { - match self { - Self::H264 => AMLENC_H264_DEFAULT_LIBRARY, - Self::H265 => AMLENC_H265_DEFAULT_LIBRARY, - } - } - - pub fn library_env(self) -> &'static str { - match self { - Self::H264 => "ONE_KVM_AMLENC_H264_LIB", - Self::H265 => "ONE_KVM_AMLENC_H265_LIB", - } - } - - pub fn device_node(self) -> &'static str { - match self { - Self::H264 => "/dev/amvenc_avc", - Self::H265 => "/dev/HevcEnc", - } - } -} - -#[derive(Debug, Clone, Copy)] -pub struct AmlencConfig { - pub codec: AmlencCodec, - pub resolution: Resolution, - pub fps: u32, - pub bitrate_kbps: u32, - pub gop: u32, -} - -impl AmlencConfig { - pub fn validate(self) -> Result<()> { - let width = self.resolution.width; - let height = self.resolution.height; - if width == 0 - || height == 0 - || width > AMLENC_MAX_WIDTH - || height > AMLENC_MAX_HEIGHT - || width % 16 != 0 - || height % 2 != 0 - { - return Err(AppError::VideoError(format!( - "AMLENC requires NV12 with 16-aligned width, even height, and at most 1920x1080 (got {}x{})", - width, height - ))); - } - if !(1..=AMLENC_MAX_FPS).contains(&self.fps) { - return Err(AppError::VideoError(format!( - "AMLENC supports 1-60 fps (got {})", - self.fps - ))); - } - if self.bitrate_kbps == 0 || self.bitrate_kbps > (c_int::MAX as u32 / 1000) { - return Err(AppError::VideoError(format!( - "Invalid AMLENC bitrate: {} kbps", - self.bitrate_kbps - ))); - } - if self.gop > c_int::MAX as u32 { - return Err(AppError::VideoError("AMLENC GOP is too large".to_string())); - } - nv12_frame_size(self.resolution)?; - Ok(()) - } - - fn bitrate_bps(self) -> c_int { - (self.bitrate_kbps * 1000) as c_int - } - - fn vendor_gop(self) -> c_int { - match self.codec { - // GXM's H.264 microcode can time out on a later natural IDR for - // complex 1080p pictures. The pinned vendor library defines zero - // as an infinite GOP (one IDR when the instance is created). - AmlencCodec::H264 => 0, - AmlencCodec::H265 => self.gop as c_int, - } - } -} - -pub fn nv12_frame_size(resolution: Resolution) -> Result { - let pixels = (resolution.width as usize) - .checked_mul(resolution.height as usize) - .ok_or_else(|| AppError::VideoError("AMLENC NV12 frame size overflow".to_string()))?; - pixels - .checked_mul(3) - .map(|value| value / 2) - .ok_or_else(|| AppError::VideoError("AMLENC NV12 frame size overflow".to_string())) -} - -fn validate_abi_version(version: c_int, path: &Path) -> Result<()> { - if version != AMLENC_ABI_VERSION { - return Err(AppError::VideoError(format!( - "AMLENC library {} has ABI {}, expected ABI v{}; apply the one-kvm-amlenc-abi-v1.patch from the standalone amlenc repository", - path.display(), - version, - AMLENC_ABI_VERSION - ))); - } - Ok(()) -} - -struct H264Api { - _library: Library, - init: H264InitFn, - encode: H264EncodeFn, - destroy: DestroyFn, -} - -struct H265Api { - _library: Library, - init: H265InitFn, - encode: H265EncodeFn, - destroy: DestroyFn, -} - -enum AmlencApi { - H264(H264Api), - H265(H265Api), -} - -unsafe fn required_symbol(library: &Library, name: &[u8], path: &Path) -> Result { - // SAFETY: the caller supplies the signature from the fixed upstream headers. - unsafe { library.get::(name) } - .map(|symbol| *symbol) - .map_err(|error| { - AppError::VideoError(format!( - "AMLENC library {} is missing {}: {}", - path.display(), - String::from_utf8_lossy(name).trim_end_matches('\0'), - error - )) - }) -} - -impl AmlencApi { - fn load(codec: AmlencCodec, path: &Path) -> Result { - // SAFETY: all calls are made through signatures checked against the pinned headers, - // and the Library remains owned by the API object for the lifetime of the pointers. - let library = unsafe { Library::new(path) }.map_err(|error| { - AppError::VideoError(format!( - "Failed to load AMLENC {} library {}: {}", - codec.codec_name(), - path.display(), - error - )) - })?; - let abi_version: AbiVersionFn = - unsafe { required_symbol(&library, b"one_kvm_amlenc_abi_version\0", path)? }; - // SAFETY: the ABI marker has no arguments or side effects. - validate_abi_version(unsafe { abi_version() }, path)?; - - Ok(match codec { - AmlencCodec::H264 => { - let init: H264InitFn = - unsafe { required_symbol(&library, b"vl_video_encoder_init\0", path)? }; - let encode: H264EncodeFn = - unsafe { required_symbol(&library, b"vl_video_encoder_encode\0", path)? }; - let destroy: DestroyFn = - unsafe { required_symbol(&library, b"vl_video_encoder_destory\0", path)? }; - Self::H264(H264Api { - _library: library, - init, - encode, - destroy, - }) - } - AmlencCodec::H265 => { - let init: H265InitFn = - unsafe { required_symbol(&library, b"vl_video_encoder_init\0", path)? }; - let encode: H265EncodeFn = - unsafe { required_symbol(&library, b"vl_video_encoder_encode\0", path)? }; - let destroy: DestroyFn = - unsafe { required_symbol(&library, b"vl_video_encoder_destory\0", path)? }; - Self::H265(H265Api { - _library: library, - init, - encode, - destroy, - }) - } - }) - } - - unsafe fn init(&self, config: AmlencConfig) -> c_long { - let width = config.resolution.width as c_int; - let height = config.resolution.height as c_int; - match self { - Self::H264(api) => unsafe { - (api.init)( - CODEC_ID_H264, - width, - height, - config.fps as c_int, - config.bitrate_bps(), - config.vendor_gop(), - IMG_FMT_NV12, - ) - }, - Self::H265(api) => unsafe { - (api.init)( - CODEC_ID_H265, - width, - height, - config.fps as c_int, - config.bitrate_bps(), - config.gop as c_int, - ) - }, - } - } - - unsafe fn encode( - &self, - handle: c_long, - frame_type: c_int, - input: *mut c_uchar, - output: *mut c_uchar, - output_len: usize, - ) -> c_int { - match self { - // H.264's fourth argument is documented as input length, but the pinned - // implementation uses it exclusively as output capacity. - Self::H264(api) => unsafe { - (api.encode)( - handle, - frame_type, - input, - output_len as c_int, - output, - H264_NV12_FORMAT, - ) - }, - Self::H265(api) => unsafe { - (api.encode)( - handle, - frame_type, - input, - output_len as c_uint, - output, - H265_NV12_FORMAT, - ) - }, - } - } - - unsafe fn destroy(&self, handle: c_long) { - match self { - Self::H264(api) => { - unsafe { (api.destroy)(handle) }; - } - Self::H265(api) => { - unsafe { (api.destroy)(handle) }; - } - } - } -} - -static AMLENC_INSTANCE_ACTIVE: AtomicBool = AtomicBool::new(false); - -struct ExclusiveInstance; - -impl ExclusiveInstance { - fn acquire() -> Result { - AMLENC_INSTANCE_ACTIVE - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .map_err(|_| { - AppError::VideoError( - "AMLENC hardware is already in use by another encoder or self-check" - .to_string(), - ) - })?; - Ok(Self) - } -} - -impl Drop for ExclusiveInstance { - fn drop(&mut self) { - AMLENC_INSTANCE_ACTIVE.store(false, Ordering::Release); - } -} - -pub struct AmlencEncoder { - api: AmlencApi, - handle: c_long, - config: AmlencConfig, - output: Vec, - force_keyframe: bool, - rebuild_before_next_frame: bool, - expect_parameterized_keyframe: bool, - last_output: Instant, - _exclusive: ExclusiveInstance, -} - -impl AmlencEncoder { - pub fn new(config: AmlencConfig) -> Result { - let path = env::var_os(config.codec.library_env()) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(config.codec.default_library())); - Self::with_library(config, path) - } - - pub fn with_library(config: AmlencConfig, path: impl AsRef) -> Result { - config.validate()?; - let exclusive = ExclusiveInstance::acquire()?; - let path = PathBuf::from(path.as_ref()); - let api = AmlencApi::load(config.codec, &path)?; - let frame_size = nv12_frame_size(config.resolution)?; - let output = vec![0; frame_size.max(MIN_OUTPUT_BUFFER_SIZE)]; - let mut encoder = Self { - api, - handle: 0, - config, - output, - force_keyframe: false, - rebuild_before_next_frame: false, - expect_parameterized_keyframe: true, - last_output: Instant::now(), - _exclusive: exclusive, - }; - encoder.create_handle()?; - Ok(encoder) - } - - pub fn codec_name(&self) -> &'static str { - self.config.codec.codec_name() - } - - pub fn config(&self) -> AmlencConfig { - self.config - } - - fn create_handle(&mut self) -> Result<()> { - debug!( - "Creating {} at {}x{} {} fps {} kbps", - self.codec_name(), - self.config.resolution.width, - self.config.resolution.height, - self.config.fps, - self.config.bitrate_kbps - ); - // SAFETY: config validation guarantees values accepted by ABI v1. - self.handle = unsafe { self.api.init(self.config) }; - if self.handle <= 0 { - return Err(AppError::VideoError(format!( - "AMLENC {} initialization failed; check {}, firmware, CMA, and device permissions", - self.codec_name(), - self.config.codec.device_node() - ))); - } - // The first H.264 picture is naturally an IDR. Never pass the - // in-place FORCE_IDR command to the GXM H.264 microcode: later IDRs can - // wedge it. H.265 does not share that observed defect and retains its - // ABI-v1 forced-IRAP behavior. - self.force_keyframe = self.config.codec == AmlencCodec::H265; - self.rebuild_before_next_frame = false; - self.expect_parameterized_keyframe = true; - self.last_output = Instant::now(); - Ok(()) - } - - fn destroy_handle(&mut self) { - if self.handle > 0 { - // SAFETY: the handle was returned by this API instance and is destroyed once. - unsafe { self.api.destroy(self.handle) }; - self.handle = 0; - } - } - - fn rebuild(&mut self, reason: &str) -> Result<()> { - warn!("Rebuilding {} encoder: {}", self.codec_name(), reason); - self.destroy_handle(); - self.create_handle() - } - - pub fn request_keyframe(&mut self) { - if self.config.codec == AmlencCodec::H264 { - // A fresh encoder reliably emits SPS/PPS + IDR on its first AUTO - // frame. Coalesce repeated client requests while a rebuild or - // fresh first frame is already pending. - if !self.expect_parameterized_keyframe { - self.rebuild_before_next_frame = true; - } - } else { - self.force_keyframe = true; - } - } - - pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> { - let mut updated = self.config; - updated.bitrate_kbps = bitrate_kbps; - updated.validate()?; - self.config = updated; - self.rebuild("bitrate changed") - } - - pub fn encode_raw(&mut self, data: &[u8]) -> Result> { - let expected = nv12_frame_size(self.config.resolution)?; - if data.len() != expected { - return Err(AppError::VideoError(format!( - "AMLENC requires contiguous NV12 data of exactly {} bytes (got {})", - expected, - data.len() - ))); - } - - if self.rebuild_before_next_frame { - self.rebuild("H.264 keyframe requested")?; - } - - match self.encode_once(data) { - Ok(frame) => Ok(frame), - Err(first_error) => { - self.rebuild(&format!("vendor encode call failed: {first_error}"))?; - self.encode_once(data).map_err(|retry_error| { - AppError::VideoError(format!( - "AMLENC encode failed after one rebuild: {}; retry: {}", - first_error, retry_error - )) - }) - } - } - } - - fn encode_once(&mut self, data: &[u8]) -> Result> { - if self.handle <= 0 { - return Err(AppError::VideoError( - "AMLENC handle is not initialized".to_string(), - )); - } - let forced = self.force_keyframe; - let require_parameterized_keyframe = self.expect_parameterized_keyframe || forced; - let frame_type = if forced { - FRAME_TYPE_IDR - } else { - FRAME_TYPE_AUTO - }; - // The vendor API takes a mutable pointer but does not modify VMALLOC input. - // SAFETY: input/output live for the call, capacities are ABI-sized and the - // output length is validated before any slice is formed. - let length = unsafe { - self.api.encode( - self.handle, - frame_type, - data.as_ptr() as *mut c_uchar, - self.output.as_mut_ptr(), - self.output.len(), - ) - }; - if length < 0 { - return Err(AppError::VideoError(format!( - "{} vendor library returned {}", - self.codec_name(), - length - ))); - } - // A keyframe request applies to one submitted frame. Repeating IDR on - // every zero-output call can trap the S912 driver in its light-reset - // loop; WebRTC will issue another request if this attempt was skipped. - if forced { - self.force_keyframe = false; - } - let length = length as usize; - if length > self.output.len() { - return Err(AppError::VideoError(format!( - "{} returned oversized output {} > {}", - self.codec_name(), - length, - self.output.len() - ))); - } - if length == 0 { - if forced { - return Err(AppError::VideoError(format!( - "{} produced no output for a forced keyframe", - self.codec_name() - ))); - } - // The vendor ABI uses zero for rate-control skips and recoverable - // hardware timeouts. Do not rebuild for a few skipped frames, but - // recover if the vendor stops producing output altogether. - if self.last_output.elapsed() >= OUTPUT_STALL_TIMEOUT { - self.rebuild("no encoded output for one second")?; - } - return Ok(None); - } - - let encoded = &self.output[..length]; - let nal_summary = inspect_annex_b(self.config.codec, encoded); - let keyframe = nal_summary.keyframe; - if require_parameterized_keyframe - && (!keyframe || !nal_summary.has_parameter_sets(self.config.codec)) - { - return Err(AppError::VideoError(format!( - "{} fresh/forced keyframe did not contain an IRAP/IDR and complete parameter sets", - self.codec_name() - ))); - } - self.force_keyframe = false; - self.expect_parameterized_keyframe = false; - self.last_output = Instant::now(); - Ok(Some((Bytes::copy_from_slice(encoded), keyframe))) - } -} - -impl Drop for AmlencEncoder { - fn drop(&mut self) { - self.destroy_handle(); - } -} - -#[derive(Default)] -struct AnnexBNalSummary { - keyframe: bool, - vps: bool, - sps: bool, - pps: bool, -} - -impl AnnexBNalSummary { - fn has_parameter_sets(&self, codec: AmlencCodec) -> bool { - match codec { - AmlencCodec::H264 => self.sps && self.pps, - AmlencCodec::H265 => self.vps && self.sps && self.pps, - } - } -} - -fn inspect_annex_b(codec: AmlencCodec, data: &[u8]) -> AnnexBNalSummary { - let mut summary = AnnexBNalSummary::default(); - let mut index = 0; - while index + 3 <= data.len() { - let start_len = if index + 4 <= data.len() && data[index..index + 4] == [0, 0, 0, 1] { - 4 - } else if data[index..index + 3] == [0, 0, 1] { - 3 - } else { - index += 1; - continue; - }; - let nal = index + start_len; - if nal >= data.len() { - break; - } - let nal_type = match codec { - AmlencCodec::H264 => data[nal] & 0x1f, - AmlencCodec::H265 => (data[nal] >> 1) & 0x3f, - }; - match codec { - AmlencCodec::H264 => match nal_type { - 5 => summary.keyframe = true, - 7 => summary.sps = true, - 8 => summary.pps = true, - _ => {} - }, - AmlencCodec::H265 => match nal_type { - 16..=23 => summary.keyframe = true, - 32 => summary.vps = true, - 33 => summary.sps = true, - 34 => summary.pps = true, - _ => {} - }, - } - index = nal + 1; - } - summary -} - -pub fn is_keyframe(codec: AmlencCodec, data: &[u8]) -> bool { - inspect_annex_b(codec, data).keyframe -} - -pub fn has_parameter_sets(codec: AmlencCodec, data: &[u8]) -> bool { - inspect_annex_b(codec, data).has_parameter_sets(codec) -} - -#[cfg_attr( - not(any(test, all(target_os = "linux", target_arch = "aarch64"))), - allow(dead_code) -)] -fn is_s912_gxm_compatible(compatible: &[u8]) -> bool { - let compatible = String::from_utf8_lossy(compatible).to_ascii_lowercase(); - // s912 - compatible.contains("amlogic,gxm") - || compatible.contains("amlogic, gxm") - || compatible.contains("amlogic,meson-gxm") - || compatible.contains("amlogic,s912") - // s905d - || compatible.contains("amlogic,meson-gxl") - || compatible.contains("amlogic,s905d") -} - -pub fn system_is_s912_gxm() -> Result { - #[cfg(all(target_os = "linux", target_arch = "aarch64"))] - { - let compatible = std::fs::read("/proc/device-tree/compatible").map_err(|error| { - AppError::VideoError(format!( - "Cannot read /proc/device-tree/compatible for AMLENC detection: {}", - error - )) - })?; - return Ok(is_s912_gxm_compatible(&compatible)); - } - #[cfg(not(all(target_os = "linux", target_arch = "aarch64")))] - Ok(false) -} - -/// Perform the destructive part of backend detection: initialize and encode one -/// 640x480 NV12 frame. The caller must first check SoC compatibility and node. -pub fn smoke_test(codec: AmlencCodec) -> Result<()> { - let resolution = Resolution::new(640, 480); - let config = AmlencConfig { - codec, - resolution, - fps: 30, - bitrate_kbps: 1_000, - gop: 30, - }; - let mut encoder = AmlencEncoder::new(config)?; - let mut frame = vec![0x80; nv12_frame_size(resolution)?]; - frame[..(resolution.width * resolution.height) as usize].fill(0x10); - for _ in 0..3 { - if encoder.encode_raw(&frame)?.is_some() { - return Ok(()); - } - } - Err(AppError::VideoError(format!( - "{} produced no output during the 640x480 probe", - codec.codec_name() - ))) -} - -#[cfg(test)] -mod tests { - use super::*; - #[cfg(unix)] - use std::process::Command; - #[cfg(unix)] - use std::sync::Mutex; - - #[cfg(unix)] - static TEST_INSTANCE_MUTEX: Mutex<()> = Mutex::new(()); - - #[cfg(unix)] - const H264_FIXTURE: &str = r#" - static int values[16]; - static int mode; - static int fail_pending; - int one_kvm_amlenc_abi_version(void) { return 1; } - long vl_video_encoder_init(int codec, int width, int height, int fps, - int bitrate, int gop, int image_format) { - values[0]++; values[1] = codec; values[2] = width; values[3] = height; - values[4] = fps; values[5] = bitrate; values[6] = gop; - values[7] = image_format; return 1; - } - int vl_video_encoder_encode(long handle, int frame_type, unsigned char *in, - int in_size, unsigned char *out, int format) { - (void)handle; (void)in; values[8]++; values[9] = frame_type; - values[10] = in_size; values[11] = format; - if (fail_pending) { fail_pending = 0; return -9; } - if (mode == 2) return 0; - if (mode == 3) return 2000000; - { unsigned char data[] = {0,0,1,0x67,0,0,1,0x68,0,0,1,0x65}; - for (unsigned long i = 0; i < sizeof(data); i++) out[i] = data[i]; - return sizeof(data); } - } - int vl_video_encoder_destory(long handle) { (void)handle; values[12]++; return 1; } - int test_get(int index) { return values[index]; } - void test_set_mode(int value) { mode = value; } - void test_fail_once(void) { fail_pending = 1; } - "#; - - #[cfg(unix)] - const H265_FIXTURE: &str = r#" - static int values[16]; - static int mode; - int one_kvm_amlenc_abi_version(void) { return 1; } - long vl_video_encoder_init(int codec, int width, int height, int fps, - int bitrate, int gop) { - values[0]++; values[1] = codec; values[2] = width; values[3] = height; - values[4] = fps; values[5] = bitrate; values[6] = gop; return 1; - } - int vl_video_encoder_encode(long handle, int frame_type, unsigned char *in, - unsigned int output_len, unsigned char *out, int format) { - (void)handle; (void)in; values[8]++; values[9] = frame_type; - values[10] = output_len; values[11] = format; - if (mode == 3) return output_len + 1; - { unsigned char data[] = {0,0,1,0x40,1,0,0,1,0x42,1,0,0,1,0x44,1, - 0,0,1,0x26,1}; - for (unsigned long i = 0; i < sizeof(data); i++) out[i] = data[i]; - return sizeof(data); } - } - int vl_video_encoder_destory(long handle) { (void)handle; values[12]++; return 1; } - int test_get(int index) { return values[index]; } - void test_set_mode(int value) { mode = value; } - "#; - - #[cfg(unix)] - fn build_fixture(directory: &Path, name: &str, source: &str) -> PathBuf { - let source_path = directory.join(format!("{name}.c")); - let library_path = directory.join(format!("lib{name}.so")); - std::fs::write(&source_path, source).unwrap(); - let status = Command::new("cc") - .args(["-shared", "-fPIC"]) - .arg(&source_path) - .arg("-o") - .arg(&library_path) - .status() - .unwrap(); - assert!(status.success()); - library_path - } - - #[test] - fn validates_geometry_fps_and_nv12_size() { - let valid = AmlencConfig { - codec: AmlencCodec::H264, - resolution: Resolution::new(1920, 1080), - fps: 60, - bitrate_kbps: 8_000, - gop: 60, - }; - assert!(valid.validate().is_ok()); - assert_eq!(nv12_frame_size(valid.resolution).unwrap(), 3_110_400); - - for invalid in [ - AmlencConfig { - resolution: Resolution::new(1919, 1080), - ..valid - }, - AmlencConfig { - resolution: Resolution::new(1920, 1079), - ..valid - }, - AmlencConfig { - resolution: Resolution::new(2560, 1440), - ..valid - }, - AmlencConfig { fps: 61, ..valid }, - ] { - assert!(invalid.validate().is_err()); - } - } - - #[test] - fn recognizes_vendor_and_mainline_gxm_compatibles() { - assert!(is_s912_gxm_compatible(b"amlogic, Gxm\0khadas,kvim2")); - assert!(is_s912_gxm_compatible( - b"amlogic,q200\0amlogic,s912\0amlogic,meson-gxm" - )); - assert!(!is_s912_gxm_compatible(b"rockchip,rk3588")); - } - - #[test] - fn validates_abi_marker() { - let path = Path::new("libvpcodec.so"); - assert!(validate_abi_version(AMLENC_ABI_VERSION, path).is_ok()); - assert!(validate_abi_version(0, path).is_err()); - } - - #[test] - fn parses_h264_idr_and_parameter_sets() { - let data = [0, 0, 0, 1, 0x67, 1, 0, 0, 1, 0x68, 2, 0, 0, 0, 1, 0x65, 3]; - assert!(is_keyframe(AmlencCodec::H264, &data)); - assert!(has_parameter_sets(AmlencCodec::H264, &data)); - assert!(!is_keyframe(AmlencCodec::H264, &[0, 0, 1, 0x41])); - } - - #[test] - fn parses_h265_irap_and_parameter_sets() { - let data = [ - 0, - 0, - 1, - 32 << 1, - 1, - 0, - 0, - 1, - 33 << 1, - 1, - 0, - 0, - 1, - 34 << 1, - 1, - 0, - 0, - 1, - 19 << 1, - 1, - ]; - assert!(is_keyframe(AmlencCodec::H265, &data)); - assert!(has_parameter_sets(AmlencCodec::H265, &data)); - assert!(!is_keyframe(AmlencCodec::H265, &[0, 0, 1, 1 << 1, 1])); - } - - #[test] - #[cfg(unix)] - fn loads_symbols_maps_both_abis_and_recovers() { - let _test_instance = TEST_INSTANCE_MUTEX.lock().unwrap(); - let directory = tempfile::tempdir().unwrap(); - let h264_path = build_fixture(directory.path(), "amlenc_h264", H264_FIXTURE); - let h265_path = build_fixture(directory.path(), "amlenc_h265", H265_FIXTURE); - - type GetFn = unsafe extern "C" fn(c_int) -> c_int; - type SetModeFn = unsafe extern "C" fn(c_int); - type FailOnceFn = unsafe extern "C" fn(); - - // Keep this second dlopen alive so the fixture's counters remain available. - let h264_control = unsafe { Library::new(&h264_path) }.unwrap(); - let h264_get: GetFn = unsafe { *h264_control.get(b"test_get\0").unwrap() }; - let h264_set_mode: SetModeFn = unsafe { *h264_control.get(b"test_set_mode\0").unwrap() }; - let h264_fail_once: FailOnceFn = unsafe { *h264_control.get(b"test_fail_once\0").unwrap() }; - - let resolution = Resolution::new(640, 480); - let frame = vec![0x80; nv12_frame_size(resolution).unwrap()]; - { - let mut encoder = AmlencEncoder::with_library( - AmlencConfig { - codec: AmlencCodec::H264, - resolution, - fps: 60, - bitrate_kbps: 2_000, - gop: 60, - }, - &h264_path, - ) - .unwrap(); - assert!(encoder.encode_raw(&frame).unwrap().unwrap().1); - // SAFETY: indices and fixture signatures are fixed above. - unsafe { - assert_eq!(h264_get(1), CODEC_ID_H264); - assert_eq!(h264_get(4), 60); - assert_eq!(h264_get(5), 2_000_000); - assert_eq!(h264_get(6), 0); - assert_eq!(h264_get(7), IMG_FMT_NV12); - assert_eq!(h264_get(9), FRAME_TYPE_AUTO); - assert_eq!(h264_get(10), MIN_OUTPUT_BUFFER_SIZE as c_int); - assert_eq!(h264_get(11), H264_NV12_FORMAT); - - h264_fail_once(); - } - assert!(encoder.encode_raw(&frame).unwrap().is_some()); - unsafe { assert_eq!(h264_get(0), 2) }; - - unsafe { h264_set_mode(2) }; - encoder.request_keyframe(); - assert!(encoder.encode_raw(&frame).unwrap().is_none()); - unsafe { assert_eq!(h264_get(9), FRAME_TYPE_AUTO) }; - unsafe { assert_eq!(h264_get(0), 3) }; - assert!(encoder.encode_raw(&frame).unwrap().is_none()); - unsafe { assert_eq!(h264_get(9), FRAME_TYPE_AUTO) }; - assert!(encoder.encode_raw(&frame).unwrap().is_none()); - unsafe { assert_eq!(h264_get(0), 3) }; - - encoder.last_output = Instant::now() - OUTPUT_STALL_TIMEOUT; - assert!(encoder.encode_raw(&frame).unwrap().is_none()); - unsafe { assert_eq!(h264_get(0), 4) }; - - unsafe { h264_set_mode(0) }; - encoder.set_bitrate(3_000).unwrap(); - assert!(encoder.encode_raw(&frame).unwrap().unwrap().1); - unsafe { - assert_eq!(h264_get(5), 3_000_000); - assert_eq!(h264_get(9), FRAME_TYPE_AUTO); - assert_eq!(h264_get(0), 5); - } - } - - let h265_control = unsafe { Library::new(&h265_path) }.unwrap(); - let h265_get: GetFn = unsafe { *h265_control.get(b"test_get\0").unwrap() }; - let h265_set_mode: SetModeFn = unsafe { *h265_control.get(b"test_set_mode\0").unwrap() }; - { - let mut encoder = AmlencEncoder::with_library( - AmlencConfig { - codec: AmlencCodec::H265, - resolution, - fps: 30, - bitrate_kbps: 1_500, - gop: 30, - }, - &h265_path, - ) - .unwrap(); - assert!(encoder.encode_raw(&frame).unwrap().unwrap().1); - unsafe { - assert_eq!(h265_get(1), CODEC_ID_H265); - assert_eq!(h265_get(4), 30); - assert_eq!(h265_get(5), 1_500_000); - assert_eq!(h265_get(9), FRAME_TYPE_IDR); - assert_eq!(h265_get(10), MIN_OUTPUT_BUFFER_SIZE as c_int); - assert_eq!(h265_get(11), H265_NV12_FORMAT); - h265_set_mode(3); - } - let error = encoder.encode_raw(&frame).unwrap_err().to_string(); - assert!(error.contains("oversized output")); - } - } - - #[test] - #[cfg(unix)] - fn rejects_unpatched_library_without_abi_symbol() { - let _test_instance = TEST_INSTANCE_MUTEX.lock().unwrap(); - let directory = tempfile::tempdir().unwrap(); - let path = build_fixture( - directory.path(), - "unpatched_amlenc", - "long vl_video_encoder_init(void) { return 1; }", - ); - let error = AmlencEncoder::with_library( - AmlencConfig { - codec: AmlencCodec::H264, - resolution: Resolution::new(640, 480), - fps: 30, - bitrate_kbps: 1_000, - gop: 30, - }, - path, - ) - .err() - .expect("unpatched library must be rejected") - .to_string(); - assert!(error.contains("one_kvm_amlenc_abi_version")); - } -} diff --git a/src/video/codec/h264.rs b/src/video/codec/h264.rs index 5ebc31e6..13d7a3a8 100644 --- a/src/video/codec/h264.rs +++ b/src/video/codec/h264.rs @@ -48,8 +48,6 @@ pub enum H264EncoderType { Rkmpp, /// V4L2 M2M (ARM generic) - requires hwcodec extension V4l2M2m, - /// Amlogic S912/GXM AMLENC - Amlogic, /// Software encoding (libx264/openh264) Software, /// No encoder available @@ -66,7 +64,6 @@ impl std::fmt::Display for H264EncoderType { H264EncoderType::Vaapi => write!(f, "VAAPI"), H264EncoderType::Rkmpp => write!(f, "RKMPP"), H264EncoderType::V4l2M2m => write!(f, "V4L2 M2M"), - H264EncoderType::Amlogic => write!(f, "AMLENC"), H264EncoderType::Software => write!(f, "Software"), H264EncoderType::None => write!(f, "None"), } @@ -83,7 +80,6 @@ impl From for H264EncoderType { EncoderBackend::Vaapi => H264EncoderType::Vaapi, EncoderBackend::Rkmpp => H264EncoderType::Rkmpp, EncoderBackend::V4l2m2m => H264EncoderType::V4l2M2m, - EncoderBackend::Amlogic => H264EncoderType::Amlogic, EncoderBackend::Software => H264EncoderType::Software, } } diff --git a/src/video/codec/h265.rs b/src/video/codec/h265.rs index dd5df5e7..08f3dbed 100644 --- a/src/video/codec/h265.rs +++ b/src/video/codec/h265.rs @@ -45,8 +45,6 @@ pub enum H265EncoderType { Rkmpp, /// V4L2 M2M (ARM generic) V4l2M2m, - /// Amlogic S912/GXM AMLENC - Amlogic, /// Software encoder (libx265) Software, /// No encoder available @@ -63,7 +61,6 @@ impl std::fmt::Display for H265EncoderType { H265EncoderType::Vaapi => write!(f, "VAAPI"), H265EncoderType::Rkmpp => write!(f, "RKMPP"), H265EncoderType::V4l2M2m => write!(f, "V4L2 M2M"), - H265EncoderType::Amlogic => write!(f, "AMLENC"), H265EncoderType::Software => write!(f, "Software"), H265EncoderType::None => write!(f, "None"), } @@ -79,7 +76,6 @@ impl From for H265EncoderType { EncoderBackend::Vaapi => H265EncoderType::Vaapi, EncoderBackend::Rkmpp => H265EncoderType::Rkmpp, EncoderBackend::V4l2m2m => H265EncoderType::V4l2M2m, - EncoderBackend::Amlogic => H265EncoderType::Amlogic, EncoderBackend::Software => H265EncoderType::Software, } } diff --git a/src/video/codec/mod.rs b/src/video/codec/mod.rs index 04d8823f..ba0ab378 100644 --- a/src/video/codec/mod.rs +++ b/src/video/codec/mod.rs @@ -3,7 +3,6 @@ use hwcodec::common::DataFormat; use hwcodec::ffmpeg_ram::CodecInfo; -pub mod amlenc; pub mod convert; pub mod h264; @@ -20,7 +19,6 @@ pub mod vp9; #[cfg(all(feature = "desktop", any(target_arch = "aarch64", target_arch = "arm")))] pub mod mjpeg_rkmpp; -pub use amlenc::{AmlencCodec, AmlencConfig, AmlencEncoder}; pub use convert::{MjpegToNv12Decoder, PixelConverter, Yuv420pBuffer}; pub use h264::{H264Config, H264Encoder, H264EncoderType, H264InputFormat}; pub use h265::{H265Config, H265Encoder, H265EncoderType, H265InputFormat}; diff --git a/src/video/codec/registry.rs b/src/video/codec/registry.rs index 3baa50c8..d12bb663 100644 --- a/src/video/codec/registry.rs +++ b/src/video/codec/registry.rs @@ -10,17 +10,11 @@ use std::sync::OnceLock; use std::time::Duration; use tracing::{debug, info, warn}; -use super::amlenc::{self, AmlencCodec, AMLENC_H264_CODEC_NAME, AMLENC_H265_CODEC_NAME}; - use hwcodec::common::{DataFormat, Quality, RateControl}; use hwcodec::ffmpeg::{resolve_pixel_format, AVPixelFormat}; use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder}; use hwcodec::ffmpeg_ram::CodecInfo; -// Keep native AMLENC behind the highest-priority desktop GPU backends while -// ensuring it is selected before hwcodec's software priority (3). -const AMLENC_PRIORITY: i32 = 2; - /// Video encoder format type #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum VideoEncoderType { @@ -102,8 +96,6 @@ pub enum EncoderBackend { Rkmpp, /// V4L2 Memory-to-Memory (ARM) V4l2m2m, - /// Amlogic S912/GXM vendor AMLENC - Amlogic, /// Software encoding (libx264, libx265, libvpx) Software, } @@ -123,8 +115,6 @@ impl EncoderBackend { EncoderBackend::Rkmpp } else if name.contains("v4l2m2m") { EncoderBackend::V4l2m2m - } else if name.contains("amlenc") { - EncoderBackend::Amlogic } else { EncoderBackend::Software } @@ -144,7 +134,6 @@ impl EncoderBackend { EncoderBackend::Amf => "AMF", EncoderBackend::Rkmpp => "RKMPP", EncoderBackend::V4l2m2m => "V4L2 M2M", - EncoderBackend::Amlogic => "AMLENC", EncoderBackend::Software => "Software", } } @@ -159,7 +148,6 @@ impl EncoderBackend { "amf" => Some(EncoderBackend::Amf), "rkmpp" => Some(EncoderBackend::Rkmpp), "v4l2m2m" | "v4l2" => Some(EncoderBackend::V4l2m2m), - "amlogic" | "amlenc" => Some(EncoderBackend::Amlogic), "software" | "cpu" => Some(EncoderBackend::Software), _ => None, } @@ -286,79 +274,6 @@ impl EncoderRegistry { } } - fn detect_amlenc(&mut self) { - match amlenc::system_is_s912_gxm() { - Ok(true) => {} - Ok(false) => { - debug!("AMLENC skipped: host is not Linux/aarch64 S912/GXM"); - return; - } - Err(error) => { - warn!("AMLENC skipped: {}", error); - return; - } - } - - self.detect_amlenc_candidates( - true, - |codec| std::path::Path::new(codec.device_node()).exists(), - amlenc::smoke_test, - ); - } - - fn detect_amlenc_candidates( - &mut self, - compatible: bool, - mut node_exists: NodeExists, - mut smoke_test: SmokeTest, - ) where - NodeExists: FnMut(AmlencCodec) -> bool, - SmokeTest: FnMut(AmlencCodec) -> crate::error::Result<()>, - { - if !compatible { - return; - } - - for (codec, format, codec_name) in [ - ( - AmlencCodec::H264, - VideoEncoderType::H264, - AMLENC_H264_CODEC_NAME, - ), - ( - AmlencCodec::H265, - VideoEncoderType::H265, - AMLENC_H265_CODEC_NAME, - ), - ] { - let node = codec.device_node(); - if !node_exists(codec) { - warn!( - "AMLENC {} unavailable: device node {} is missing", - format, node - ); - continue; - } - - match smoke_test(codec) { - Ok(()) => { - self.encoders - .entry(format) - .or_default() - .push(AvailableEncoder { - format, - codec_name: codec_name.to_string(), - backend: EncoderBackend::Amlogic, - priority: AMLENC_PRIORITY, - is_hardware: true, - }); - info!("Registered native AMLENC encoder: {}", codec_name); - } - Err(error) => warn!("AMLENC {} unavailable ({}): {}", format, node, error), - } - } - } - /// Get the global registry instance /// /// The registry is initialized lazily on first access with 1280x720 detection. @@ -426,8 +341,6 @@ impl EncoderRegistry { } } - self.detect_amlenc(); - // Sort encoders by priority (lower is better) for encoders in self.encoders.values_mut() { encoders.sort_by_key(|e| e.priority); @@ -624,14 +537,6 @@ mod tests { EncoderBackend::from_codec_name("libx264"), EncoderBackend::Software ); - assert_eq!( - EncoderBackend::from_codec_name("h264_amlenc"), - EncoderBackend::Amlogic - ); - assert_eq!( - EncoderBackend::from_str("amlogic"), - Some(EncoderBackend::Amlogic) - ); } #[test] @@ -656,65 +561,4 @@ mod tests { println!("Available formats: {:?}", registry.available_formats(false)); println!("Selectable formats: {:?}", registry.selectable_formats()); } - - #[test] - fn test_amlenc_registration_prerequisite_matrix() { - let ok = |_codec| Ok(()); - - let mut incompatible = EncoderRegistry::new(); - incompatible.detect_amlenc_candidates(false, |_| true, ok); - assert!(incompatible.encoders.is_empty()); - - let mut no_nodes = EncoderRegistry::new(); - no_nodes.detect_amlenc_candidates(true, |_| false, ok); - assert!(no_nodes.encoders.is_empty()); - - for reason in ["library missing", "ABI marker missing"] { - let mut rejected = EncoderRegistry::new(); - rejected.detect_amlenc_candidates( - true, - |_| true, - |_| Err(crate::error::AppError::VideoError(reason.to_string())), - ); - assert!(rejected.encoders.is_empty()); - } - - let mut h264_only = EncoderRegistry::new(); - h264_only.detect_amlenc_candidates(true, |codec| codec == AmlencCodec::H264, ok); - assert!(h264_only - .encoder_with_backend(VideoEncoderType::H264, EncoderBackend::Amlogic) - .is_some()); - assert!(h264_only - .encoder_with_backend(VideoEncoderType::H265, EncoderBackend::Amlogic) - .is_none()); - - let mut both = EncoderRegistry::new(); - both.detect_amlenc_candidates(true, |_| true, ok); - assert!(both - .encoder_with_backend(VideoEncoderType::H264, EncoderBackend::Amlogic) - .is_some()); - assert!(both - .encoder_with_backend(VideoEncoderType::H265, EncoderBackend::Amlogic) - .is_some()); - - both.encoders - .entry(VideoEncoderType::H264) - .or_default() - .push(AvailableEncoder { - format: VideoEncoderType::H264, - codec_name: "libx264".to_string(), - backend: EncoderBackend::Software, - priority: 3, - is_hardware: false, - }); - both.encoders - .get_mut(&VideoEncoderType::H264) - .unwrap() - .sort_by_key(|encoder| encoder.priority); - assert_eq!( - both.best_available_encoder(VideoEncoderType::H264) - .map(|encoder| encoder.backend), - Some(EncoderBackend::Amlogic) - ); - } } diff --git a/src/video/codec/self_check.rs b/src/video/codec/self_check.rs index 8325e95a..be6eed32 100644 --- a/src/video/codec/self_check.rs +++ b/src/video/codec/self_check.rs @@ -3,8 +3,8 @@ use std::sync::mpsc; use std::time::{Duration, Instant}; use super::{ - AmlencCodec, AmlencConfig, AmlencEncoder, EncoderRegistry, H264Config, H264Encoder, H265Config, - H265Encoder, VP8Config, VP8Encoder, VP9Config, VP9Encoder, VideoEncoderType, + EncoderRegistry, H264Config, H264Encoder, H265Config, H265Encoder, VP8Config, VP8Encoder, + VP9Config, VP9Encoder, VideoEncoderType, }; use crate::error::{AppError, Result}; use crate::video::format::{PixelFormat, Resolution}; @@ -226,9 +226,6 @@ fn run_smoke_test( resolution: Resolution, codec_name_ffmpeg: &str, ) -> Result<()> { - if codec_name_ffmpeg.contains("amlenc") { - return run_amlenc_smoke_test(codec, resolution); - } match codec { VideoEncoderType::H264 => run_h264_smoke_test(resolution, codec_name_ffmpeg), VideoEncoderType::H265 => run_h265_smoke_test(resolution, codec_name_ffmpeg), @@ -237,37 +234,6 @@ fn run_smoke_test( } } -fn run_amlenc_smoke_test(codec: VideoEncoderType, resolution: Resolution) -> Result<()> { - let amlenc_codec = match codec { - VideoEncoderType::H264 => AmlencCodec::H264, - VideoEncoderType::H265 => AmlencCodec::H265, - _ => { - return Err(AppError::VideoError( - "AMLENC only supports H.264 and H.265".to_string(), - )) - } - }; - let mut encoder = AmlencEncoder::new(AmlencConfig { - codec: amlenc_codec, - resolution, - fps: 30, - bitrate_kbps: bitrate_kbps_for_resolution(resolution), - gop: 30, - })?; - let frame_len = PixelFormat::Nv12.frame_size(resolution).ok_or_else(|| { - AppError::VideoError("Cannot calculate AMLENC NV12 self-check size".to_string()) - })?; - let frame = build_nv12_test_frame(resolution, frame_len); - for _ in 0..SELF_CHECK_FRAME_ATTEMPTS { - if encoder.encode_raw(&frame)?.is_some() { - return Ok(()); - } - } - Err(AppError::VideoError( - "AMLENC produced no output after multiple frames".to_string(), - )) -} - fn run_h264_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> { let mut encoder = H264Encoder::with_codec( H264Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)), diff --git a/src/video/pipeline/encoder_state.rs b/src/video/pipeline/encoder_state.rs index 02e9a032..a7b63609 100644 --- a/src/video/pipeline/encoder_state.rs +++ b/src/video/pipeline/encoder_state.rs @@ -1,5 +1,4 @@ use crate::error::{AppError, Result}; -use crate::video::codec::amlenc::{AmlencCodec, AmlencConfig, AmlencEncoder}; use crate::video::codec::convert::{MjpegToNv12Decoder, Nv12Converter, PixelConverter}; use crate::video::codec::h264::{H264Config, H264Encoder, H264InputFormat}; use crate::video::codec::h265::{H265Config, H265Encoder, H265InputFormat}; @@ -117,47 +116,6 @@ impl VideoEncoderTrait for H265EncoderWrapper { } } -struct AmlencEncoderWrapper(AmlencEncoder); - -impl VideoEncoderTrait for AmlencEncoderWrapper { - fn encode_raw(&mut self, data: &[u8], _pts_ms: i64) -> Result> { - Ok(match self.0.encode_raw(data)? { - Some((data, keyframe)) => vec![EncodedFrame { - data, - key: i32::from(keyframe), - }], - None => Vec::new(), - }) - } - - fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> { - self.0.set_bitrate(bitrate_kbps) - } - - fn codec_name(&self) -> &str { - self.0.codec_name() - } - - fn request_keyframe(&mut self) { - self.0.request_keyframe() - } -} - -fn create_amlenc_encoder( - config: &SharedVideoPipelineConfig, - codec: AmlencCodec, -) -> Result> { - let encoder = AmlencEncoder::new(AmlencConfig { - codec, - resolution: config.resolution, - fps: config.fps, - bitrate_kbps: config.bitrate_kbps(), - gop: config.gop_size(), - })?; - info!("Created native AMLENC encoder: {}", encoder.codec_name()); - Ok(Box::new(AmlencEncoderWrapper(encoder))) -} - struct VP8EncoderWrapper(VP8Encoder); impl VideoEncoderTrait for VP8EncoderWrapper { @@ -231,7 +189,7 @@ fn create_mjpeg_decoder(resolution: Resolution) -> Result<(MjpegDecoderKind, Pix Ok((libyuv_mjpeg_decoder(resolution), PixelFormat::Nv12)) } -/// Amlogic hardware encoders and libjpeg-turbo use independent CPU/hardware +/// V4L2 M2M hardware encoding and libjpeg-turbo use independent CPU/hardware /// resources. Decode MJPEG in the capture worker so encoding the previous NV12 /// frame can overlap with decoding the next frame. pub(super) fn should_parallel_decode_mjpeg(config: &SharedVideoPipelineConfig) -> bool { @@ -248,12 +206,7 @@ pub(super) fn should_parallel_decode_mjpeg(config: &SharedVideoPipelineConfig) - Some(backend) => registry.encoder_with_backend(config.output_codec, backend), None => registry.best_available_encoder(config.output_codec), }; - selected.is_some_and(|encoder| { - matches!( - encoder.backend, - EncoderBackend::Amlogic | EncoderBackend::V4l2m2m - ) - }) + selected.is_some_and(|encoder| encoder.backend == EncoderBackend::V4l2m2m) } pub(super) fn build_encoder_state( @@ -420,80 +373,70 @@ pub(super) fn build_encoder_state( let encoder: Box = match config.output_codec { VideoEncoderType::H264 => { let codec_name = selected_codec_name.clone(); - if codec_name == crate::video::codec::amlenc::AMLENC_H264_CODEC_NAME { - create_amlenc_encoder(config, AmlencCodec::H264)? - } else { - let direct_input_format = - h264_direct_input_format(&codec_name, pipeline_input_format); - let input_format = direct_input_format.unwrap_or_else(|| { - if codec_name.contains("libx264") { - H264InputFormat::Yuv420p - } else { - H264InputFormat::Nv12 - } - }); - - if use_rkmpp_direct { - info!( - "Creating H264 encoder with RKMPP backend for {} direct input (codec: {})", - config.input_format, codec_name - ); - } else if let Some(ref backend) = config.encoder_backend { - info!( - "Creating H264 encoder with backend {:?} (codec: {})", - backend, codec_name - ); + let direct_input_format = h264_direct_input_format(&codec_name, pipeline_input_format); + let input_format = direct_input_format.unwrap_or_else(|| { + if codec_name.contains("libx264") { + H264InputFormat::Yuv420p + } else { + H264InputFormat::Nv12 } + }); - create_h264_encoder(config, input_format, &codec_name)? + if use_rkmpp_direct { + info!( + "Creating H264 encoder with RKMPP backend for {} direct input (codec: {})", + config.input_format, codec_name + ); + } else if let Some(ref backend) = config.encoder_backend { + info!( + "Creating H264 encoder with backend {:?} (codec: {})", + backend, codec_name + ); } + + create_h264_encoder(config, input_format, &codec_name)? } VideoEncoderType::H265 => { let codec_name = selected_codec_name.clone(); - if codec_name == crate::video::codec::amlenc::AMLENC_H265_CODEC_NAME { - create_amlenc_encoder(config, AmlencCodec::H265)? - } else { - let direct_input_format = - h265_direct_input_format(&codec_name, pipeline_input_format); - let input_format = direct_input_format.unwrap_or_else(|| { - if codec_name.contains("libx265") { - H265InputFormat::Yuv420p - } else { - H265InputFormat::Nv12 - } - }); - - if use_rkmpp_direct { - info!( - "Creating H265 encoder with RKMPP backend for {} direct input (codec: {})", - config.input_format, codec_name - ); - } else if let Some(ref backend) = config.encoder_backend { - info!( - "Creating H265 encoder with backend {:?} (codec: {})", - backend, codec_name - ); + let direct_input_format = h265_direct_input_format(&codec_name, pipeline_input_format); + let input_format = direct_input_format.unwrap_or_else(|| { + if codec_name.contains("libx265") { + H265InputFormat::Yuv420p + } else { + H265InputFormat::Nv12 } + }); - let encoder = H265Encoder::with_codec( - H265Config { - base: EncoderConfig { - resolution: config.resolution, - input_format: config.input_format, - quality: config.bitrate_kbps(), - fps: config.fps, - gop_size: config.gop_size(), - }, - bitrate_kbps: config.bitrate_kbps(), - gop_size: config.gop_size(), - fps: config.fps, - input_format, - }, - &codec_name, - )?; - info!("Created H265 encoder: {}", encoder.codec_name()); - Box::new(H265EncoderWrapper(encoder)) + if use_rkmpp_direct { + info!( + "Creating H265 encoder with RKMPP backend for {} direct input (codec: {})", + config.input_format, codec_name + ); + } else if let Some(ref backend) = config.encoder_backend { + info!( + "Creating H265 encoder with backend {:?} (codec: {})", + backend, codec_name + ); } + + let encoder = H265Encoder::with_codec( + H265Config { + base: EncoderConfig { + resolution: config.resolution, + input_format: config.input_format, + quality: config.bitrate_kbps(), + fps: config.fps, + gop_size: config.gop_size(), + }, + bitrate_kbps: config.bitrate_kbps(), + gop_size: config.gop_size(), + fps: config.fps, + input_format, + }, + &codec_name, + )?; + info!("Created H265 encoder: {}", encoder.codec_name()); + Box::new(H265EncoderWrapper(encoder)) } VideoEncoderType::VP8 => { let codec_name = selected_codec_name.clone(); @@ -528,9 +471,7 @@ pub(super) fn build_encoder_state( }; let codec_name = encoder.codec_name(); - let use_direct_input = if codec_name.contains("amlenc") { - pipeline_input_format == PixelFormat::Nv12 - } else if codec_name.contains("rkmpp") { + let use_direct_input = if codec_name.contains("rkmpp") { matches!( pipeline_input_format, PixelFormat::Yuyv diff --git a/src/video/pipeline/shared.rs b/src/video/pipeline/shared.rs index 8fb51921..6cacd308 100644 --- a/src/video/pipeline/shared.rs +++ b/src/video/pipeline/shared.rs @@ -30,7 +30,6 @@ use super::encoder_state::{build_encoder_state, should_parallel_decode_mjpeg, En /// Grace period before auto-stopping pipeline when no subscribers (in seconds) const AUTO_STOP_GRACE_PERIOD_SECS: u64 = 3; -const AMLENC_MAX_FPS: u32 = 60; /// After this many consecutive timeouts, log a prominent warning. const CAPTURE_TIMEOUT_RESTART_THRESHOLD: u32 = 5; const CAPTURE_TIMEOUT_SOFT_RESTART_THRESHOLD: u32 = 3; @@ -56,9 +55,6 @@ use crate::video::device::parse_bridge_kind; use crate::video::device::VideoControlMode; use crate::video::format::{PixelFormat, Resolution}; -fn amlenc_supported_fps(requested_fps: u32) -> u32 { - requested_fps.min(AMLENC_MAX_FPS) -} use crate::video::frame::{FrameBuffer, FrameBufferPool, VideoFrame}; use crate::video::recovery::{wait_for_source_change, CaptureRecoveryPolicy}; use crate::video::signal::SignalStatus; @@ -287,9 +283,8 @@ pub struct SharedVideoPipeline { stats: Mutex, running: watch::Sender, running_rx: watch::Receiver, - /// Becomes true only after the synchronous encoder worker has dropped its - /// vendor handles. Capture teardown alone is not sufficient for AMLENC: - /// a blocked dequeue/encode can otherwise overlap the next pipeline. + /// Becomes true only after the synchronous encoder worker has exited and + /// dropped its encoder handles. encoder_done: watch::Sender, encoder_done_rx: watch::Receiver, h264_profile_level_id: watch::Sender>, @@ -554,17 +549,6 @@ impl SharedVideoPipeline { let mut config = self.config.read().await.clone(); let parallel_mjpeg_decode = should_parallel_decode_mjpeg(&config); - if parallel_mjpeg_decode { - let stable_fps = amlenc_supported_fps(config.fps); - if stable_fps != config.fps { - warn!( - "Limiting S912 AMLENC capture at {}x{} from {} to {} fps (hardware limit)", - config.resolution.width, config.resolution.height, config.fps, stable_fps - ); - config.fps = stable_fps; - *self.config.write().await = config.clone(); - } - } { let mut last = self.last_state_notification.lock(); *last = None; @@ -596,9 +580,6 @@ impl SharedVideoPipeline { } config.resolution = negotiated_res; config.input_format = negotiated_fmt; - if parallel_mjpeg_decode { - config.fps = amlenc_supported_fps(config.fps); - } if previous != (config.resolution, config.input_format, config.fps) { info!( "Negotiated capture {}x{} {:?} @ {} fps (configured {}x{} {:?} @ {} fps) — aligning encoder to source", @@ -741,8 +722,7 @@ impl SharedVideoPipeline { } pipeline.clear_cmd_tx(); - // Dropping encoder_state here releases AMLENC before a caller - // is allowed to construct a replacement pipeline. + // Release encoder resources before allowing a replacement pipeline. drop(encoder_state); let _ = pipeline.encoder_done.send(true); }); @@ -1604,11 +1584,6 @@ mod tests { let h265 = SharedVideoPipelineConfig::h265(Resolution::HD720, BitratePreset::Speed); assert_eq!(h265.output_codec, VideoEncoderType::H265); - - assert_eq!(amlenc_supported_fps(30), 30); - assert_eq!(amlenc_supported_fps(50), 50); - assert_eq!(amlenc_supported_fps(60), 60); - assert_eq!(amlenc_supported_fps(120), 60); } #[test] diff --git a/src/webrtc/webrtc_streamer.rs b/src/webrtc/webrtc_streamer.rs index 27884dc4..852c403d 100644 --- a/src/webrtc/webrtc_streamer.rs +++ b/src/webrtc/webrtc_streamer.rs @@ -14,7 +14,6 @@ use crate::events::{EventBus, StreamKind, SystemEvent}; use crate::hid::HidController; use crate::video::capture::DEFAULT_CAPTURE_BUFFER_COUNT; use crate::video::codec::h264_bitstream; -use crate::video::codec::EncoderRegistry; use crate::video::device::{ enumerate_devices, select_recovery_device, VideoControlMode, VideoDevice, VideoDeviceInfo, VideoDeviceRecoveryHint, @@ -1314,26 +1313,6 @@ impl WebRtcStreamer { }; if pipeline_running { - let pipeline = self.video_pipeline.read().await.clone(); - if let Some(pipeline) = pipeline { - let pipeline_config = pipeline.config().await; - let selected_backend = pipeline_config.encoder_backend.or_else(|| { - EncoderRegistry::global() - .best_available_encoder(pipeline_config.output_codec) - .map(|encoder| encoder.backend) - }); - if pipeline_config.input_format == PixelFormat::Mjpeg - && selected_backend == Some(EncoderBackend::Amlogic) - { - info!( - "Applying AMLENC bitrate {} in the encoder worker without restarting MJPEG decode", - preset - ); - pipeline.set_bitrate_preset(preset).await?; - return Ok(()); - } - } - info!("Restarting video pipeline to apply new bitrate: {}", preset); self.stop_video_pipeline_and_release().await?; diff --git a/web/src/types/generated.ts b/web/src/types/generated.ts index 890ea13d..5223b850 100644 --- a/web/src/types/generated.ts +++ b/web/src/types/generated.ts @@ -148,7 +148,6 @@ export enum EncoderType { Amf = "amf", Rkmpp = "rkmpp", V4l2m2m = "v4l2m2m", - Amlogic = "amlogic", } export type BitratePreset =