mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 11:04:25 +08:00
feat: 初步增加 Windows 支持
This commit is contained in:
865
src/video/codec/convert.rs
Normal file
865
src/video/codec/convert.rs
Normal file
@@ -0,0 +1,865 @@
|
||||
//! Pixel format conversion utilities
|
||||
//!
|
||||
//! This module provides SIMD-accelerated color space conversion using libyuv.
|
||||
//! Primary use case: YUYV (from V4L2 capture) → YUV420P/NV12 (for H264 encoding)
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
/// YUV420P buffer with separate Y, U, V planes
|
||||
pub struct Yuv420pBuffer {
|
||||
/// Raw buffer containing all planes
|
||||
data: Vec<u8>,
|
||||
/// Width of the frame
|
||||
width: u32,
|
||||
/// Height of the frame
|
||||
height: u32,
|
||||
/// Y plane offset (always 0)
|
||||
y_offset: usize,
|
||||
/// U plane offset
|
||||
u_offset: usize,
|
||||
/// V plane offset
|
||||
v_offset: usize,
|
||||
}
|
||||
|
||||
impl Yuv420pBuffer {
|
||||
/// Create a new YUV420P buffer for the given resolution
|
||||
pub fn new(resolution: Resolution) -> Self {
|
||||
let width = resolution.width;
|
||||
let height = resolution.height;
|
||||
|
||||
// YUV420P: Y = width*height, U = width*height/4, V = width*height/4
|
||||
let y_size = (width * height) as usize;
|
||||
let uv_size = y_size / 4;
|
||||
let total_size = y_size + uv_size * 2;
|
||||
|
||||
Self {
|
||||
data: vec![0u8; total_size],
|
||||
width,
|
||||
height,
|
||||
y_offset: 0,
|
||||
u_offset: y_size,
|
||||
v_offset: y_size + uv_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the raw buffer as bytes
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Get the raw buffer as mutable bytes
|
||||
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.data
|
||||
}
|
||||
|
||||
/// Get Y plane
|
||||
pub fn y_plane(&self) -> &[u8] {
|
||||
&self.data[self.y_offset..self.u_offset]
|
||||
}
|
||||
|
||||
/// Get Y plane mutable
|
||||
pub fn y_plane_mut(&mut self) -> &mut [u8] {
|
||||
let u_offset = self.u_offset;
|
||||
&mut self.data[self.y_offset..u_offset]
|
||||
}
|
||||
|
||||
/// Get U plane
|
||||
pub fn u_plane(&self) -> &[u8] {
|
||||
&self.data[self.u_offset..self.v_offset]
|
||||
}
|
||||
|
||||
/// Get U plane mutable
|
||||
pub fn u_plane_mut(&mut self) -> &mut [u8] {
|
||||
let v_offset = self.v_offset;
|
||||
let u_offset = self.u_offset;
|
||||
&mut self.data[u_offset..v_offset]
|
||||
}
|
||||
|
||||
/// Get V plane
|
||||
pub fn v_plane(&self) -> &[u8] {
|
||||
&self.data[self.v_offset..]
|
||||
}
|
||||
|
||||
/// Get V plane mutable
|
||||
pub fn v_plane_mut(&mut self) -> &mut [u8] {
|
||||
let v_offset = self.v_offset;
|
||||
&mut self.data[v_offset..]
|
||||
}
|
||||
|
||||
/// Get buffer length
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
/// Check if buffer is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.data.is_empty()
|
||||
}
|
||||
|
||||
/// Get resolution
|
||||
pub fn resolution(&self) -> Resolution {
|
||||
Resolution::new(self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
/// NV12 buffer with Y plane and interleaved UV plane
|
||||
pub struct Nv12Buffer {
|
||||
/// Raw buffer containing Y plane followed by interleaved UV plane
|
||||
data: Vec<u8>,
|
||||
/// Width of the frame
|
||||
width: u32,
|
||||
/// Height of the frame
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl Nv12Buffer {
|
||||
/// Create a new NV12 buffer for the given resolution
|
||||
pub fn new(resolution: Resolution) -> Self {
|
||||
let width = resolution.width;
|
||||
let height = resolution.height;
|
||||
// NV12: Y = width*height, UV = width*height/2 (interleaved)
|
||||
let y_size = (width * height) as usize;
|
||||
let uv_size = y_size / 2;
|
||||
let total_size = y_size + uv_size;
|
||||
|
||||
Self {
|
||||
data: vec![0u8; total_size],
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the raw buffer as bytes
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Get the raw buffer as mutable bytes
|
||||
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.data
|
||||
}
|
||||
|
||||
/// Get Y plane
|
||||
pub fn y_plane(&self) -> &[u8] {
|
||||
let y_size = (self.width * self.height) as usize;
|
||||
&self.data[..y_size]
|
||||
}
|
||||
|
||||
/// Get Y plane mutable
|
||||
pub fn y_plane_mut(&mut self) -> &mut [u8] {
|
||||
let y_size = (self.width * self.height) as usize;
|
||||
&mut self.data[..y_size]
|
||||
}
|
||||
|
||||
/// Get UV plane (interleaved)
|
||||
pub fn uv_plane(&self) -> &[u8] {
|
||||
let y_size = (self.width * self.height) as usize;
|
||||
&self.data[y_size..]
|
||||
}
|
||||
|
||||
/// Get UV plane mutable
|
||||
pub fn uv_plane_mut(&mut self) -> &mut [u8] {
|
||||
let y_size = (self.width * self.height) as usize;
|
||||
&mut self.data[y_size..]
|
||||
}
|
||||
|
||||
/// Get buffer length
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
/// Check if buffer is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.data.is_empty()
|
||||
}
|
||||
|
||||
/// Get resolution
|
||||
pub fn resolution(&self) -> Resolution {
|
||||
Resolution::new(self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
/// Pixel format converter using libyuv (SIMD accelerated)
|
||||
pub struct PixelConverter {
|
||||
/// Source format
|
||||
src_format: PixelFormat,
|
||||
/// Destination format
|
||||
dst_format: PixelFormat,
|
||||
/// Frame resolution
|
||||
resolution: Resolution,
|
||||
/// Output buffer (reused across conversions)
|
||||
output_buffer: Yuv420pBuffer,
|
||||
/// Scratch buffer for split chroma planes when converting semiplanar 4:2:2 / 4:4:4 input.
|
||||
uv_split_buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PixelConverter {
|
||||
fn new(src_format: PixelFormat, dst_format: PixelFormat, resolution: Resolution) -> Self {
|
||||
let max_uv_plane_size = (resolution.width * resolution.height) as usize;
|
||||
Self {
|
||||
src_format,
|
||||
dst_format,
|
||||
resolution,
|
||||
output_buffer: Yuv420pBuffer::new(resolution),
|
||||
uv_split_buffer: vec![0u8; max_uv_plane_size * 2],
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new converter for YUYV → YUV420P
|
||||
pub fn yuyv_to_yuv420p(resolution: Resolution) -> Self {
|
||||
Self::new(PixelFormat::Yuyv, PixelFormat::Yuv420, resolution)
|
||||
}
|
||||
|
||||
/// Create a new converter for UYVY → YUV420P
|
||||
pub fn uyvy_to_yuv420p(resolution: Resolution) -> Self {
|
||||
Self::new(PixelFormat::Uyvy, PixelFormat::Yuv420, resolution)
|
||||
}
|
||||
|
||||
/// Create a new converter for YVYU → YUV420P
|
||||
pub fn yvyu_to_yuv420p(resolution: Resolution) -> Self {
|
||||
Self::new(PixelFormat::Yvyu, PixelFormat::Yuv420, resolution)
|
||||
}
|
||||
|
||||
/// Create a new converter for NV12 → YUV420P
|
||||
pub fn nv12_to_yuv420p(resolution: Resolution) -> Self {
|
||||
Self::new(PixelFormat::Nv12, PixelFormat::Yuv420, resolution)
|
||||
}
|
||||
|
||||
/// Create a new converter for NV21 → YUV420P
|
||||
pub fn nv21_to_yuv420p(resolution: Resolution) -> Self {
|
||||
Self::new(PixelFormat::Nv21, PixelFormat::Yuv420, resolution)
|
||||
}
|
||||
|
||||
/// Create a new converter for NV16 → YUV420P
|
||||
pub fn nv16_to_yuv420p(resolution: Resolution) -> Self {
|
||||
Self::new(PixelFormat::Nv16, PixelFormat::Yuv420, resolution)
|
||||
}
|
||||
|
||||
/// Create a new converter for NV24 → YUV420P
|
||||
pub fn nv24_to_yuv420p(resolution: Resolution) -> Self {
|
||||
Self::new(PixelFormat::Nv24, PixelFormat::Yuv420, resolution)
|
||||
}
|
||||
|
||||
/// Create a new converter for YVU420 → YUV420P (swap U and V planes)
|
||||
pub fn yvu420_to_yuv420p(resolution: Resolution) -> Self {
|
||||
Self::new(PixelFormat::Yvu420, PixelFormat::Yuv420, resolution)
|
||||
}
|
||||
|
||||
/// Create a new converter for RGB24 → YUV420P
|
||||
pub fn rgb24_to_yuv420p(resolution: Resolution) -> Self {
|
||||
Self::new(PixelFormat::Rgb24, PixelFormat::Yuv420, resolution)
|
||||
}
|
||||
|
||||
/// Create a new converter for BGR24 → YUV420P
|
||||
pub fn bgr24_to_yuv420p(resolution: Resolution) -> Self {
|
||||
Self::new(PixelFormat::Bgr24, PixelFormat::Yuv420, resolution)
|
||||
}
|
||||
|
||||
/// Convert a frame and return reference to the output buffer
|
||||
pub fn convert(&mut self, input: &[u8]) -> Result<&[u8]> {
|
||||
let width = self.resolution.width as i32;
|
||||
let height = self.resolution.height as i32;
|
||||
let expected_size = self.output_buffer.len();
|
||||
|
||||
match (self.src_format, self.dst_format) {
|
||||
(PixelFormat::Yuyv, PixelFormat::Yuv420) => {
|
||||
libyuv::yuy2_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
|
||||
.map_err(|e| {
|
||||
AppError::VideoError(format!("libyuv conversion failed: {}", e))
|
||||
})?;
|
||||
}
|
||||
(PixelFormat::Uyvy, PixelFormat::Yuv420) => {
|
||||
libyuv::uyvy_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
|
||||
.map_err(|e| {
|
||||
AppError::VideoError(format!("libyuv conversion failed: {}", e))
|
||||
})?;
|
||||
}
|
||||
(PixelFormat::Nv12, PixelFormat::Yuv420) => {
|
||||
libyuv::nv12_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
|
||||
.map_err(|e| {
|
||||
AppError::VideoError(format!("libyuv conversion failed: {}", e))
|
||||
})?;
|
||||
}
|
||||
(PixelFormat::Nv21, PixelFormat::Yuv420) => {
|
||||
libyuv::nv21_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
|
||||
.map_err(|e| {
|
||||
AppError::VideoError(format!("libyuv conversion failed: {}", e))
|
||||
})?;
|
||||
}
|
||||
(PixelFormat::Nv16, PixelFormat::Yuv420) => {
|
||||
self.convert_nv16_to_yuv420p(input)?;
|
||||
}
|
||||
(PixelFormat::Nv24, PixelFormat::Yuv420) => {
|
||||
self.convert_nv24_to_yuv420p(input)?;
|
||||
}
|
||||
(PixelFormat::Rgb24, PixelFormat::Yuv420) => {
|
||||
libyuv::rgb24_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
|
||||
.map_err(|e| {
|
||||
AppError::VideoError(format!("libyuv conversion failed: {}", e))
|
||||
})?;
|
||||
}
|
||||
(PixelFormat::Bgr24, PixelFormat::Yuv420) => {
|
||||
libyuv::bgr24_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
|
||||
.map_err(|e| {
|
||||
AppError::VideoError(format!("libyuv conversion failed: {}", e))
|
||||
})?;
|
||||
}
|
||||
(PixelFormat::Yvyu, PixelFormat::Yuv420) => {
|
||||
// YVYU is not directly supported by libyuv, use software conversion
|
||||
self.convert_yvyu_to_yuv420p_sw(input)?;
|
||||
}
|
||||
(PixelFormat::Yvu420, PixelFormat::Yuv420) => {
|
||||
// YVU420 just swaps U and V planes
|
||||
self.convert_yvu420_to_yuv420p_sw(input)?;
|
||||
}
|
||||
(PixelFormat::Yuv420, PixelFormat::Yuv420) => {
|
||||
// No conversion needed, just copy
|
||||
if input.len() < expected_size {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"Input buffer too small: {} < {}",
|
||||
input.len(),
|
||||
expected_size
|
||||
)));
|
||||
}
|
||||
self.output_buffer
|
||||
.as_bytes_mut()
|
||||
.copy_from_slice(&input[..expected_size]);
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"Unsupported conversion: {} → {}",
|
||||
self.src_format, self.dst_format
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(self.output_buffer.as_bytes())
|
||||
}
|
||||
|
||||
/// Get output buffer length
|
||||
pub fn output_len(&self) -> usize {
|
||||
self.output_buffer.len()
|
||||
}
|
||||
|
||||
/// Get resolution
|
||||
pub fn resolution(&self) -> Resolution {
|
||||
self.resolution
|
||||
}
|
||||
|
||||
/// Software conversion for YVYU (not supported by libyuv)
|
||||
fn convert_yvyu_to_yuv420p_sw(&mut self, yvyu: &[u8]) -> Result<()> {
|
||||
let width = self.resolution.width as usize;
|
||||
let height = self.resolution.height as usize;
|
||||
let y_size = width * height;
|
||||
let uv_size = y_size / 4;
|
||||
let half_width = width / 2;
|
||||
|
||||
let data = self.output_buffer.as_bytes_mut();
|
||||
let (y_plane, uv_planes) = data.split_at_mut(y_size);
|
||||
let (u_plane, v_plane) = uv_planes.split_at_mut(uv_size);
|
||||
|
||||
for row in (0..height).step_by(2) {
|
||||
let yvyu_row0_offset = row * width * 2;
|
||||
let yvyu_row1_offset = (row + 1) * width * 2;
|
||||
let y_row0_offset = row * width;
|
||||
let y_row1_offset = (row + 1) * width;
|
||||
let uv_row_offset = (row / 2) * half_width;
|
||||
|
||||
for col in (0..width).step_by(2) {
|
||||
let yvyu_offset0 = yvyu_row0_offset + col * 2;
|
||||
let yvyu_offset1 = yvyu_row1_offset + col * 2;
|
||||
|
||||
// YVYU: Y0, V0, Y1, U0
|
||||
let y0_0 = yvyu[yvyu_offset0];
|
||||
let v0 = yvyu[yvyu_offset0 + 1];
|
||||
let y0_1 = yvyu[yvyu_offset0 + 2];
|
||||
let u0 = yvyu[yvyu_offset0 + 3];
|
||||
|
||||
let y1_0 = yvyu[yvyu_offset1];
|
||||
let v1 = yvyu[yvyu_offset1 + 1];
|
||||
let y1_1 = yvyu[yvyu_offset1 + 2];
|
||||
let u1 = yvyu[yvyu_offset1 + 3];
|
||||
|
||||
y_plane[y_row0_offset + col] = y0_0;
|
||||
y_plane[y_row0_offset + col + 1] = y0_1;
|
||||
y_plane[y_row1_offset + col] = y1_0;
|
||||
y_plane[y_row1_offset + col + 1] = y1_1;
|
||||
|
||||
let uv_idx = uv_row_offset + col / 2;
|
||||
u_plane[uv_idx] = ((u0 as u16 + u1 as u16) / 2) as u8;
|
||||
v_plane[uv_idx] = ((v0 as u16 + v1 as u16) / 2) as u8;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Software conversion for YVU420 (just swap U and V)
|
||||
fn convert_yvu420_to_yuv420p_sw(&mut self, yvu420: &[u8]) -> Result<()> {
|
||||
let width = self.resolution.width as usize;
|
||||
let height = self.resolution.height as usize;
|
||||
let y_size = width * height;
|
||||
let uv_size = y_size / 4;
|
||||
|
||||
let data = self.output_buffer.as_bytes_mut();
|
||||
let (y_plane, uv_planes) = data.split_at_mut(y_size);
|
||||
let (u_plane, v_plane) = uv_planes.split_at_mut(uv_size);
|
||||
|
||||
// Copy Y plane directly
|
||||
y_plane.copy_from_slice(&yvu420[..y_size]);
|
||||
|
||||
// In YVU420, V comes before U
|
||||
let v_src = &yvu420[y_size..y_size + uv_size];
|
||||
let u_src = &yvu420[y_size + uv_size..];
|
||||
|
||||
// Swap U and V
|
||||
u_plane.copy_from_slice(u_src);
|
||||
v_plane.copy_from_slice(v_src);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert NV16 (4:2:2 semiplanar) → YUV420P using libyuv split + I422 downsample
|
||||
fn convert_nv16_to_yuv420p(&mut self, nv16: &[u8]) -> Result<()> {
|
||||
let width = self.resolution.width as usize;
|
||||
let height = self.resolution.height as usize;
|
||||
let y_size = width * height;
|
||||
let uv_size = y_size;
|
||||
|
||||
if nv16.len() < y_size + uv_size {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"NV16 data too small: {} < {}",
|
||||
nv16.len(),
|
||||
y_size + uv_size
|
||||
)));
|
||||
}
|
||||
|
||||
let src_uv = &nv16[y_size..y_size + uv_size];
|
||||
let chroma_plane_size = y_size / 2;
|
||||
let (u_plane_422, rest) = self.uv_split_buffer.split_at_mut(chroma_plane_size);
|
||||
let (v_plane_422, _) = rest.split_at_mut(chroma_plane_size);
|
||||
|
||||
libyuv::split_uv_plane(
|
||||
src_uv,
|
||||
width as i32,
|
||||
u_plane_422,
|
||||
(width / 2) as i32,
|
||||
v_plane_422,
|
||||
(width / 2) as i32,
|
||||
(width / 2) as i32,
|
||||
height as i32,
|
||||
)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv NV16 split failed: {}", e)))?;
|
||||
|
||||
libyuv::i422_to_i420_planar(
|
||||
&nv16[..y_size],
|
||||
width as i32,
|
||||
u_plane_422,
|
||||
(width / 2) as i32,
|
||||
v_plane_422,
|
||||
(width / 2) as i32,
|
||||
self.output_buffer.as_bytes_mut(),
|
||||
width as i32,
|
||||
height as i32,
|
||||
)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv NV16→I420 failed: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert NV24 (4:4:4 semiplanar) → YUV420P using libyuv split + I444 downsample
|
||||
fn convert_nv24_to_yuv420p(&mut self, nv24: &[u8]) -> Result<()> {
|
||||
let width = self.resolution.width as usize;
|
||||
let height = self.resolution.height as usize;
|
||||
let y_size = width * height;
|
||||
let uv_size = y_size * 2;
|
||||
|
||||
if nv24.len() < y_size + uv_size {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"NV24 data too small: {} < {}",
|
||||
nv24.len(),
|
||||
y_size + uv_size
|
||||
)));
|
||||
}
|
||||
|
||||
let src_uv = &nv24[y_size..y_size + uv_size];
|
||||
let chroma_plane_size = y_size;
|
||||
let (u_plane_444, rest) = self.uv_split_buffer.split_at_mut(chroma_plane_size);
|
||||
let (v_plane_444, _) = rest.split_at_mut(chroma_plane_size);
|
||||
|
||||
libyuv::split_uv_plane(
|
||||
src_uv,
|
||||
(width * 2) as i32,
|
||||
u_plane_444,
|
||||
width as i32,
|
||||
v_plane_444,
|
||||
width as i32,
|
||||
width as i32,
|
||||
height as i32,
|
||||
)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv NV24 split failed: {}", e)))?;
|
||||
|
||||
libyuv::i444_to_i420_planar(
|
||||
&nv24[..y_size],
|
||||
width as i32,
|
||||
u_plane_444,
|
||||
width as i32,
|
||||
v_plane_444,
|
||||
width as i32,
|
||||
self.output_buffer.as_bytes_mut(),
|
||||
width as i32,
|
||||
height as i32,
|
||||
)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv NV24→I420 failed: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate YUV420P buffer size for a given resolution
|
||||
pub fn yuv420p_buffer_size(resolution: Resolution) -> usize {
|
||||
let pixels = (resolution.width * resolution.height) as usize;
|
||||
pixels + pixels / 2
|
||||
}
|
||||
|
||||
/// Calculate YUYV buffer size for a given resolution
|
||||
pub fn yuyv_buffer_size(resolution: Resolution) -> usize {
|
||||
(resolution.width * resolution.height * 2) as usize
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NV12 Converter for VAAPI encoder (using libyuv)
|
||||
// ============================================================================
|
||||
|
||||
/// Pixel format converter that outputs NV12 (for VAAPI encoders)
|
||||
pub struct Nv12Converter {
|
||||
/// Source format
|
||||
src_format: PixelFormat,
|
||||
/// Frame resolution
|
||||
resolution: Resolution,
|
||||
/// Output buffer (reused across conversions)
|
||||
output_buffer: Nv12Buffer,
|
||||
/// Optional I420 buffer for intermediate conversions
|
||||
i420_buffer: Option<Yuv420pBuffer>,
|
||||
}
|
||||
|
||||
impl Nv12Converter {
|
||||
/// Create a new converter for BGR24 → NV12
|
||||
pub fn bgr24_to_nv12(resolution: Resolution) -> Self {
|
||||
Self {
|
||||
src_format: PixelFormat::Bgr24,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new converter for RGB24 → NV12
|
||||
pub fn rgb24_to_nv12(resolution: Resolution) -> Self {
|
||||
Self {
|
||||
src_format: PixelFormat::Rgb24,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new converter for YUYV → NV12
|
||||
pub fn yuyv_to_nv12(resolution: Resolution) -> Self {
|
||||
Self {
|
||||
src_format: PixelFormat::Yuyv,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new converter for YUV420P (I420) → NV12
|
||||
pub fn yuv420_to_nv12(resolution: Resolution) -> Self {
|
||||
Self {
|
||||
src_format: PixelFormat::Yuv420,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new converter for NV21 → NV12
|
||||
pub fn nv21_to_nv12(resolution: Resolution) -> Self {
|
||||
Self {
|
||||
src_format: PixelFormat::Nv21,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: Some(Yuv420pBuffer::new(resolution)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new converter for NV16 → NV12 (downsample chroma vertically)
|
||||
pub fn nv16_to_nv12(resolution: Resolution) -> Self {
|
||||
Self {
|
||||
src_format: PixelFormat::Nv16,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new converter for NV24 → NV12
|
||||
pub fn nv24_to_nv12(resolution: Resolution) -> Self {
|
||||
Self {
|
||||
src_format: PixelFormat::Nv24,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a frame and return reference to the output buffer
|
||||
pub fn convert(&mut self, input: &[u8]) -> Result<&[u8]> {
|
||||
let width = self.resolution.width as i32;
|
||||
let height = self.resolution.height as i32;
|
||||
|
||||
// Handle formats that need custom conversion without holding dst borrow
|
||||
match self.src_format {
|
||||
PixelFormat::Nv21 => {
|
||||
let mut i420 = self.i420_buffer.take().ok_or_else(|| {
|
||||
AppError::VideoError("NV21 I420 buffer not initialized".to_string())
|
||||
})?;
|
||||
{
|
||||
let dst = self.output_buffer.as_bytes_mut();
|
||||
Self::convert_nv21_to_nv12_with_dims(
|
||||
self.resolution.width as usize,
|
||||
self.resolution.height as usize,
|
||||
input,
|
||||
dst,
|
||||
&mut i420,
|
||||
)?;
|
||||
}
|
||||
self.i420_buffer = Some(i420);
|
||||
return Ok(self.output_buffer.as_bytes());
|
||||
}
|
||||
PixelFormat::Nv16 => {
|
||||
let dst = self.output_buffer.as_bytes_mut();
|
||||
Self::convert_nv16_to_nv12_with_dims(
|
||||
self.resolution.width as usize,
|
||||
self.resolution.height as usize,
|
||||
input,
|
||||
dst,
|
||||
)?;
|
||||
return Ok(self.output_buffer.as_bytes());
|
||||
}
|
||||
PixelFormat::Nv24 => {
|
||||
let dst = self.output_buffer.as_bytes_mut();
|
||||
Self::convert_nv24_to_nv12_with_dims(
|
||||
self.resolution.width as usize,
|
||||
self.resolution.height as usize,
|
||||
input,
|
||||
dst,
|
||||
)?;
|
||||
return Ok(self.output_buffer.as_bytes());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let dst = self.output_buffer.as_bytes_mut();
|
||||
let result = match self.src_format {
|
||||
PixelFormat::Bgr24 => libyuv::bgr24_to_nv12(input, dst, width, height),
|
||||
PixelFormat::Rgb24 => libyuv::rgb24_to_nv12(input, dst, width, height),
|
||||
PixelFormat::Yuyv => libyuv::yuy2_to_nv12(input, dst, width, height),
|
||||
PixelFormat::Yuv420 => libyuv::i420_to_nv12(input, dst, width, height),
|
||||
_ => {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"Unsupported conversion to NV12: {}",
|
||||
self.src_format
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
result
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv NV12 conversion failed: {}", e)))?;
|
||||
Ok(self.output_buffer.as_bytes())
|
||||
}
|
||||
|
||||
fn convert_nv21_to_nv12_with_dims(
|
||||
width: usize,
|
||||
height: usize,
|
||||
input: &[u8],
|
||||
dst: &mut [u8],
|
||||
yuv: &mut Yuv420pBuffer,
|
||||
) -> Result<()> {
|
||||
libyuv::nv21_to_i420(input, yuv.as_bytes_mut(), width as i32, height as i32)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv NV21->I420 failed: {}", e)))?;
|
||||
libyuv::i420_to_nv12(yuv.as_bytes(), dst, width as i32, height as i32)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv I420->NV12 failed: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_nv16_to_nv12_with_dims(
|
||||
width: usize,
|
||||
height: usize,
|
||||
input: &[u8],
|
||||
dst: &mut [u8],
|
||||
) -> Result<()> {
|
||||
let y_size = width * height;
|
||||
let uv_size_nv16 = y_size; // NV16 chroma plane is full height
|
||||
let uv_size_nv12 = y_size / 2;
|
||||
|
||||
if input.len() < y_size + uv_size_nv16 {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"NV16 data too small: {} < {}",
|
||||
input.len(),
|
||||
y_size + uv_size_nv16
|
||||
)));
|
||||
}
|
||||
|
||||
// Copy Y plane as-is
|
||||
dst[..y_size].copy_from_slice(&input[..y_size]);
|
||||
|
||||
// Downsample chroma vertically: average pairs of rows
|
||||
let src_uv = &input[y_size..y_size + uv_size_nv16];
|
||||
let dst_uv = &mut dst[y_size..y_size + uv_size_nv12];
|
||||
|
||||
let src_row_bytes = width;
|
||||
let dst_row_bytes = width;
|
||||
let dst_rows = height / 2;
|
||||
|
||||
for row in 0..dst_rows {
|
||||
let src_row0 =
|
||||
&src_uv[row * 2 * src_row_bytes..row * 2 * src_row_bytes + src_row_bytes];
|
||||
let src_row1 = &src_uv
|
||||
[(row * 2 + 1) * src_row_bytes..(row * 2 + 1) * src_row_bytes + src_row_bytes];
|
||||
let dst_row = &mut dst_uv[row * dst_row_bytes..row * dst_row_bytes + dst_row_bytes];
|
||||
|
||||
for i in 0..dst_row_bytes {
|
||||
let sum = src_row0[i] as u16 + src_row1[i] as u16;
|
||||
dst_row[i] = (sum / 2) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_nv24_to_nv12_with_dims(
|
||||
width: usize,
|
||||
height: usize,
|
||||
input: &[u8],
|
||||
dst: &mut [u8],
|
||||
) -> Result<()> {
|
||||
let y_size = width * height;
|
||||
let uv_size_nv24 = y_size * 2;
|
||||
let uv_size_nv12 = y_size / 2;
|
||||
|
||||
if input.len() < y_size + uv_size_nv24 {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"NV24 data too small: {} < {}",
|
||||
input.len(),
|
||||
y_size + uv_size_nv24
|
||||
)));
|
||||
}
|
||||
|
||||
dst[..y_size].copy_from_slice(&input[..y_size]);
|
||||
|
||||
let src_uv = &input[y_size..y_size + uv_size_nv24];
|
||||
let dst_uv = &mut dst[y_size..y_size + uv_size_nv12];
|
||||
let dst_rows = height / 2;
|
||||
|
||||
for row in 0..dst_rows {
|
||||
let src_row0 = &src_uv[row * 2 * width * 2..row * 2 * width * 2 + width * 2];
|
||||
let src_row1 =
|
||||
&src_uv[(row * 2 + 1) * width * 2..(row * 2 + 1) * width * 2 + width * 2];
|
||||
let dst_row = &mut dst_uv[row * width..row * width + width];
|
||||
|
||||
for pair in 0..(width / 2) {
|
||||
let src_idx0 = pair * 4;
|
||||
let src_idx1 = src_idx0 + 2;
|
||||
let dst_idx = pair * 2;
|
||||
|
||||
dst_row[dst_idx] = ((src_row0[src_idx0] as u32
|
||||
+ src_row0[src_idx1] as u32
|
||||
+ src_row1[src_idx0] as u32
|
||||
+ src_row1[src_idx1] as u32)
|
||||
/ 4) as u8;
|
||||
dst_row[dst_idx + 1] = ((src_row0[src_idx0 + 1] as u32
|
||||
+ src_row0[src_idx1 + 1] as u32
|
||||
+ src_row1[src_idx0 + 1] as u32
|
||||
+ src_row1[src_idx1 + 1] as u32)
|
||||
/ 4) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get output buffer length
|
||||
pub fn output_len(&self) -> usize {
|
||||
self.output_buffer.len()
|
||||
}
|
||||
|
||||
/// Get resolution
|
||||
pub fn resolution(&self) -> Resolution {
|
||||
self.resolution
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Standalone conversion functions (using libyuv)
|
||||
// ============================================================================
|
||||
|
||||
/// Convert BGR24 to NV12 using libyuv
|
||||
pub fn bgr_to_nv12(bgr: &[u8], nv12: &mut [u8], width: usize, height: usize) {
|
||||
if let Err(e) = libyuv::bgr24_to_nv12(bgr, nv12, width as i32, height as i32) {
|
||||
tracing::error!("libyuv BGR24→NV12 conversion failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert RGB24 to NV12 using libyuv
|
||||
pub fn rgb_to_nv12(rgb: &[u8], nv12: &mut [u8], width: usize, height: usize) {
|
||||
if let Err(e) = libyuv::rgb24_to_nv12(rgb, nv12, width as i32, height as i32) {
|
||||
tracing::error!("libyuv RGB24→NV12 conversion failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert YUYV to NV12 using libyuv
|
||||
pub fn yuyv_to_nv12(yuyv: &[u8], nv12: &mut [u8], width: usize, height: usize) {
|
||||
if let Err(e) = libyuv::yuy2_to_nv12(yuyv, nv12, width as i32, height as i32) {
|
||||
tracing::error!("libyuv YUYV→NV12 conversion failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_yuv420p_buffer_creation() {
|
||||
let buffer = Yuv420pBuffer::new(Resolution::HD720);
|
||||
assert_eq!(buffer.len(), 1280 * 720 * 3 / 2);
|
||||
assert_eq!(buffer.y_plane().len(), 1280 * 720);
|
||||
assert_eq!(buffer.u_plane().len(), 1280 * 720 / 4);
|
||||
assert_eq!(buffer.v_plane().len(), 1280 * 720 / 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nv12_buffer_creation() {
|
||||
let buffer = Nv12Buffer::new(Resolution::HD720);
|
||||
assert_eq!(buffer.len(), 1280 * 720 * 3 / 2);
|
||||
assert_eq!(buffer.y_plane().len(), 1280 * 720);
|
||||
assert_eq!(buffer.uv_plane().len(), 1280 * 720 / 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_yuyv_to_yuv420p_conversion() {
|
||||
let resolution = Resolution::new(4, 4);
|
||||
let mut converter = PixelConverter::yuyv_to_yuv420p(resolution);
|
||||
|
||||
// Create YUYV data (4x4 = 32 bytes)
|
||||
let yuyv = vec![
|
||||
16, 128, 17, 129, 18, 130, 19, 131, 20, 132, 21, 133, 22, 134, 23, 135, 24, 136, 25,
|
||||
137, 26, 138, 27, 139, 28, 140, 29, 141, 30, 142, 31, 143,
|
||||
];
|
||||
|
||||
let result = converter.convert(&yuyv).unwrap();
|
||||
assert_eq!(result.len(), 24); // 4*4 + 2*2 + 2*2 = 24 bytes
|
||||
}
|
||||
}
|
||||
528
src/video/codec/h264.rs
Normal file
528
src/video/codec/h264.rs
Normal file
@@ -0,0 +1,528 @@
|
||||
//! H.264 encoder using hwcodec (rustdesk's FFmpeg wrapper)
|
||||
//!
|
||||
//! Supports multiple encoder backends via FFmpeg:
|
||||
//! - VAAPI (Intel/AMD/NVIDIA on Linux)
|
||||
//! - NVENC (NVIDIA)
|
||||
//! - AMF (AMD)
|
||||
//! - Software (libx264)
|
||||
//!
|
||||
//! The encoder is selected automatically based on availability.
|
||||
|
||||
use bytes::Bytes;
|
||||
use std::sync::Once;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use hwcodec::common::{Quality, RateControl};
|
||||
use hwcodec::ffmpeg::{resolve_pixel_format, AVPixelFormat};
|
||||
use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder};
|
||||
use hwcodec::ffmpeg_ram::CodecInfo;
|
||||
|
||||
use super::detect_best_codec_for_format;
|
||||
use super::registry::EncoderBackend;
|
||||
use super::traits::{EncodedFormat, EncodedFrame, Encoder, EncoderConfig};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
static INIT_LOGGING: Once = Once::new();
|
||||
|
||||
/// Initialize hwcodec logging (only once)
|
||||
fn init_hwcodec_logging() {
|
||||
INIT_LOGGING.call_once(|| {
|
||||
// hwcodec uses the `log` crate, which will work with our tracing subscriber
|
||||
debug!("hwcodec logging initialized");
|
||||
});
|
||||
}
|
||||
|
||||
/// H.264 encoder type (detected from hwcodec)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum H264EncoderType {
|
||||
/// NVIDIA NVENC
|
||||
Nvenc,
|
||||
/// Intel Quick Sync (QSV)
|
||||
Qsv,
|
||||
/// AMD AMF
|
||||
Amf,
|
||||
/// VAAPI (Linux generic)
|
||||
Vaapi,
|
||||
/// RKMPP (Rockchip) - requires hwcodec extension
|
||||
Rkmpp,
|
||||
/// V4L2 M2M (ARM generic) - requires hwcodec extension
|
||||
V4l2M2m,
|
||||
/// Software encoding (libx264/openh264)
|
||||
Software,
|
||||
/// No encoder available
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for H264EncoderType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
H264EncoderType::Nvenc => write!(f, "NVENC"),
|
||||
H264EncoderType::Qsv => write!(f, "QSV"),
|
||||
H264EncoderType::Amf => write!(f, "AMF"),
|
||||
H264EncoderType::Vaapi => write!(f, "VAAPI"),
|
||||
H264EncoderType::Rkmpp => write!(f, "RKMPP"),
|
||||
H264EncoderType::V4l2M2m => write!(f, "V4L2 M2M"),
|
||||
H264EncoderType::Software => write!(f, "Software"),
|
||||
H264EncoderType::None => write!(f, "None"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Map codec name to encoder type
|
||||
impl From<EncoderBackend> for H264EncoderType {
|
||||
fn from(backend: EncoderBackend) -> Self {
|
||||
match backend {
|
||||
EncoderBackend::Nvenc => H264EncoderType::Nvenc,
|
||||
EncoderBackend::Qsv => H264EncoderType::Qsv,
|
||||
EncoderBackend::Amf => H264EncoderType::Amf,
|
||||
EncoderBackend::Vaapi => H264EncoderType::Vaapi,
|
||||
EncoderBackend::Rkmpp => H264EncoderType::Rkmpp,
|
||||
EncoderBackend::V4l2m2m => H264EncoderType::V4l2M2m,
|
||||
EncoderBackend::Software => H264EncoderType::Software,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Input pixel format for H264 encoder
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum H264InputFormat {
|
||||
/// YUV420P (I420) - planar Y, U, V
|
||||
Yuv420p,
|
||||
/// NV12 - Y plane + interleaved UV plane (optimal for VAAPI)
|
||||
#[default]
|
||||
Nv12,
|
||||
/// NV21 - Y plane + interleaved VU plane
|
||||
Nv21,
|
||||
/// NV16 - Y plane + interleaved UV plane (4:2:2)
|
||||
Nv16,
|
||||
/// NV24 - Y plane + interleaved UV plane (4:4:4)
|
||||
Nv24,
|
||||
/// YUYV422 - packed YUV 4:2:2 format (optimal for RKMPP direct input)
|
||||
Yuyv422,
|
||||
/// RGB24 - packed RGB format (RKMPP direct input)
|
||||
Rgb24,
|
||||
/// BGR24 - packed BGR format (RKMPP direct input)
|
||||
Bgr24,
|
||||
}
|
||||
|
||||
/// H.264 encoder configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct H264Config {
|
||||
/// Base encoder config
|
||||
pub base: EncoderConfig,
|
||||
/// Target bitrate in kbps
|
||||
pub bitrate_kbps: u32,
|
||||
/// GOP size (keyframe interval)
|
||||
pub gop_size: u32,
|
||||
/// Frame rate
|
||||
pub fps: u32,
|
||||
/// Input pixel format
|
||||
pub input_format: H264InputFormat,
|
||||
}
|
||||
|
||||
impl Default for H264Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base: EncoderConfig::default(),
|
||||
bitrate_kbps: 1000,
|
||||
gop_size: 30,
|
||||
fps: 30,
|
||||
input_format: H264InputFormat::Nv12,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl H264Config {
|
||||
/// Create config for low latency streaming with NV12 input (optimal for VAAPI)
|
||||
pub fn low_latency(resolution: Resolution, bitrate_kbps: u32) -> Self {
|
||||
Self {
|
||||
base: EncoderConfig::h264(resolution, bitrate_kbps),
|
||||
bitrate_kbps,
|
||||
gop_size: 30,
|
||||
fps: 30,
|
||||
input_format: H264InputFormat::Nv12,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create config for low latency streaming with YUV420P input
|
||||
pub fn low_latency_yuv420p(resolution: Resolution, bitrate_kbps: u32) -> Self {
|
||||
Self {
|
||||
base: EncoderConfig::h264(resolution, bitrate_kbps),
|
||||
bitrate_kbps,
|
||||
gop_size: 30,
|
||||
fps: 30,
|
||||
input_format: H264InputFormat::Yuv420p,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create config for low latency streaming with YUYV422 input (optimal for RKMPP direct input)
|
||||
pub fn low_latency_yuyv422(resolution: Resolution, bitrate_kbps: u32) -> Self {
|
||||
Self {
|
||||
base: EncoderConfig::h264(resolution, bitrate_kbps),
|
||||
bitrate_kbps,
|
||||
gop_size: 30,
|
||||
fps: 30,
|
||||
input_format: H264InputFormat::Yuyv422,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create config for quality streaming
|
||||
pub fn quality(resolution: Resolution, bitrate_kbps: u32) -> Self {
|
||||
Self {
|
||||
base: EncoderConfig::h264(resolution, bitrate_kbps),
|
||||
bitrate_kbps,
|
||||
gop_size: 60,
|
||||
fps: 30,
|
||||
input_format: H264InputFormat::Nv12,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set input format
|
||||
pub fn with_input_format(mut self, format: H264InputFormat) -> Self {
|
||||
self.input_format = format;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Get available H264 encoders from hwcodec
|
||||
pub fn get_available_encoders(width: u32, height: u32) -> Vec<CodecInfo> {
|
||||
init_hwcodec_logging();
|
||||
|
||||
let ctx = EncodeContext {
|
||||
name: String::new(),
|
||||
mc_name: None,
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
pixfmt: resolve_pixel_format("yuv420p", AVPixelFormat::AV_PIX_FMT_YUV420P),
|
||||
align: 1,
|
||||
fps: 30,
|
||||
gop: 30,
|
||||
rc: RateControl::RC_CBR,
|
||||
quality: Quality::Quality_Low, // Use low quality preset for fastest encoding (ultrafast)
|
||||
kbs: 2000,
|
||||
q: 23,
|
||||
thread_count: 4,
|
||||
};
|
||||
|
||||
HwEncoder::available_encoders(ctx, None)
|
||||
}
|
||||
|
||||
/// Detect best available H.264 encoder
|
||||
pub fn detect_best_encoder(width: u32, height: u32) -> (H264EncoderType, Option<String>) {
|
||||
let encoders = get_available_encoders(width, height);
|
||||
|
||||
if let Some((encoder_type, codec_name)) =
|
||||
detect_best_codec_for_format(&encoders, hwcodec::common::DataFormat::H264, |_| true)
|
||||
{
|
||||
info!("Best H.264 encoder: {} ({})", codec_name, encoder_type);
|
||||
(encoder_type, Some(codec_name))
|
||||
} else {
|
||||
warn!("No H.264 encoders available from hwcodec");
|
||||
(H264EncoderType::None, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Encoded frame from hwcodec (cloned for ownership)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HwEncodeFrame {
|
||||
pub data: Vec<u8>,
|
||||
pub pts: i64,
|
||||
pub key: i32,
|
||||
}
|
||||
|
||||
/// H.264 encoder using hwcodec
|
||||
pub struct H264Encoder {
|
||||
/// hwcodec encoder instance
|
||||
inner: HwEncoder,
|
||||
/// Encoder configuration
|
||||
config: H264Config,
|
||||
/// Detected encoder type
|
||||
encoder_type: H264EncoderType,
|
||||
/// Codec name
|
||||
codec_name: String,
|
||||
/// Frame counter
|
||||
frame_count: u64,
|
||||
/// Required YUV buffer length from hwcodec
|
||||
yuv_length: i32,
|
||||
}
|
||||
|
||||
impl H264Encoder {
|
||||
/// Create a new H.264 encoder with automatic codec detection
|
||||
pub fn new(config: H264Config) -> Result<Self> {
|
||||
init_hwcodec_logging();
|
||||
|
||||
let width = config.base.resolution.width;
|
||||
let height = config.base.resolution.height;
|
||||
|
||||
// Detect best encoder
|
||||
let (_encoder_type, codec_name) = detect_best_encoder(width, height);
|
||||
|
||||
let codec_name = codec_name
|
||||
.ok_or_else(|| AppError::VideoError("No H.264 encoder available".to_string()))?;
|
||||
|
||||
Self::with_codec(config, &codec_name)
|
||||
}
|
||||
|
||||
/// Create encoder with specific codec name
|
||||
pub fn with_codec(config: H264Config, codec_name: &str) -> Result<Self> {
|
||||
init_hwcodec_logging();
|
||||
|
||||
let width = config.base.resolution.width;
|
||||
let height = config.base.resolution.height;
|
||||
|
||||
// Select pixel format based on config
|
||||
let (pixfmt_name, pixfmt_fallback) = match config.input_format {
|
||||
H264InputFormat::Nv12 => ("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
|
||||
H264InputFormat::Nv21 => ("nv21", AVPixelFormat::AV_PIX_FMT_NV21),
|
||||
H264InputFormat::Nv16 => ("nv16", AVPixelFormat::AV_PIX_FMT_NV16),
|
||||
H264InputFormat::Nv24 => ("nv24", AVPixelFormat::AV_PIX_FMT_NV24),
|
||||
H264InputFormat::Yuv420p => ("yuv420p", AVPixelFormat::AV_PIX_FMT_YUV420P),
|
||||
H264InputFormat::Yuyv422 => ("yuyv422", AVPixelFormat::AV_PIX_FMT_YUYV422),
|
||||
H264InputFormat::Rgb24 => ("rgb24", AVPixelFormat::AV_PIX_FMT_RGB24),
|
||||
H264InputFormat::Bgr24 => ("bgr24", AVPixelFormat::AV_PIX_FMT_BGR24),
|
||||
};
|
||||
let pixfmt = resolve_pixel_format(pixfmt_name, pixfmt_fallback);
|
||||
|
||||
info!(
|
||||
"Creating H.264 encoder: {} at {}x{} @ {} kbps (input: {:?})",
|
||||
codec_name, width, height, config.bitrate_kbps, config.input_format
|
||||
);
|
||||
|
||||
let ctx = EncodeContext {
|
||||
name: codec_name.to_string(),
|
||||
mc_name: None,
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
pixfmt,
|
||||
align: 1,
|
||||
fps: config.fps as i32,
|
||||
gop: config.gop_size as i32,
|
||||
rc: RateControl::RC_CBR,
|
||||
quality: Quality::Quality_Low, // Use low quality preset for fastest encoding (lowest latency)
|
||||
kbs: config.bitrate_kbps as i32,
|
||||
q: 23,
|
||||
thread_count: 4, // Use 4 threads for better performance
|
||||
};
|
||||
|
||||
let inner = HwEncoder::new(ctx).map_err(|_| {
|
||||
AppError::VideoError(format!("Failed to create encoder: {}", codec_name))
|
||||
})?;
|
||||
|
||||
let yuv_length = inner.length;
|
||||
let encoder_type = H264EncoderType::from(EncoderBackend::from_codec_name(codec_name));
|
||||
|
||||
info!(
|
||||
"H.264 encoder created: {} (type: {}, buffer_length: {}, input_format: {:?})",
|
||||
codec_name, encoder_type, yuv_length, config.input_format
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
config,
|
||||
encoder_type,
|
||||
codec_name: codec_name.to_string(),
|
||||
frame_count: 0,
|
||||
yuv_length,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create with auto-detected encoder
|
||||
pub fn auto(resolution: Resolution, bitrate_kbps: u32) -> Result<Self> {
|
||||
let config = H264Config::low_latency(resolution, bitrate_kbps);
|
||||
Self::new(config)
|
||||
}
|
||||
|
||||
/// Get encoder type
|
||||
pub fn encoder_type(&self) -> &H264EncoderType {
|
||||
&self.encoder_type
|
||||
}
|
||||
|
||||
/// Get codec name
|
||||
pub fn codec_name(&self) -> &str {
|
||||
&self.codec_name
|
||||
}
|
||||
|
||||
/// Update bitrate dynamically
|
||||
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
|
||||
self.inner
|
||||
.set_bitrate(bitrate_kbps as i32)
|
||||
.map_err(|_| AppError::VideoError("Failed to set bitrate".to_string()))?;
|
||||
self.config.bitrate_kbps = bitrate_kbps;
|
||||
debug!("Bitrate updated to {} kbps", bitrate_kbps);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Request next frame to be a keyframe (IDR)
|
||||
pub fn request_keyframe(&mut self) {
|
||||
self.inner.request_keyframe();
|
||||
debug!("H264 keyframe requested");
|
||||
}
|
||||
|
||||
/// Encode raw frame data (YUV420P or NV12 depending on config)
|
||||
pub fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
|
||||
if data.len() < self.yuv_length as usize {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"Frame data too small: {} < {}",
|
||||
data.len(),
|
||||
self.yuv_length
|
||||
)));
|
||||
}
|
||||
|
||||
self.frame_count += 1;
|
||||
|
||||
match self.inner.encode(data, pts_ms) {
|
||||
Ok(frames) => {
|
||||
// Zero-copy: drain frames from hwcodec buffer instead of cloning
|
||||
// hwcodec returns &mut Vec, so we can take ownership via drain
|
||||
let owned_frames: Vec<HwEncodeFrame> = frames
|
||||
.drain(..)
|
||||
.map(|f| HwEncodeFrame {
|
||||
data: f.data, // Move, not clone
|
||||
pts: f.pts,
|
||||
key: f.key,
|
||||
})
|
||||
.collect();
|
||||
Ok(owned_frames)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Encode failed: {}", e);
|
||||
Err(AppError::VideoError(format!("Encode failed: {}", e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode YUV420P data (legacy method, use encode_raw for new code)
|
||||
pub fn encode_yuv420p(&mut self, yuv_data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
|
||||
self.encode_raw(yuv_data, pts_ms)
|
||||
}
|
||||
|
||||
/// Encode NV12 data
|
||||
pub fn encode_nv12(&mut self, nv12_data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
|
||||
self.encode_raw(nv12_data, pts_ms)
|
||||
}
|
||||
|
||||
/// Get input format
|
||||
pub fn input_format(&self) -> H264InputFormat {
|
||||
self.config.input_format
|
||||
}
|
||||
|
||||
/// Get YUV buffer info (linesize, offset, length)
|
||||
pub fn yuv_info(&self) -> (Vec<i32>, Vec<i32>, i32) {
|
||||
(
|
||||
self.inner.linesize.clone(),
|
||||
self.inner.offset.clone(),
|
||||
self.inner.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: H264Encoder contains hwcodec::ffmpeg_ram::encode::Encoder which has raw pointers
|
||||
// that are not Send by default. However, we ensure that H264Encoder is only used from
|
||||
// a single task/thread at a time (encoding is sequential), so this is safe.
|
||||
// The raw pointers are internal FFmpeg context that doesn't escape the encoder.
|
||||
unsafe impl Send for H264Encoder {}
|
||||
|
||||
impl Encoder for H264Encoder {
|
||||
fn name(&self) -> &str {
|
||||
&self.codec_name
|
||||
}
|
||||
|
||||
fn output_format(&self) -> EncodedFormat {
|
||||
EncodedFormat::H264
|
||||
}
|
||||
|
||||
fn encode(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
|
||||
// Assume input is YUV420P
|
||||
let pts_ms = (sequence * 1000 / self.config.fps as u64) as i64;
|
||||
|
||||
let mut frames = self.encode_yuv420p(data, pts_ms)?;
|
||||
|
||||
if frames.is_empty() {
|
||||
// Encoder needs more frames (shouldn't happen with our config)
|
||||
warn!("Encoder returned no frames");
|
||||
return Err(AppError::VideoError(
|
||||
"Encoder returned no frames".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Take ownership of the first frame (zero-copy)
|
||||
let frame = frames.remove(0);
|
||||
let key_frame = frame.key == 1;
|
||||
|
||||
Ok(EncodedFrame::h264(
|
||||
Bytes::from(frame.data), // Move Vec into Bytes (zero-copy)
|
||||
self.config.base.resolution,
|
||||
key_frame,
|
||||
sequence,
|
||||
frame.pts as u64,
|
||||
frame.pts as u64,
|
||||
))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<Vec<EncodedFrame>> {
|
||||
// hwcodec doesn't have explicit flush, return empty
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> Result<()> {
|
||||
self.frame_count = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn config(&self) -> &EncoderConfig {
|
||||
&self.config.base
|
||||
}
|
||||
|
||||
fn supports_format(&self, format: PixelFormat) -> bool {
|
||||
// Check if the format matches our configured input format
|
||||
match self.config.input_format {
|
||||
H264InputFormat::Nv12 => matches!(format, PixelFormat::Nv12),
|
||||
H264InputFormat::Nv21 => matches!(format, PixelFormat::Nv21),
|
||||
H264InputFormat::Nv16 => matches!(format, PixelFormat::Nv16),
|
||||
H264InputFormat::Nv24 => matches!(format, PixelFormat::Nv24),
|
||||
H264InputFormat::Yuv420p => matches!(format, PixelFormat::Yuv420),
|
||||
H264InputFormat::Yuyv422 => matches!(format, PixelFormat::Yuyv),
|
||||
H264InputFormat::Rgb24 => matches!(format, PixelFormat::Rgb24),
|
||||
H264InputFormat::Bgr24 => matches!(format, PixelFormat::Bgr24),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_detect_encoder() {
|
||||
let (encoder_type, codec_name) = detect_best_encoder(1280, 720);
|
||||
println!("Detected encoder: {:?} ({:?})", encoder_type, codec_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_available_encoders() {
|
||||
let encoders = get_available_encoders(1280, 720);
|
||||
println!("Available encoders:");
|
||||
for enc in &encoders {
|
||||
println!(" - {} ({:?})", enc.name, enc.format);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_encoder() {
|
||||
let config = H264Config::low_latency(Resolution::HD720, 2000);
|
||||
match H264Encoder::new(config) {
|
||||
Ok(encoder) => {
|
||||
println!(
|
||||
"Created encoder: {} ({})",
|
||||
encoder.codec_name(),
|
||||
encoder.encoder_type()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to create encoder: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
299
src/video/codec/h264_bitstream.rs
Normal file
299
src/video/codec/h264_bitstream.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
//! H.264 Annex-B/AVCC bitstream helpers shared by WebRTC, RTSP and RustDesk.
|
||||
|
||||
pub const FALLBACK_WEBRTC_PROFILE_LEVEL_ID: &str = "42e01f";
|
||||
|
||||
pub fn webrtc_fmtp_line(profile_level_id: &str) -> String {
|
||||
format!(
|
||||
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id={}",
|
||||
profile_level_id
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fallback_webrtc_fmtp_line() -> String {
|
||||
webrtc_fmtp_line(FALLBACK_WEBRTC_PROFILE_LEVEL_ID)
|
||||
}
|
||||
|
||||
pub fn strip_aud_nal_units(data: &[u8]) -> Vec<u8> {
|
||||
let mut result = Vec::with_capacity(data.len());
|
||||
let mut i = 0;
|
||||
|
||||
while i < data.len() {
|
||||
let (start_code_pos, start_code_len) = if i + 4 <= data.len()
|
||||
&& data[i] == 0
|
||||
&& data[i + 1] == 0
|
||||
&& data[i + 2] == 0
|
||||
&& data[i + 3] == 1
|
||||
{
|
||||
(i, 4)
|
||||
} else if i + 3 <= data.len() && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
|
||||
(i, 3)
|
||||
} else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let nal_start = start_code_pos + start_code_len;
|
||||
if nal_start >= data.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let nal_type = data[nal_start] & 0x1F;
|
||||
|
||||
let mut nal_end = data.len();
|
||||
let mut j = nal_start + 1;
|
||||
while j + 3 <= data.len() {
|
||||
if (data[j] == 0 && data[j + 1] == 0 && data[j + 2] == 1)
|
||||
|| (j + 4 <= data.len()
|
||||
&& data[j] == 0
|
||||
&& data[j + 1] == 0
|
||||
&& data[j + 2] == 0
|
||||
&& data[j + 3] == 1)
|
||||
{
|
||||
nal_end = j;
|
||||
break;
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
|
||||
if nal_type != 9 && nal_type != 12 {
|
||||
result.extend_from_slice(&data[start_code_pos..nal_end]);
|
||||
}
|
||||
|
||||
i = nal_end;
|
||||
}
|
||||
|
||||
if result.is_empty() && !data.is_empty() {
|
||||
return data.to_vec();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn extract_sps_pps(data: &[u8]) -> (Option<Vec<u8>>, Option<Vec<u8>>) {
|
||||
let mut sps: Option<Vec<u8>> = None;
|
||||
let mut pps: Option<Vec<u8>> = None;
|
||||
let mut i = 0;
|
||||
|
||||
while i < data.len() {
|
||||
let start_code_len = if i + 4 <= data.len()
|
||||
&& data[i] == 0
|
||||
&& data[i + 1] == 0
|
||||
&& data[i + 2] == 0
|
||||
&& data[i + 3] == 1
|
||||
{
|
||||
4
|
||||
} else if i + 3 <= data.len() && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
|
||||
3
|
||||
} else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let nal_start = i + start_code_len;
|
||||
if nal_start >= data.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let nal_type = data[nal_start] & 0x1F;
|
||||
|
||||
let mut nal_end = data.len();
|
||||
let mut j = nal_start + 1;
|
||||
while j + 3 <= data.len() {
|
||||
if (data[j] == 0 && data[j + 1] == 0 && data[j + 2] == 1)
|
||||
|| (j + 4 <= data.len()
|
||||
&& data[j] == 0
|
||||
&& data[j + 1] == 0
|
||||
&& data[j + 2] == 0
|
||||
&& data[j + 3] == 1)
|
||||
{
|
||||
nal_end = j;
|
||||
break;
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
|
||||
match nal_type {
|
||||
7 => {
|
||||
sps = Some(data[nal_start..nal_end].to_vec());
|
||||
}
|
||||
8 => {
|
||||
pps = Some(data[nal_start..nal_end].to_vec());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
i = nal_end;
|
||||
}
|
||||
|
||||
(sps, pps)
|
||||
}
|
||||
|
||||
pub fn has_sps_pps(data: &[u8]) -> bool {
|
||||
let mut has_sps = false;
|
||||
let mut has_pps = false;
|
||||
let mut i = 0;
|
||||
|
||||
while i < data.len() {
|
||||
let start_code_len = if i + 4 <= data.len()
|
||||
&& data[i] == 0
|
||||
&& data[i + 1] == 0
|
||||
&& data[i + 2] == 0
|
||||
&& data[i + 3] == 1
|
||||
{
|
||||
4
|
||||
} else if i + 3 <= data.len() && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
|
||||
3
|
||||
} else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let nal_start = i + start_code_len;
|
||||
if nal_start >= data.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let nal_type = data[nal_start] & 0x1F;
|
||||
|
||||
match nal_type {
|
||||
7 => has_sps = true,
|
||||
8 => has_pps = true,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if has_sps && has_pps {
|
||||
return true;
|
||||
}
|
||||
|
||||
i = nal_start + 1;
|
||||
}
|
||||
|
||||
has_sps && has_pps
|
||||
}
|
||||
|
||||
pub fn is_keyframe(data: &[u8]) -> bool {
|
||||
let mut i = 0;
|
||||
while i < data.len() {
|
||||
if i + 3 < data.len() && data[i] == 0 && data[i + 1] == 0 {
|
||||
let nal_start = if data[i + 2] == 1 {
|
||||
i + 3
|
||||
} else if i + 4 < data.len() && data[i + 2] == 0 && data[i + 3] == 1 {
|
||||
i + 4
|
||||
} else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
if nal_start < data.len() {
|
||||
let nal_type = data[nal_start] & 0x1F;
|
||||
if nal_type == 5 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
i = nal_start;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// `profile-level-id` hex for SDP (`42001f` etc.); expects SPS NAL without start code.
|
||||
pub fn parse_profile_level_id_from_sps(sps: &[u8]) -> Option<String> {
|
||||
if sps.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let profile_idc = sps[1];
|
||||
let constraint_set_flags = sps[2];
|
||||
let level_idc = sps[3];
|
||||
|
||||
Some(format!(
|
||||
"{:02x}{:02x}{:02x}",
|
||||
profile_idc, constraint_set_flags, level_idc
|
||||
))
|
||||
}
|
||||
|
||||
pub fn extract_profile_level_id(data: &[u8]) -> Option<String> {
|
||||
let (sps, _) = extract_sps_pps(data);
|
||||
sps.and_then(|sps_data| parse_profile_level_id_from_sps(&sps_data))
|
||||
}
|
||||
|
||||
pub fn is_annex_b(data: &[u8]) -> bool {
|
||||
data.starts_with(&[0, 0, 1]) || data.starts_with(&[0, 0, 0, 1])
|
||||
}
|
||||
|
||||
pub fn avcc_to_annex_b(data: &[u8]) -> Option<Vec<u8>> {
|
||||
let mut pos = 0;
|
||||
let mut output = Vec::with_capacity(data.len() + 16);
|
||||
let mut nalu_count = 0usize;
|
||||
|
||||
while pos + 4 <= data.len() {
|
||||
let nalu_len =
|
||||
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
|
||||
pos += 4;
|
||||
if nalu_len == 0 || pos + nalu_len > data.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let nal_type = data[pos] & 0x1F;
|
||||
if nal_type != 9 && nal_type != 12 {
|
||||
output.extend_from_slice(&[0, 0, 0, 1]);
|
||||
output.extend_from_slice(&data[pos..pos + nalu_len]);
|
||||
}
|
||||
nalu_count += 1;
|
||||
pos += nalu_len;
|
||||
}
|
||||
|
||||
if pos == data.len() && nalu_count > 0 && !output.is_empty() {
|
||||
Some(output)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_for_webrtc(data: &[u8]) -> Vec<u8> {
|
||||
if is_annex_b(data) {
|
||||
return strip_aud_nal_units(data);
|
||||
}
|
||||
|
||||
if let Some(annex_b) = avcc_to_annex_b(data) {
|
||||
return strip_aud_nal_units(&annex_b);
|
||||
}
|
||||
|
||||
data.to_vec()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn detects_h264_keyframes() {
|
||||
let idr_frame = vec![0x00, 0x00, 0x00, 0x01, 0x65];
|
||||
assert!(is_keyframe(&idr_frame));
|
||||
|
||||
let idr_frame_3 = vec![0x00, 0x00, 0x01, 0x65];
|
||||
assert!(is_keyframe(&idr_frame_3));
|
||||
|
||||
let p_frame = vec![0x00, 0x00, 0x00, 0x01, 0x41];
|
||||
assert!(!is_keyframe(&p_frame));
|
||||
|
||||
let sps = vec![0x00, 0x00, 0x00, 0x01, 0x67];
|
||||
assert!(!is_keyframe(&sps));
|
||||
|
||||
let multi_nal = vec![
|
||||
0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x01, 0x68, 0xce,
|
||||
0x38, 0x80, 0x00, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84,
|
||||
];
|
||||
assert!(is_keyframe(&multi_nal));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_profile_level_id_from_sps() {
|
||||
assert_eq!(
|
||||
parse_profile_level_id_from_sps(&[0x67, 0x42, 0x40, 0x2a]),
|
||||
Some("42402a".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
635
src/video/codec/h265.rs
Normal file
635
src/video/codec/h265.rs
Normal file
@@ -0,0 +1,635 @@
|
||||
//! H.265/HEVC encoder using hwcodec (FFmpeg wrapper)
|
||||
//!
|
||||
//! Supports both hardware and software encoding:
|
||||
//! - Hardware: VAAPI, NVENC, QSV, AMF, RKMPP, V4L2 M2M
|
||||
//! - Software: libx265 (CPU-based, high CPU usage)
|
||||
//!
|
||||
//! Hardware encoding is preferred when available for better performance.
|
||||
|
||||
use bytes::Bytes;
|
||||
use std::sync::Once;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
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;
|
||||
|
||||
use super::detect_best_codec_for_format;
|
||||
use super::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType};
|
||||
use super::traits::{EncodedFormat, EncodedFrame, Encoder, EncoderConfig};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
static INIT_LOGGING: Once = Once::new();
|
||||
|
||||
/// Initialize hwcodec logging (only once)
|
||||
fn init_hwcodec_logging() {
|
||||
INIT_LOGGING.call_once(|| {
|
||||
debug!("hwcodec logging initialized for H265");
|
||||
});
|
||||
}
|
||||
|
||||
/// H.265 encoder type (detected from hwcodec)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum H265EncoderType {
|
||||
/// NVIDIA NVENC
|
||||
Nvenc,
|
||||
/// Intel Quick Sync (QSV)
|
||||
Qsv,
|
||||
/// AMD AMF
|
||||
Amf,
|
||||
/// VAAPI (Linux generic)
|
||||
Vaapi,
|
||||
/// RKMPP (Rockchip)
|
||||
Rkmpp,
|
||||
/// V4L2 M2M (ARM generic)
|
||||
V4l2M2m,
|
||||
/// Software encoder (libx265)
|
||||
Software,
|
||||
/// No encoder available
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for H265EncoderType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
H265EncoderType::Nvenc => write!(f, "NVENC"),
|
||||
H265EncoderType::Qsv => write!(f, "QSV"),
|
||||
H265EncoderType::Amf => write!(f, "AMF"),
|
||||
H265EncoderType::Vaapi => write!(f, "VAAPI"),
|
||||
H265EncoderType::Rkmpp => write!(f, "RKMPP"),
|
||||
H265EncoderType::V4l2M2m => write!(f, "V4L2 M2M"),
|
||||
H265EncoderType::Software => write!(f, "Software"),
|
||||
H265EncoderType::None => write!(f, "None"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EncoderBackend> for H265EncoderType {
|
||||
fn from(backend: EncoderBackend) -> Self {
|
||||
match backend {
|
||||
EncoderBackend::Nvenc => H265EncoderType::Nvenc,
|
||||
EncoderBackend::Qsv => H265EncoderType::Qsv,
|
||||
EncoderBackend::Amf => H265EncoderType::Amf,
|
||||
EncoderBackend::Vaapi => H265EncoderType::Vaapi,
|
||||
EncoderBackend::Rkmpp => H265EncoderType::Rkmpp,
|
||||
EncoderBackend::V4l2m2m => H265EncoderType::V4l2M2m,
|
||||
EncoderBackend::Software => H265EncoderType::Software,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Input pixel format for H265 encoder
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum H265InputFormat {
|
||||
/// YUV420P (I420) - planar Y, U, V
|
||||
Yuv420p,
|
||||
/// NV12 - Y plane + interleaved UV plane (optimal for hardware encoders)
|
||||
#[default]
|
||||
Nv12,
|
||||
/// NV21 - Y plane + interleaved VU plane
|
||||
Nv21,
|
||||
/// NV16 - Y plane + interleaved UV plane (4:2:2)
|
||||
Nv16,
|
||||
/// NV24 - Y plane + interleaved UV plane (4:4:4)
|
||||
Nv24,
|
||||
/// YUYV422 - packed YUV 4:2:2 format (optimal for RKMPP direct input)
|
||||
Yuyv422,
|
||||
/// RGB24 - packed RGB format (RKMPP direct input)
|
||||
Rgb24,
|
||||
/// BGR24 - packed BGR format (RKMPP direct input)
|
||||
Bgr24,
|
||||
}
|
||||
|
||||
/// H.265 encoder configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct H265Config {
|
||||
/// Base encoder config
|
||||
pub base: EncoderConfig,
|
||||
/// Target bitrate in kbps
|
||||
pub bitrate_kbps: u32,
|
||||
/// GOP size (keyframe interval)
|
||||
pub gop_size: u32,
|
||||
/// Frame rate
|
||||
pub fps: u32,
|
||||
/// Input pixel format
|
||||
pub input_format: H265InputFormat,
|
||||
}
|
||||
|
||||
impl Default for H265Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base: EncoderConfig::default(),
|
||||
bitrate_kbps: 8000,
|
||||
gop_size: 30,
|
||||
fps: 30,
|
||||
input_format: H265InputFormat::Nv12,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl H265Config {
|
||||
/// Create config for low latency streaming with NV12 input
|
||||
pub fn low_latency(resolution: Resolution, bitrate_kbps: u32) -> Self {
|
||||
Self {
|
||||
base: EncoderConfig {
|
||||
resolution,
|
||||
input_format: PixelFormat::Nv12,
|
||||
quality: bitrate_kbps,
|
||||
fps: 30,
|
||||
gop_size: 30,
|
||||
},
|
||||
bitrate_kbps,
|
||||
gop_size: 30,
|
||||
fps: 30,
|
||||
input_format: H265InputFormat::Nv12,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create config for low latency streaming with YUYV422 input (optimal for RKMPP direct input)
|
||||
pub fn low_latency_yuyv422(resolution: Resolution, bitrate_kbps: u32) -> Self {
|
||||
Self {
|
||||
base: EncoderConfig {
|
||||
resolution,
|
||||
input_format: PixelFormat::Yuyv,
|
||||
quality: bitrate_kbps,
|
||||
fps: 30,
|
||||
gop_size: 30,
|
||||
},
|
||||
bitrate_kbps,
|
||||
gop_size: 30,
|
||||
fps: 30,
|
||||
input_format: H265InputFormat::Yuyv422,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create config for quality streaming
|
||||
pub fn quality(resolution: Resolution, bitrate_kbps: u32) -> Self {
|
||||
Self {
|
||||
base: EncoderConfig {
|
||||
resolution,
|
||||
input_format: PixelFormat::Nv12,
|
||||
quality: bitrate_kbps,
|
||||
fps: 30,
|
||||
gop_size: 60,
|
||||
},
|
||||
bitrate_kbps,
|
||||
gop_size: 60,
|
||||
fps: 30,
|
||||
input_format: H265InputFormat::Nv12,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set input format
|
||||
pub fn with_input_format(mut self, format: H265InputFormat) -> Self {
|
||||
self.input_format = format;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Get available H265 hardware encoders from hwcodec
|
||||
pub fn get_available_h265_encoders(width: u32, height: u32) -> Vec<CodecInfo> {
|
||||
init_hwcodec_logging();
|
||||
|
||||
let ctx = EncodeContext {
|
||||
name: String::new(),
|
||||
mc_name: None,
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
|
||||
align: 1,
|
||||
fps: 30,
|
||||
gop: 30,
|
||||
rc: RateControl::RC_CBR,
|
||||
quality: Quality::Quality_Default,
|
||||
kbs: 2000,
|
||||
q: 23,
|
||||
thread_count: 1,
|
||||
};
|
||||
|
||||
let all_encoders = HwEncoder::available_encoders(ctx, None);
|
||||
|
||||
// Include both hardware and software H265 encoders
|
||||
all_encoders
|
||||
.into_iter()
|
||||
.filter(|e| e.format == DataFormat::H265)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Detect best available H.265 encoder (hardware preferred, software fallback)
|
||||
pub fn detect_best_h265_encoder(width: u32, height: u32) -> (H265EncoderType, Option<String>) {
|
||||
let encoders = get_available_h265_encoders(width, height);
|
||||
|
||||
// Prefer hardware encoders over software (libx265)
|
||||
// Hardware priority: NVENC > QSV > AMF > VAAPI > RKMPP > V4L2 M2M > Software
|
||||
if let Some((encoder_type, codec_name)) =
|
||||
detect_best_codec_for_format(&encoders, DataFormat::H265, |codec| {
|
||||
!codec.name.contains("libx265")
|
||||
})
|
||||
{
|
||||
info!("Selected H.265 encoder: {} ({})", codec_name, encoder_type);
|
||||
(encoder_type, Some(codec_name))
|
||||
} else {
|
||||
warn!("No H.265 encoders available");
|
||||
(H265EncoderType::None, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if H265 hardware encoding is available
|
||||
pub fn is_h265_available() -> bool {
|
||||
let registry = EncoderRegistry::global();
|
||||
registry.is_codec_available(VideoEncoderType::H265)
|
||||
}
|
||||
|
||||
/// Encoded frame from hwcodec (cloned for ownership)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HwEncodeFrame {
|
||||
pub data: Vec<u8>,
|
||||
pub pts: i64,
|
||||
pub key: i32,
|
||||
}
|
||||
|
||||
/// H.265 encoder using hwcodec
|
||||
pub struct H265Encoder {
|
||||
/// hwcodec encoder instance
|
||||
inner: HwEncoder,
|
||||
/// Encoder configuration
|
||||
config: H265Config,
|
||||
/// Detected encoder type
|
||||
encoder_type: H265EncoderType,
|
||||
/// Codec name
|
||||
codec_name: String,
|
||||
/// Frame counter
|
||||
frame_count: u64,
|
||||
/// Required buffer length from hwcodec
|
||||
buffer_length: i32,
|
||||
}
|
||||
|
||||
impl H265Encoder {
|
||||
/// Create a new H.265 encoder with automatic hardware codec detection
|
||||
///
|
||||
/// Returns an error if no hardware encoder is available.
|
||||
pub fn new(config: H265Config) -> Result<Self> {
|
||||
init_hwcodec_logging();
|
||||
|
||||
let width = config.base.resolution.width;
|
||||
let height = config.base.resolution.height;
|
||||
|
||||
// Detect best hardware encoder
|
||||
let (encoder_type, codec_name) = detect_best_h265_encoder(width, height);
|
||||
|
||||
if encoder_type == H265EncoderType::None {
|
||||
return Err(AppError::VideoError(
|
||||
"No H.265 encoder available. Please ensure FFmpeg is built with libx265 support."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let codec_name = codec_name.unwrap();
|
||||
Self::with_codec(config, &codec_name)
|
||||
}
|
||||
|
||||
/// Create encoder with specific codec name
|
||||
pub fn with_codec(config: H265Config, codec_name: &str) -> Result<Self> {
|
||||
init_hwcodec_logging();
|
||||
|
||||
// Determine if this is a software encoder
|
||||
let is_software = codec_name.contains("libx265");
|
||||
|
||||
// Warn about software encoder performance
|
||||
if is_software {
|
||||
warn!(
|
||||
"Using software H.265 encoder (libx265) - high CPU usage expected. \
|
||||
Hardware encoder is recommended for better performance."
|
||||
);
|
||||
}
|
||||
|
||||
let width = config.base.resolution.width;
|
||||
let height = config.base.resolution.height;
|
||||
|
||||
// Software encoders (libx265) require YUV420P, hardware encoders use NV12 or YUYV422
|
||||
let (pixfmt_name, pixfmt_fallback, actual_input_format) = if is_software {
|
||||
(
|
||||
"yuv420p",
|
||||
AVPixelFormat::AV_PIX_FMT_YUV420P,
|
||||
H265InputFormat::Yuv420p,
|
||||
)
|
||||
} else {
|
||||
match config.input_format {
|
||||
H265InputFormat::Nv12 => (
|
||||
"nv12",
|
||||
AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
H265InputFormat::Nv12,
|
||||
),
|
||||
H265InputFormat::Nv21 => (
|
||||
"nv21",
|
||||
AVPixelFormat::AV_PIX_FMT_NV21,
|
||||
H265InputFormat::Nv21,
|
||||
),
|
||||
H265InputFormat::Nv16 => (
|
||||
"nv16",
|
||||
AVPixelFormat::AV_PIX_FMT_NV16,
|
||||
H265InputFormat::Nv16,
|
||||
),
|
||||
H265InputFormat::Nv24 => (
|
||||
"nv24",
|
||||
AVPixelFormat::AV_PIX_FMT_NV24,
|
||||
H265InputFormat::Nv24,
|
||||
),
|
||||
H265InputFormat::Yuv420p => (
|
||||
"yuv420p",
|
||||
AVPixelFormat::AV_PIX_FMT_YUV420P,
|
||||
H265InputFormat::Yuv420p,
|
||||
),
|
||||
H265InputFormat::Yuyv422 => (
|
||||
"yuyv422",
|
||||
AVPixelFormat::AV_PIX_FMT_YUYV422,
|
||||
H265InputFormat::Yuyv422,
|
||||
),
|
||||
H265InputFormat::Rgb24 => (
|
||||
"rgb24",
|
||||
AVPixelFormat::AV_PIX_FMT_RGB24,
|
||||
H265InputFormat::Rgb24,
|
||||
),
|
||||
H265InputFormat::Bgr24 => (
|
||||
"bgr24",
|
||||
AVPixelFormat::AV_PIX_FMT_BGR24,
|
||||
H265InputFormat::Bgr24,
|
||||
),
|
||||
}
|
||||
};
|
||||
let pixfmt = resolve_pixel_format(pixfmt_name, pixfmt_fallback);
|
||||
|
||||
info!(
|
||||
"Creating H.265 encoder: {} at {}x{} @ {} kbps (input: {:?})",
|
||||
codec_name, width, height, config.bitrate_kbps, actual_input_format
|
||||
);
|
||||
|
||||
let ctx = EncodeContext {
|
||||
name: codec_name.to_string(),
|
||||
mc_name: None,
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
pixfmt,
|
||||
align: 1,
|
||||
fps: config.fps as i32,
|
||||
gop: config.gop_size as i32,
|
||||
rc: RateControl::RC_CBR,
|
||||
quality: Quality::Quality_Default,
|
||||
kbs: config.bitrate_kbps as i32,
|
||||
q: 23,
|
||||
thread_count: 1,
|
||||
};
|
||||
|
||||
let inner = HwEncoder::new(ctx).map_err(|_| {
|
||||
AppError::VideoError(format!("Failed to create H.265 encoder: {}", codec_name))
|
||||
})?;
|
||||
|
||||
let buffer_length = inner.length;
|
||||
let backend = EncoderBackend::from_codec_name(codec_name);
|
||||
let encoder_type = H265EncoderType::from(backend);
|
||||
|
||||
// Update config to reflect actual input format used
|
||||
let mut config = config;
|
||||
config.input_format = actual_input_format;
|
||||
|
||||
info!(
|
||||
"H.265 encoder created: {} (type: {}, buffer_length: {})",
|
||||
codec_name, encoder_type, buffer_length
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
config,
|
||||
encoder_type,
|
||||
codec_name: codec_name.to_string(),
|
||||
frame_count: 0,
|
||||
buffer_length,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create with auto-detected encoder
|
||||
pub fn auto(resolution: Resolution, bitrate_kbps: u32) -> Result<Self> {
|
||||
let config = H265Config::low_latency(resolution, bitrate_kbps);
|
||||
Self::new(config)
|
||||
}
|
||||
|
||||
/// Get encoder type
|
||||
pub fn encoder_type(&self) -> &H265EncoderType {
|
||||
&self.encoder_type
|
||||
}
|
||||
|
||||
/// Get codec name
|
||||
pub fn codec_name(&self) -> &str {
|
||||
&self.codec_name
|
||||
}
|
||||
|
||||
/// Update bitrate dynamically
|
||||
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
|
||||
self.inner
|
||||
.set_bitrate(bitrate_kbps as i32)
|
||||
.map_err(|_| AppError::VideoError("Failed to set H.265 bitrate".to_string()))?;
|
||||
self.config.bitrate_kbps = bitrate_kbps;
|
||||
debug!("H.265 bitrate updated to {} kbps", bitrate_kbps);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Request next frame to be a keyframe (IDR)
|
||||
pub fn request_keyframe(&mut self) {
|
||||
self.inner.request_keyframe();
|
||||
debug!("H265 keyframe requested");
|
||||
}
|
||||
|
||||
/// Encode raw frame data (NV12 or YUV420P depending on config)
|
||||
pub fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
|
||||
if data.len() < self.buffer_length as usize {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"Frame data too small: {} < {}",
|
||||
data.len(),
|
||||
self.buffer_length
|
||||
)));
|
||||
}
|
||||
|
||||
self.frame_count += 1;
|
||||
|
||||
// Debug log every 30 frames (1 second at 30fps)
|
||||
if self.frame_count % 30 == 1 {
|
||||
debug!(
|
||||
"[H265] Encoding frame #{}: input_size={}, pts_ms={}, codec={}",
|
||||
self.frame_count,
|
||||
data.len(),
|
||||
pts_ms,
|
||||
self.codec_name
|
||||
);
|
||||
}
|
||||
|
||||
match self.inner.encode(data, pts_ms) {
|
||||
Ok(frames) => {
|
||||
// Zero-copy: drain frames from hwcodec buffer instead of cloning
|
||||
let owned_frames: Vec<HwEncodeFrame> = frames
|
||||
.drain(..)
|
||||
.map(|f| HwEncodeFrame {
|
||||
data: f.data, // Move, not clone
|
||||
pts: f.pts,
|
||||
key: f.key,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Log encoded output
|
||||
if !owned_frames.is_empty() {
|
||||
let total_size: usize = owned_frames.iter().map(|f| f.data.len()).sum();
|
||||
let keyframe = owned_frames.iter().any(|f| f.key == 1);
|
||||
|
||||
if keyframe || self.frame_count % 30 == 1 {
|
||||
debug!(
|
||||
"[H265] Encoded frame #{}: output_size={}, keyframe={}, frame_count={}",
|
||||
self.frame_count,
|
||||
total_size,
|
||||
keyframe,
|
||||
owned_frames.len()
|
||||
);
|
||||
|
||||
// Log first few bytes of keyframe for debugging
|
||||
if keyframe && !owned_frames[0].data.is_empty() {
|
||||
let preview_len = owned_frames[0].data.len().min(32);
|
||||
debug!(
|
||||
"[H265] Keyframe data preview: {:02x?}",
|
||||
&owned_frames[0].data[..preview_len]
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"[H265] Encoder returned empty frame list for frame #{}",
|
||||
self.frame_count
|
||||
);
|
||||
}
|
||||
|
||||
Ok(owned_frames)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("[H265] Encode failed at frame #{}: {}", self.frame_count, e);
|
||||
Err(AppError::VideoError(format!("H.265 encode failed: {}", e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode NV12 data
|
||||
pub fn encode_nv12(&mut self, nv12_data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
|
||||
self.encode_raw(nv12_data, pts_ms)
|
||||
}
|
||||
|
||||
/// Get input format
|
||||
pub fn input_format(&self) -> H265InputFormat {
|
||||
self.config.input_format
|
||||
}
|
||||
|
||||
/// Get buffer info (linesize, offset, length)
|
||||
pub fn buffer_info(&self) -> (Vec<i32>, Vec<i32>, i32) {
|
||||
(
|
||||
self.inner.linesize.clone(),
|
||||
self.inner.offset.clone(),
|
||||
self.inner.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: H265Encoder contains hwcodec::ffmpeg_ram::encode::Encoder which has raw pointers
|
||||
// that are not Send by default. However, we ensure that H265Encoder is only used from
|
||||
// a single task/thread at a time (encoding is sequential), so this is safe.
|
||||
unsafe impl Send for H265Encoder {}
|
||||
|
||||
impl Encoder for H265Encoder {
|
||||
fn name(&self) -> &str {
|
||||
&self.codec_name
|
||||
}
|
||||
|
||||
fn output_format(&self) -> EncodedFormat {
|
||||
EncodedFormat::H265
|
||||
}
|
||||
|
||||
fn encode(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
|
||||
let pts_ms = (sequence * 1000 / self.config.fps as u64) as i64;
|
||||
|
||||
let mut frames = self.encode_raw(data, pts_ms)?;
|
||||
|
||||
if frames.is_empty() {
|
||||
warn!("H.265 encoder returned no frames");
|
||||
return Err(AppError::VideoError(
|
||||
"H.265 encoder returned no frames".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Take ownership of the first frame (zero-copy)
|
||||
let frame = frames.remove(0);
|
||||
let key_frame = frame.key == 1;
|
||||
|
||||
Ok(EncodedFrame {
|
||||
data: Bytes::from(frame.data), // Move Vec into Bytes (zero-copy)
|
||||
format: EncodedFormat::H265,
|
||||
resolution: self.config.base.resolution,
|
||||
key_frame,
|
||||
sequence,
|
||||
timestamp: std::time::Instant::now(),
|
||||
pts: frame.pts as u64,
|
||||
dts: frame.pts as u64,
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<Vec<EncodedFrame>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> Result<()> {
|
||||
self.frame_count = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn config(&self) -> &EncoderConfig {
|
||||
&self.config.base
|
||||
}
|
||||
|
||||
fn supports_format(&self, format: PixelFormat) -> bool {
|
||||
match self.config.input_format {
|
||||
H265InputFormat::Nv12 => matches!(format, PixelFormat::Nv12),
|
||||
H265InputFormat::Nv21 => matches!(format, PixelFormat::Nv21),
|
||||
H265InputFormat::Nv16 => matches!(format, PixelFormat::Nv16),
|
||||
H265InputFormat::Nv24 => matches!(format, PixelFormat::Nv24),
|
||||
H265InputFormat::Yuv420p => matches!(format, PixelFormat::Yuv420),
|
||||
H265InputFormat::Yuyv422 => matches!(format, PixelFormat::Yuyv),
|
||||
H265InputFormat::Rgb24 => matches!(format, PixelFormat::Rgb24),
|
||||
H265InputFormat::Bgr24 => matches!(format, PixelFormat::Bgr24),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_detect_h265_encoder() {
|
||||
let (encoder_type, codec_name) = detect_best_h265_encoder(1280, 720);
|
||||
println!(
|
||||
"Detected H.265 encoder: {:?} ({:?})",
|
||||
encoder_type, codec_name
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_available_h265_encoders() {
|
||||
let encoders = get_available_h265_encoders(1280, 720);
|
||||
println!("Available H.265 hardware encoders:");
|
||||
for enc in &encoders {
|
||||
println!(" - {} ({:?})", enc.name, enc.format);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_h265_availability() {
|
||||
let available = is_h265_available();
|
||||
println!("H.265 hardware encoding available: {}", available);
|
||||
}
|
||||
}
|
||||
404
src/video/codec/jpeg.rs
Normal file
404
src/video/codec/jpeg.rs
Normal file
@@ -0,0 +1,404 @@
|
||||
//! JPEG encoder implementation
|
||||
//!
|
||||
//! Provides JPEG encoding for raw video frames (YUYV, NV12, NV16, NV24, RGB, BGR)
|
||||
//! Uses libyuv for SIMD-accelerated color space conversion to I420,
|
||||
//! then turbojpeg for direct YUV encoding (skips internal color conversion).
|
||||
|
||||
use bytes::Bytes;
|
||||
|
||||
use super::traits::{EncodedFormat, EncodedFrame, EncoderConfig};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
/// JPEG encoder using libyuv + turbojpeg
|
||||
///
|
||||
/// Encoding pipeline (all SIMD accelerated):
|
||||
/// ```text
|
||||
/// YUYV/NV12/NV16/NV24/BGR24/RGB24 ──libyuv──> I420 ──turbojpeg──> JPEG
|
||||
/// ```
|
||||
///
|
||||
/// Note: This encoder is NOT thread-safe due to turbojpeg limitations.
|
||||
/// Use it from a single thread or wrap in a Mutex.
|
||||
pub struct JpegEncoder {
|
||||
config: EncoderConfig,
|
||||
compressor: turbojpeg::Compressor,
|
||||
/// I420 buffer for YUV encoding (Y + U + V planes)
|
||||
i420_buffer: Vec<u8>,
|
||||
/// Scratch buffer for split chroma planes when converting semiplanar 4:2:2 / 4:4:4 input.
|
||||
uv_split_buffer: Vec<u8>,
|
||||
/// BGRA buffer used when a source format needs explicit YUV matrix expansion before JPEG.
|
||||
bgra_buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl JpegEncoder {
|
||||
/// Create a new JPEG encoder
|
||||
pub fn new(config: EncoderConfig) -> Result<Self> {
|
||||
let resolution = config.resolution;
|
||||
let width = resolution.width as usize;
|
||||
let height = resolution.height as usize;
|
||||
// I420: Y = width*height, U = width*height/4, V = width*height/4
|
||||
let i420_size = width * height * 3 / 2;
|
||||
let max_uv_plane_size = width * height;
|
||||
let bgra_size = width * height * 4;
|
||||
|
||||
let mut compressor = turbojpeg::Compressor::new().map_err(|e| {
|
||||
AppError::VideoError(format!("Failed to create turbojpeg compressor: {}", e))
|
||||
})?;
|
||||
|
||||
compressor
|
||||
.set_quality(config.quality.min(100) as i32)
|
||||
.map_err(|e| AppError::VideoError(format!("Failed to set JPEG quality: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
compressor,
|
||||
i420_buffer: vec![0u8; i420_size],
|
||||
uv_split_buffer: vec![0u8; max_uv_plane_size * 2],
|
||||
bgra_buffer: vec![0u8; bgra_size],
|
||||
})
|
||||
}
|
||||
|
||||
/// Create with specific quality
|
||||
pub fn with_quality(resolution: Resolution, quality: u32) -> Result<Self> {
|
||||
let config = EncoderConfig::jpeg(resolution, quality);
|
||||
Self::new(config)
|
||||
}
|
||||
|
||||
/// Set JPEG quality (1-100)
|
||||
pub fn set_quality(&mut self, quality: u32) -> Result<()> {
|
||||
self.compressor
|
||||
.set_quality(quality.min(100) as i32)
|
||||
.map_err(|e| AppError::VideoError(format!("Failed to set JPEG quality: {}", e)))?;
|
||||
self.config.quality = quality;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encode I420 buffer to JPEG using turbojpeg's YUV encoder
|
||||
#[inline]
|
||||
fn encode_i420_to_jpeg(&mut self, sequence: u64) -> Result<EncodedFrame> {
|
||||
let width = self.config.resolution.width as usize;
|
||||
let height = self.config.resolution.height as usize;
|
||||
|
||||
// Create YuvImage for turbojpeg (I420 = YUV420 = Sub2x2)
|
||||
let yuv_image = turbojpeg::YuvImage {
|
||||
pixels: self.i420_buffer.as_slice(),
|
||||
width,
|
||||
height,
|
||||
align: 1, // No padding between rows
|
||||
subsamp: turbojpeg::Subsamp::Sub2x2, // YUV 4:2:0
|
||||
};
|
||||
|
||||
// Compress YUV directly to JPEG (skips color space conversion!)
|
||||
let jpeg_data = self
|
||||
.compressor
|
||||
.compress_yuv_to_vec(yuv_image)
|
||||
.map_err(|e| AppError::VideoError(format!("JPEG compression failed: {}", e)))?;
|
||||
|
||||
Ok(EncodedFrame::jpeg(
|
||||
Bytes::from(jpeg_data),
|
||||
self.config.resolution,
|
||||
sequence,
|
||||
))
|
||||
}
|
||||
|
||||
/// Encode BGRA buffer to JPEG using turbojpeg's RGB path.
|
||||
#[inline]
|
||||
fn encode_bgra_to_jpeg(&mut self, sequence: u64) -> Result<EncodedFrame> {
|
||||
let width = self.config.resolution.width as usize;
|
||||
let height = self.config.resolution.height as usize;
|
||||
|
||||
self.compressor
|
||||
.set_subsamp(turbojpeg::Subsamp::Sub2x2)
|
||||
.map_err(|e| AppError::VideoError(format!("Failed to set JPEG subsampling: {}", e)))?;
|
||||
|
||||
let image = turbojpeg::Image {
|
||||
pixels: self.bgra_buffer.as_slice(),
|
||||
width,
|
||||
pitch: width * 4,
|
||||
height,
|
||||
format: turbojpeg::PixelFormat::BGRA,
|
||||
};
|
||||
|
||||
let jpeg_data = self
|
||||
.compressor
|
||||
.compress_to_vec(image)
|
||||
.map_err(|e| AppError::VideoError(format!("JPEG compression failed: {}", e)))?;
|
||||
|
||||
Ok(EncodedFrame::jpeg(
|
||||
Bytes::from(jpeg_data),
|
||||
self.config.resolution,
|
||||
sequence,
|
||||
))
|
||||
}
|
||||
|
||||
/// Encode YUYV (YUV422) frame to JPEG
|
||||
pub fn encode_yuyv(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
|
||||
let width = self.config.resolution.width as usize;
|
||||
let height = self.config.resolution.height as usize;
|
||||
let expected_size = width * height * 2;
|
||||
|
||||
if data.len() < expected_size {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"YUYV data too small: {} < {}",
|
||||
data.len(),
|
||||
expected_size
|
||||
)));
|
||||
}
|
||||
|
||||
// Convert YUYV to I420 using libyuv (SIMD accelerated)
|
||||
libyuv::yuy2_to_i420(data, &mut self.i420_buffer, width as i32, height as i32)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv YUYV→I420 failed: {}", e)))?;
|
||||
|
||||
self.encode_i420_to_jpeg(sequence)
|
||||
}
|
||||
|
||||
/// YVYU → swap chroma to YUYV in scratch, then same as [`Self::encode_yuyv`].
|
||||
pub fn encode_yvyu(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
|
||||
let width = self.config.resolution.width as usize;
|
||||
let height = self.config.resolution.height as usize;
|
||||
let expected_size = width * height * 2;
|
||||
|
||||
if data.len() < expected_size {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"YVYU data too small: {} < {}",
|
||||
data.len(),
|
||||
expected_size
|
||||
)));
|
||||
}
|
||||
|
||||
// Reuse bgra_buffer as scratch for the swapped YUYV data.
|
||||
if self.bgra_buffer.len() < expected_size {
|
||||
self.bgra_buffer.resize(expected_size, 0);
|
||||
}
|
||||
let dst = &mut self.bgra_buffer[..expected_size];
|
||||
let src = &data[..expected_size];
|
||||
|
||||
// Swap bytes [1] and [3] in every 4-byte macropixel: Y0 V0 Y1 U0 → Y0 U0 Y1 V0
|
||||
for (chunk_dst, chunk_src) in dst.chunks_exact_mut(4).zip(src.chunks_exact(4)) {
|
||||
chunk_dst[0] = chunk_src[0]; // Y0
|
||||
chunk_dst[1] = chunk_src[3]; // U0
|
||||
chunk_dst[2] = chunk_src[2]; // Y1
|
||||
chunk_dst[3] = chunk_src[1]; // V0
|
||||
}
|
||||
|
||||
libyuv::yuy2_to_i420(dst, &mut self.i420_buffer, width as i32, height as i32)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv YVYU→I420 failed: {}", e)))?;
|
||||
|
||||
self.encode_i420_to_jpeg(sequence)
|
||||
}
|
||||
|
||||
/// Encode NV12 frame to JPEG
|
||||
pub fn encode_nv12(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
|
||||
let width = self.config.resolution.width as usize;
|
||||
let height = self.config.resolution.height as usize;
|
||||
let expected_size = width * height * 3 / 2;
|
||||
|
||||
if data.len() < expected_size {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"NV12 data too small: {} < {}",
|
||||
data.len(),
|
||||
expected_size
|
||||
)));
|
||||
}
|
||||
|
||||
// Convert NV12 to I420 using libyuv (SIMD accelerated)
|
||||
libyuv::nv12_to_i420(data, &mut self.i420_buffer, width as i32, height as i32)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv NV12→I420 failed: {}", e)))?;
|
||||
|
||||
self.encode_i420_to_jpeg(sequence)
|
||||
}
|
||||
|
||||
/// Encode NV16 frame to JPEG
|
||||
pub fn encode_nv16(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
|
||||
let width = self.config.resolution.width as usize;
|
||||
let height = self.config.resolution.height as usize;
|
||||
let y_size = width * height;
|
||||
let uv_size = y_size;
|
||||
let expected_size = y_size + uv_size;
|
||||
|
||||
if data.len() < expected_size {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"NV16 data too small: {} < {}",
|
||||
data.len(),
|
||||
expected_size
|
||||
)));
|
||||
}
|
||||
|
||||
let src_uv = &data[y_size..expected_size];
|
||||
let chroma_plane_size = y_size / 2;
|
||||
let (u_plane_422, rest) = self.uv_split_buffer.split_at_mut(chroma_plane_size);
|
||||
let (v_plane_422, _) = rest.split_at_mut(chroma_plane_size);
|
||||
|
||||
libyuv::split_uv_plane(
|
||||
src_uv,
|
||||
width as i32,
|
||||
u_plane_422,
|
||||
(width / 2) as i32,
|
||||
v_plane_422,
|
||||
(width / 2) as i32,
|
||||
(width / 2) as i32,
|
||||
height as i32,
|
||||
)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv NV16 split failed: {}", e)))?;
|
||||
|
||||
libyuv::i422_to_i420_planar(
|
||||
&data[..y_size],
|
||||
width as i32,
|
||||
u_plane_422,
|
||||
(width / 2) as i32,
|
||||
v_plane_422,
|
||||
(width / 2) as i32,
|
||||
&mut self.i420_buffer,
|
||||
width as i32,
|
||||
height as i32,
|
||||
)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv NV16→I420 failed: {}", e)))?;
|
||||
|
||||
self.encode_i420_to_jpeg(sequence)
|
||||
}
|
||||
|
||||
/// Encode NV24 frame to JPEG
|
||||
pub fn encode_nv24(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
|
||||
let width = self.config.resolution.width as usize;
|
||||
let height = self.config.resolution.height as usize;
|
||||
let y_size = width * height;
|
||||
let uv_size = y_size * 2;
|
||||
let expected_size = y_size + uv_size;
|
||||
|
||||
if data.len() < expected_size {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"NV24 data too small: {} < {}",
|
||||
data.len(),
|
||||
expected_size
|
||||
)));
|
||||
}
|
||||
|
||||
let src_uv = &data[y_size..expected_size];
|
||||
let chroma_plane_size = y_size;
|
||||
let (u_plane_444, rest) = self.uv_split_buffer.split_at_mut(chroma_plane_size);
|
||||
let (v_plane_444, _) = rest.split_at_mut(chroma_plane_size);
|
||||
|
||||
libyuv::split_uv_plane(
|
||||
src_uv,
|
||||
(width * 2) as i32,
|
||||
u_plane_444,
|
||||
width as i32,
|
||||
v_plane_444,
|
||||
width as i32,
|
||||
width as i32,
|
||||
height as i32,
|
||||
)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv NV24 split failed: {}", e)))?;
|
||||
|
||||
libyuv::h444_to_bgra(
|
||||
&data[..y_size],
|
||||
u_plane_444,
|
||||
v_plane_444,
|
||||
&mut self.bgra_buffer,
|
||||
width as i32,
|
||||
height as i32,
|
||||
)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv NV24(H444)→BGRA failed: {}", e)))?;
|
||||
|
||||
self.encode_bgra_to_jpeg(sequence)
|
||||
}
|
||||
|
||||
/// Encode RGB24 frame to JPEG
|
||||
pub fn encode_rgb(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
|
||||
let width = self.config.resolution.width as usize;
|
||||
let height = self.config.resolution.height as usize;
|
||||
let expected_size = width * height * 3;
|
||||
|
||||
if data.len() < expected_size {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"RGB data too small: {} < {}",
|
||||
data.len(),
|
||||
expected_size
|
||||
)));
|
||||
}
|
||||
|
||||
// Convert RGB24 to I420 using libyuv (SIMD accelerated)
|
||||
libyuv::rgb24_to_i420(data, &mut self.i420_buffer, width as i32, height as i32)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv RGB24→I420 failed: {}", e)))?;
|
||||
|
||||
self.encode_i420_to_jpeg(sequence)
|
||||
}
|
||||
|
||||
/// Encode BGR24 frame to JPEG
|
||||
pub fn encode_bgr(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
|
||||
let width = self.config.resolution.width as usize;
|
||||
let height = self.config.resolution.height as usize;
|
||||
let expected_size = width * height * 3;
|
||||
|
||||
if data.len() < expected_size {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"BGR data too small: {} < {}",
|
||||
data.len(),
|
||||
expected_size
|
||||
)));
|
||||
}
|
||||
|
||||
// Convert BGR24 to I420 using libyuv (SIMD accelerated)
|
||||
// Note: libyuv's RAWToI420 is BGR24 → I420
|
||||
libyuv::bgr24_to_i420(data, &mut self.i420_buffer, width as i32, height as i32)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv BGR24→I420 failed: {}", e)))?;
|
||||
|
||||
self.encode_i420_to_jpeg(sequence)
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::video::codec::traits::Encoder for JpegEncoder {
|
||||
fn name(&self) -> &str {
|
||||
"JPEG (libyuv+turbojpeg)"
|
||||
}
|
||||
|
||||
fn output_format(&self) -> EncodedFormat {
|
||||
EncodedFormat::Jpeg
|
||||
}
|
||||
|
||||
fn encode(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
|
||||
match self.config.input_format {
|
||||
PixelFormat::Yuyv => self.encode_yuyv(data, sequence),
|
||||
PixelFormat::Yvyu => self.encode_yvyu(data, sequence),
|
||||
PixelFormat::Nv12 => self.encode_nv12(data, sequence),
|
||||
PixelFormat::Nv16 => self.encode_nv16(data, sequence),
|
||||
PixelFormat::Nv24 => self.encode_nv24(data, sequence),
|
||||
PixelFormat::Rgb24 => self.encode_rgb(data, sequence),
|
||||
PixelFormat::Bgr24 => self.encode_bgr(data, sequence),
|
||||
_ => Err(AppError::VideoError(format!(
|
||||
"Unsupported input format for JPEG: {}",
|
||||
self.config.input_format
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn config(&self) -> &EncoderConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
fn supports_format(&self, format: PixelFormat) -> bool {
|
||||
matches!(
|
||||
format,
|
||||
PixelFormat::Yuyv
|
||||
| PixelFormat::Yvyu
|
||||
| PixelFormat::Nv12
|
||||
| PixelFormat::Nv16
|
||||
| PixelFormat::Nv24
|
||||
| PixelFormat::Rgb24
|
||||
| PixelFormat::Bgr24
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_i420_buffer_size() {
|
||||
// 1920x1080 I420 = 1920*1080 + 960*540 + 960*540 = 3110400 bytes
|
||||
let config = EncoderConfig::jpeg(Resolution::HD1080, 80);
|
||||
let encoder = JpegEncoder::new(config).unwrap();
|
||||
assert_eq!(encoder.i420_buffer.len(), 1920 * 1080 * 3 / 2);
|
||||
}
|
||||
}
|
||||
98
src/video/codec/mjpeg_rkmpp.rs
Normal file
98
src/video/codec/mjpeg_rkmpp.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
//! MJPEG decoder using RKMPP via hwcodec (FFmpeg RAM).
|
||||
|
||||
use hwcodec::ffmpeg::AVPixelFormat;
|
||||
use hwcodec::ffmpeg_ram::decode::{DecodeContext, Decoder};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::codec::convert::Nv12Converter;
|
||||
use crate::video::format::Resolution;
|
||||
|
||||
pub struct MjpegRkmppDecoder {
|
||||
decoder: Decoder,
|
||||
resolution: Resolution,
|
||||
nv16_to_nv12: Option<Nv12Converter>,
|
||||
last_pixfmt: Option<AVPixelFormat>,
|
||||
}
|
||||
|
||||
impl MjpegRkmppDecoder {
|
||||
pub fn new(resolution: Resolution) -> Result<Self> {
|
||||
let ctx = DecodeContext {
|
||||
name: "mjpeg_rkmpp".to_string(),
|
||||
width: resolution.width as i32,
|
||||
height: resolution.height as i32,
|
||||
sw_pixfmt: AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
thread_count: 1,
|
||||
};
|
||||
let decoder = Decoder::new(ctx).map_err(|_| {
|
||||
AppError::VideoError("Failed to create mjpeg_rkmpp decoder".to_string())
|
||||
})?;
|
||||
Ok(Self {
|
||||
decoder,
|
||||
resolution,
|
||||
nv16_to_nv12: None,
|
||||
last_pixfmt: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decode_to_nv12(&mut self, mjpeg: &[u8]) -> Result<Vec<u8>> {
|
||||
let frames = self
|
||||
.decoder
|
||||
.decode(mjpeg)
|
||||
.map_err(|e| AppError::VideoError(format!("mjpeg_rkmpp decode failed: {}", e)))?;
|
||||
if frames.is_empty() {
|
||||
return Err(AppError::VideoError(
|
||||
"mjpeg_rkmpp decode returned no frames".to_string(),
|
||||
));
|
||||
}
|
||||
if frames.len() > 1 {
|
||||
warn!(
|
||||
"mjpeg_rkmpp decode returned {} frames, using last",
|
||||
frames.len()
|
||||
);
|
||||
}
|
||||
let frame = frames
|
||||
.pop()
|
||||
.ok_or_else(|| AppError::VideoError("mjpeg_rkmpp decode returned empty".to_string()))?;
|
||||
|
||||
if frame.width as u32 != self.resolution.width
|
||||
|| frame.height as u32 != self.resolution.height
|
||||
{
|
||||
warn!(
|
||||
"mjpeg_rkmpp output size {}x{} differs from expected {}x{}",
|
||||
frame.width, frame.height, self.resolution.width, self.resolution.height
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(last) = self.last_pixfmt {
|
||||
if frame.pixfmt != last {
|
||||
warn!(
|
||||
"mjpeg_rkmpp output pixfmt changed from {:?} to {:?}",
|
||||
last, frame.pixfmt
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if frame.pixfmt == AVPixelFormat::AV_PIX_FMT_NV16 {
|
||||
info!("mjpeg_rkmpp output pixfmt NV16 on first frame; converting to NV12");
|
||||
}
|
||||
self.last_pixfmt = Some(frame.pixfmt);
|
||||
}
|
||||
|
||||
let pixfmt = self.last_pixfmt.unwrap_or(frame.pixfmt);
|
||||
match pixfmt {
|
||||
AVPixelFormat::AV_PIX_FMT_NV12 => Ok(frame.data),
|
||||
AVPixelFormat::AV_PIX_FMT_NV16 => {
|
||||
if self.nv16_to_nv12.is_none() {
|
||||
self.nv16_to_nv12 = Some(Nv12Converter::nv16_to_nv12(self.resolution));
|
||||
}
|
||||
let conv = self.nv16_to_nv12.as_mut().unwrap();
|
||||
let nv12 = conv.convert(&frame.data)?;
|
||||
Ok(nv12.to_vec())
|
||||
}
|
||||
other => Err(AppError::VideoError(format!(
|
||||
"mjpeg_rkmpp output pixfmt {:?} (expected NV12/NV16)",
|
||||
other
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/video/codec/mjpeg_turbo.rs
Normal file
54
src/video/codec/mjpeg_turbo.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
//! MJPEG decoder using TurboJPEG (software) -> RGB24.
|
||||
|
||||
use turbojpeg::{Decompressor, Image, PixelFormat as TJPixelFormat};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::format::Resolution;
|
||||
|
||||
pub struct MjpegTurboDecoder {
|
||||
decompressor: Decompressor,
|
||||
resolution: Resolution,
|
||||
}
|
||||
|
||||
impl MjpegTurboDecoder {
|
||||
pub fn new(resolution: Resolution) -> Result<Self> {
|
||||
let decompressor = Decompressor::new().map_err(|e| {
|
||||
AppError::VideoError(format!("Failed to create turbojpeg decoder: {}", e))
|
||||
})?;
|
||||
Ok(Self {
|
||||
decompressor,
|
||||
resolution,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decode_to_rgb(&mut self, mjpeg: &[u8]) -> Result<Vec<u8>> {
|
||||
let header = self
|
||||
.decompressor
|
||||
.read_header(mjpeg)
|
||||
.map_err(|e| AppError::VideoError(format!("turbojpeg read_header failed: {}", e)))?;
|
||||
|
||||
if header.width as u32 != self.resolution.width
|
||||
|| header.height as u32 != self.resolution.height
|
||||
{
|
||||
return Err(AppError::VideoError(format!(
|
||||
"turbojpeg size mismatch: {}x{} (expected {}x{})",
|
||||
header.width, header.height, self.resolution.width, self.resolution.height
|
||||
)));
|
||||
}
|
||||
|
||||
let pitch = header.width * 3;
|
||||
let mut image = Image {
|
||||
pixels: vec![0u8; header.height * pitch],
|
||||
width: header.width,
|
||||
pitch,
|
||||
height: header.height,
|
||||
format: TJPixelFormat::RGB,
|
||||
};
|
||||
|
||||
self.decompressor
|
||||
.decompress(mjpeg, image.as_deref_mut())
|
||||
.map_err(|e| AppError::VideoError(format!("turbojpeg decode failed: {}", e)))?;
|
||||
|
||||
Ok(image.pixels)
|
||||
}
|
||||
}
|
||||
72
src/video/codec/mod.rs
Normal file
72
src/video/codec/mod.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
//! Video codec, conversion, encoding, and decoding implementations.
|
||||
|
||||
use hwcodec::common::DataFormat;
|
||||
use hwcodec::ffmpeg_ram::CodecInfo;
|
||||
|
||||
pub mod convert;
|
||||
|
||||
pub mod h264;
|
||||
pub mod h264_bitstream;
|
||||
pub mod h265;
|
||||
pub mod jpeg;
|
||||
pub mod registry;
|
||||
pub mod self_check;
|
||||
pub mod traits;
|
||||
pub mod video_codec;
|
||||
pub mod vp8;
|
||||
pub mod vp9;
|
||||
|
||||
pub mod mjpeg_turbo;
|
||||
|
||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
||||
pub mod mjpeg_rkmpp;
|
||||
|
||||
pub use convert::{PixelConverter, Yuv420pBuffer};
|
||||
pub use h264::{H264Config, H264Encoder, H264EncoderType, H264InputFormat};
|
||||
pub use h265::{H265Config, H265Encoder, H265EncoderType, H265InputFormat};
|
||||
pub use jpeg::JpegEncoder;
|
||||
pub use mjpeg_turbo::MjpegTurboDecoder;
|
||||
pub use registry::{AvailableEncoder, EncoderBackend, EncoderRegistry, VideoEncoderType};
|
||||
pub use self_check::{
|
||||
build_hardware_self_check_runtime_error, run_hardware_self_check, VideoEncoderSelfCheckCell,
|
||||
VideoEncoderSelfCheckCodec, VideoEncoderSelfCheckResponse, VideoEncoderSelfCheckRow,
|
||||
};
|
||||
pub use traits::{
|
||||
BitratePreset, EncodedFormat, EncodedFrame, Encoder, EncoderConfig, EncoderFactory,
|
||||
};
|
||||
pub use video_codec::{
|
||||
CodecFrame, VideoCodec, VideoCodecConfig, VideoCodecFactory, VideoCodecType,
|
||||
};
|
||||
pub use vp8::{VP8Config, VP8Encoder, VP8EncoderType, VP8InputFormat};
|
||||
pub use vp9::{VP9Config, VP9Encoder, VP9EncoderType, VP9InputFormat};
|
||||
|
||||
pub(crate) fn select_codec_for_format<F>(
|
||||
encoders: &[CodecInfo],
|
||||
format: DataFormat,
|
||||
preferred: F,
|
||||
) -> Option<&CodecInfo>
|
||||
where
|
||||
F: Fn(&CodecInfo) -> bool,
|
||||
{
|
||||
encoders
|
||||
.iter()
|
||||
.find(|codec| codec.format == format && preferred(codec))
|
||||
.or_else(|| encoders.iter().find(|codec| codec.format == format))
|
||||
}
|
||||
|
||||
pub(crate) fn detect_best_codec_for_format<T, F>(
|
||||
encoders: &[CodecInfo],
|
||||
format: DataFormat,
|
||||
preferred: F,
|
||||
) -> Option<(T, String)>
|
||||
where
|
||||
T: From<EncoderBackend>,
|
||||
F: Fn(&CodecInfo) -> bool,
|
||||
{
|
||||
select_codec_for_format(encoders, format, preferred).map(|codec| {
|
||||
(
|
||||
T::from(EncoderBackend::from_codec_name(&codec.name)),
|
||||
codec.name.clone(),
|
||||
)
|
||||
})
|
||||
}
|
||||
566
src/video/codec/registry.rs
Normal file
566
src/video/codec/registry.rs
Normal file
@@ -0,0 +1,566 @@
|
||||
//! Encoder registry - Detection and management of available video encoders
|
||||
//!
|
||||
//! This module provides:
|
||||
//! - Automatic detection of available hardware/software encoders
|
||||
//! - Encoder selection based on format and priority
|
||||
//! - Global registry for encoder availability queries
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
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;
|
||||
|
||||
/// Video encoder format type
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum VideoEncoderType {
|
||||
/// H.264/AVC
|
||||
H264,
|
||||
/// H.265/HEVC
|
||||
H265,
|
||||
/// VP8
|
||||
VP8,
|
||||
/// VP9
|
||||
VP9,
|
||||
}
|
||||
|
||||
impl VideoEncoderType {
|
||||
pub const fn ordered() -> [Self; 4] {
|
||||
[Self::H264, Self::H265, Self::VP8, Self::VP9]
|
||||
}
|
||||
|
||||
/// Convert to hwcodec DataFormat
|
||||
pub fn to_data_format(&self) -> DataFormat {
|
||||
match self {
|
||||
VideoEncoderType::H264 => DataFormat::H264,
|
||||
VideoEncoderType::H265 => DataFormat::H265,
|
||||
VideoEncoderType::VP8 => DataFormat::VP8,
|
||||
VideoEncoderType::VP9 => DataFormat::VP9,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from hwcodec DataFormat
|
||||
pub fn from_data_format(format: DataFormat) -> Option<Self> {
|
||||
match format {
|
||||
DataFormat::H264 => Some(VideoEncoderType::H264),
|
||||
DataFormat::H265 => Some(VideoEncoderType::H265),
|
||||
DataFormat::VP8 => Some(VideoEncoderType::VP8),
|
||||
DataFormat::VP9 => Some(VideoEncoderType::VP9),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get codec name prefix for FFmpeg
|
||||
pub fn codec_prefix(&self) -> &'static str {
|
||||
match self {
|
||||
VideoEncoderType::H264 => "h264",
|
||||
VideoEncoderType::H265 => "hevc",
|
||||
VideoEncoderType::VP8 => "vp8",
|
||||
VideoEncoderType::VP9 => "vp9",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get display name
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
VideoEncoderType::H264 => "H.264",
|
||||
VideoEncoderType::H265 => "H.265/HEVC",
|
||||
VideoEncoderType::VP8 => "VP8",
|
||||
VideoEncoderType::VP9 => "VP9",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VideoEncoderType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.display_name())
|
||||
}
|
||||
}
|
||||
|
||||
/// Encoder backend type
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum EncoderBackend {
|
||||
/// Intel/AMD/NVIDIA VAAPI (Linux)
|
||||
Vaapi,
|
||||
/// NVIDIA NVENC
|
||||
Nvenc,
|
||||
/// Intel Quick Sync Video
|
||||
Qsv,
|
||||
/// AMD AMF
|
||||
Amf,
|
||||
/// Rockchip MPP
|
||||
Rkmpp,
|
||||
/// V4L2 Memory-to-Memory (ARM)
|
||||
V4l2m2m,
|
||||
/// Software encoding (libx264, libx265, libvpx)
|
||||
Software,
|
||||
}
|
||||
|
||||
impl EncoderBackend {
|
||||
/// Detect backend from codec name
|
||||
pub fn from_codec_name(name: &str) -> Self {
|
||||
if name.contains("vaapi") {
|
||||
EncoderBackend::Vaapi
|
||||
} else if name.contains("nvenc") {
|
||||
EncoderBackend::Nvenc
|
||||
} else if name.contains("qsv") {
|
||||
EncoderBackend::Qsv
|
||||
} else if name.contains("amf") {
|
||||
EncoderBackend::Amf
|
||||
} else if name.contains("rkmpp") {
|
||||
EncoderBackend::Rkmpp
|
||||
} else if name.contains("v4l2m2m") {
|
||||
EncoderBackend::V4l2m2m
|
||||
} else {
|
||||
EncoderBackend::Software
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is a hardware backend
|
||||
pub fn is_hardware(&self) -> bool {
|
||||
!matches!(self, EncoderBackend::Software)
|
||||
}
|
||||
|
||||
/// Get display name
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
EncoderBackend::Vaapi => "VAAPI",
|
||||
EncoderBackend::Nvenc => "NVENC",
|
||||
EncoderBackend::Qsv => "QSV",
|
||||
EncoderBackend::Amf => "AMF",
|
||||
EncoderBackend::Rkmpp => "RKMPP",
|
||||
EncoderBackend::V4l2m2m => "V4L2 M2M",
|
||||
EncoderBackend::Software => "Software",
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse from string (case-insensitive)
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"vaapi" => Some(EncoderBackend::Vaapi),
|
||||
"nvenc" => Some(EncoderBackend::Nvenc),
|
||||
"qsv" => Some(EncoderBackend::Qsv),
|
||||
"amf" => Some(EncoderBackend::Amf),
|
||||
"rkmpp" => Some(EncoderBackend::Rkmpp),
|
||||
"v4l2m2m" | "v4l2" => Some(EncoderBackend::V4l2m2m),
|
||||
"software" | "cpu" => Some(EncoderBackend::Software),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EncoderBackend {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.display_name())
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about an available encoder
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AvailableEncoder {
|
||||
/// Encoder format type
|
||||
pub format: VideoEncoderType,
|
||||
/// FFmpeg codec name (e.g., "h264_vaapi", "hevc_nvenc")
|
||||
pub codec_name: String,
|
||||
/// Backend type
|
||||
pub backend: EncoderBackend,
|
||||
/// Priority (lower is better)
|
||||
pub priority: i32,
|
||||
/// Whether this is a hardware encoder
|
||||
pub is_hardware: bool,
|
||||
}
|
||||
|
||||
impl AvailableEncoder {
|
||||
/// Create from hwcodec CodecInfo
|
||||
pub fn from_codec_info(info: &CodecInfo) -> Option<Self> {
|
||||
let format = VideoEncoderType::from_data_format(info.format)?;
|
||||
let backend = EncoderBackend::from_codec_name(&info.name);
|
||||
let is_hardware = backend.is_hardware();
|
||||
|
||||
Some(Self {
|
||||
format,
|
||||
codec_name: info.name.clone(),
|
||||
backend,
|
||||
priority: info.priority,
|
||||
is_hardware,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Global encoder registry
|
||||
///
|
||||
/// Detects and caches available encoders at startup.
|
||||
/// Use `EncoderRegistry::global()` to access the singleton instance.
|
||||
pub struct EncoderRegistry {
|
||||
/// Available encoders grouped by format
|
||||
encoders: HashMap<VideoEncoderType, Vec<AvailableEncoder>>,
|
||||
/// Detection resolution (used for testing)
|
||||
detection_resolution: (u32, u32),
|
||||
}
|
||||
|
||||
impl EncoderRegistry {
|
||||
fn detect_encoders_with_timeout(ctx: EncodeContext, timeout: Duration) -> Vec<CodecInfo> {
|
||||
use std::sync::mpsc;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let handle = std::thread::Builder::new()
|
||||
.name("ffmpeg-encoder-detect".to_string())
|
||||
.spawn(move || {
|
||||
let result = HwEncoder::available_encoders(ctx, None);
|
||||
let _ = tx.send(result);
|
||||
});
|
||||
|
||||
let Ok(handle) = handle else {
|
||||
warn!("Failed to spawn encoder detection thread");
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
match rx.recv_timeout(timeout) {
|
||||
Ok(encoders) => {
|
||||
let _ = handle.join();
|
||||
encoders
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||
warn!(
|
||||
"Encoder detection timed out after {}ms, skipping hardware detection",
|
||||
timeout.as_millis()
|
||||
);
|
||||
std::thread::spawn(move || {
|
||||
let _ = handle.join();
|
||||
});
|
||||
Vec::new()
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
let _ = handle.join();
|
||||
warn!("Encoder detection thread exited unexpectedly");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_software_fallbacks(&mut self) {
|
||||
info!("Registering software encoders...");
|
||||
|
||||
for format in VideoEncoderType::ordered() {
|
||||
let encoders = self.encoders.entry(format).or_default();
|
||||
if encoders.iter().any(|encoder| !encoder.is_hardware) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let codec_name = match format {
|
||||
VideoEncoderType::H264 => "libx264",
|
||||
VideoEncoderType::H265 => "libx265",
|
||||
VideoEncoderType::VP8 => "libvpx",
|
||||
VideoEncoderType::VP9 => "libvpx-vp9",
|
||||
};
|
||||
|
||||
encoders.push(AvailableEncoder {
|
||||
format,
|
||||
codec_name: codec_name.to_string(),
|
||||
backend: EncoderBackend::Software,
|
||||
priority: 100,
|
||||
is_hardware: false,
|
||||
});
|
||||
|
||||
debug!(
|
||||
"Registered software encoder: {} for {} (priority: {})",
|
||||
codec_name, format, 100
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the global registry instance
|
||||
///
|
||||
/// The registry is initialized lazily on first access with 1280x720 detection.
|
||||
pub fn global() -> &'static Self {
|
||||
static INSTANCE: OnceLock<EncoderRegistry> = OnceLock::new();
|
||||
INSTANCE.get_or_init(|| {
|
||||
let mut registry = EncoderRegistry::new();
|
||||
registry.detect_encoders(1280, 720);
|
||||
registry
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new empty registry
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
encoders: HashMap::new(),
|
||||
detection_resolution: (0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect all available encoders
|
||||
///
|
||||
/// This queries hwcodec/FFmpeg for available encoders and populates the registry.
|
||||
pub fn detect_encoders(&mut self, width: u32, height: u32) {
|
||||
info!("Detecting available video encoders at {}x{}", width, height);
|
||||
|
||||
self.encoders.clear();
|
||||
self.detection_resolution = (width, height);
|
||||
|
||||
// Create test context for encoder detection
|
||||
let ctx = EncodeContext {
|
||||
name: String::new(),
|
||||
mc_name: None,
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
|
||||
align: 1,
|
||||
fps: 30,
|
||||
gop: 30,
|
||||
rc: RateControl::RC_CBR,
|
||||
quality: Quality::Quality_Default,
|
||||
kbs: 2000,
|
||||
q: 23,
|
||||
thread_count: 1,
|
||||
};
|
||||
|
||||
const DETECT_TIMEOUT_MS: u64 = 5000;
|
||||
info!("Encoder detection timeout: {}ms", DETECT_TIMEOUT_MS);
|
||||
let all_encoders = Self::detect_encoders_with_timeout(
|
||||
ctx.clone(),
|
||||
Duration::from_millis(DETECT_TIMEOUT_MS),
|
||||
);
|
||||
|
||||
info!("Found {} encoders from hwcodec", all_encoders.len());
|
||||
|
||||
for codec_info in &all_encoders {
|
||||
if let Some(encoder) = AvailableEncoder::from_codec_info(codec_info) {
|
||||
debug!(
|
||||
"Detected encoder: {} ({}) - {} priority={}",
|
||||
encoder.codec_name, encoder.format, encoder.backend, encoder.priority
|
||||
);
|
||||
|
||||
self.encoders
|
||||
.entry(encoder.format)
|
||||
.or_default()
|
||||
.push(encoder);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort encoders by priority (lower is better)
|
||||
for encoders in self.encoders.values_mut() {
|
||||
encoders.sort_by_key(|e| e.priority);
|
||||
}
|
||||
|
||||
self.register_software_fallbacks();
|
||||
|
||||
// Log summary
|
||||
for (format, encoders) in &self.encoders {
|
||||
let hw_count = encoders.iter().filter(|e| e.is_hardware).count();
|
||||
let sw_count = encoders.len() - hw_count;
|
||||
info!(
|
||||
"{}: {} encoders ({} hardware, {} software)",
|
||||
format,
|
||||
encoders.len(),
|
||||
hw_count,
|
||||
sw_count
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the best encoder for a format
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `format` - The video format to encode
|
||||
/// * `hardware_only` - If true, only return hardware encoders
|
||||
///
|
||||
/// # Returns
|
||||
/// The best available encoder, or None if no suitable encoder is found
|
||||
pub fn best_encoder(
|
||||
&self,
|
||||
format: VideoEncoderType,
|
||||
hardware_only: bool,
|
||||
) -> Option<&AvailableEncoder> {
|
||||
self.encoders.get(&format)?.iter().find(
|
||||
|e| {
|
||||
if hardware_only {
|
||||
e.is_hardware
|
||||
} else {
|
||||
true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn best_available_encoder(&self, format: VideoEncoderType) -> Option<&AvailableEncoder> {
|
||||
self.best_encoder(format, false)
|
||||
}
|
||||
|
||||
/// Get all encoders for a format
|
||||
pub fn encoders_for_format(&self, format: VideoEncoderType) -> &[AvailableEncoder] {
|
||||
self.encoders
|
||||
.get(&format)
|
||||
.map(|v| v.as_slice())
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
/// Get all available formats
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `hardware_only` - If true, only return formats with hardware encoders
|
||||
pub fn available_formats(&self, hardware_only: bool) -> Vec<VideoEncoderType> {
|
||||
self.encoders
|
||||
.iter()
|
||||
.filter(|(_, encoders)| {
|
||||
if hardware_only {
|
||||
encoders.iter().any(|e| e.is_hardware)
|
||||
} else {
|
||||
!encoders.is_empty()
|
||||
}
|
||||
})
|
||||
.map(|(format, _)| *format)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Check if a format is available
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `format` - The video format to check
|
||||
/// * `hardware_only` - If true, only check for hardware encoders
|
||||
pub fn is_format_available(&self, format: VideoEncoderType, hardware_only: bool) -> bool {
|
||||
self.best_encoder(format, hardware_only).is_some()
|
||||
}
|
||||
|
||||
pub fn is_codec_available(&self, format: VideoEncoderType) -> bool {
|
||||
self.best_available_encoder(format).is_some()
|
||||
}
|
||||
|
||||
/// Get available formats for user selection
|
||||
///
|
||||
pub fn selectable_formats(&self) -> Vec<VideoEncoderType> {
|
||||
VideoEncoderType::ordered()
|
||||
.into_iter()
|
||||
.filter(|format| self.is_codec_available(*format))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get detection resolution
|
||||
pub fn detection_resolution(&self) -> (u32, u32) {
|
||||
self.detection_resolution
|
||||
}
|
||||
|
||||
/// Get all available backend types
|
||||
pub fn available_backends(&self) -> Vec<EncoderBackend> {
|
||||
use std::collections::HashSet;
|
||||
|
||||
let mut backends = HashSet::new();
|
||||
for encoders in self.encoders.values() {
|
||||
for encoder in encoders {
|
||||
backends.insert(encoder.backend);
|
||||
}
|
||||
}
|
||||
|
||||
let mut result: Vec<_> = backends.into_iter().collect();
|
||||
// Sort: hardware backends first, software last
|
||||
result.sort_by_key(|b| if b.is_hardware() { 0 } else { 1 });
|
||||
result
|
||||
}
|
||||
|
||||
/// Get formats supported by a specific backend
|
||||
pub fn formats_for_backend(&self, backend: EncoderBackend) -> Vec<VideoEncoderType> {
|
||||
let mut formats = Vec::new();
|
||||
for (format, encoders) in &self.encoders {
|
||||
if encoders.iter().any(|e| e.backend == backend) {
|
||||
formats.push(*format);
|
||||
}
|
||||
}
|
||||
formats
|
||||
}
|
||||
|
||||
/// Get encoder for a format with specific backend
|
||||
pub fn encoder_with_backend(
|
||||
&self,
|
||||
format: VideoEncoderType,
|
||||
backend: EncoderBackend,
|
||||
) -> Option<&AvailableEncoder> {
|
||||
self.encoders
|
||||
.get(&format)?
|
||||
.iter()
|
||||
.find(|e| e.backend == backend)
|
||||
}
|
||||
|
||||
/// Get encoders grouped by backend for a format
|
||||
pub fn encoders_by_backend(
|
||||
&self,
|
||||
format: VideoEncoderType,
|
||||
) -> HashMap<EncoderBackend, Vec<&AvailableEncoder>> {
|
||||
let mut grouped = HashMap::new();
|
||||
if let Some(encoders) = self.encoders.get(&format) {
|
||||
for encoder in encoders {
|
||||
grouped
|
||||
.entry(encoder.backend)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(encoder);
|
||||
}
|
||||
}
|
||||
grouped
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EncoderRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_video_encoder_type_display() {
|
||||
assert_eq!(VideoEncoderType::H264.display_name(), "H.264");
|
||||
assert_eq!(VideoEncoderType::H265.display_name(), "H.265/HEVC");
|
||||
assert_eq!(VideoEncoderType::VP8.display_name(), "VP8");
|
||||
assert_eq!(VideoEncoderType::VP9.display_name(), "VP9");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encoder_backend_detection() {
|
||||
assert_eq!(
|
||||
EncoderBackend::from_codec_name("h264_vaapi"),
|
||||
EncoderBackend::Vaapi
|
||||
);
|
||||
assert_eq!(
|
||||
EncoderBackend::from_codec_name("hevc_nvenc"),
|
||||
EncoderBackend::Nvenc
|
||||
);
|
||||
assert_eq!(
|
||||
EncoderBackend::from_codec_name("h264_qsv"),
|
||||
EncoderBackend::Qsv
|
||||
);
|
||||
assert_eq!(
|
||||
EncoderBackend::from_codec_name("libx264"),
|
||||
EncoderBackend::Software
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec_ordering() {
|
||||
assert_eq!(
|
||||
VideoEncoderType::ordered(),
|
||||
[
|
||||
VideoEncoderType::H264,
|
||||
VideoEncoderType::H265,
|
||||
VideoEncoderType::VP8,
|
||||
VideoEncoderType::VP9,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registry_detection() {
|
||||
let mut registry = EncoderRegistry::new();
|
||||
registry.detect_encoders(1280, 720);
|
||||
|
||||
// Should have detected at least H264 (software fallback available)
|
||||
println!("Available formats: {:?}", registry.available_formats(false));
|
||||
println!("Selectable formats: {:?}", registry.selectable_formats());
|
||||
}
|
||||
}
|
||||
335
src/video/codec/self_check.rs
Normal file
335
src/video/codec/self_check.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
use serde::Serialize;
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::{
|
||||
EncoderRegistry, H264Config, H264Encoder, H265Config, H265Encoder, VP8Config, VP8Encoder,
|
||||
VP9Config, VP9Encoder, VideoEncoderType,
|
||||
};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
const SELF_CHECK_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const SELF_CHECK_FRAME_ATTEMPTS: u64 = 3;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct VideoEncoderSelfCheckCodec {
|
||||
pub id: &'static str,
|
||||
pub name: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct VideoEncoderSelfCheckCell {
|
||||
pub codec_id: &'static str,
|
||||
pub ok: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub elapsed_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct VideoEncoderSelfCheckRow {
|
||||
pub resolution_id: &'static str,
|
||||
pub resolution_label: &'static str,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub cells: Vec<VideoEncoderSelfCheckCell>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct VideoEncoderSelfCheckResponse {
|
||||
pub current_hardware_encoder: String,
|
||||
pub codecs: Vec<VideoEncoderSelfCheckCodec>,
|
||||
pub rows: Vec<VideoEncoderSelfCheckRow>,
|
||||
}
|
||||
|
||||
pub fn run_hardware_self_check() -> VideoEncoderSelfCheckResponse {
|
||||
let registry = EncoderRegistry::global();
|
||||
let codecs = codec_columns();
|
||||
let mut rows = Vec::new();
|
||||
|
||||
for (resolution_id, resolution_label, resolution) in test_resolutions() {
|
||||
let mut cells = Vec::new();
|
||||
|
||||
for codec in test_codecs() {
|
||||
let cell = match registry.best_encoder(codec, true) {
|
||||
Some(encoder) => run_single_check(codec, resolution, encoder.codec_name.clone()),
|
||||
None => unsupported_cell(codec),
|
||||
};
|
||||
|
||||
cells.push(cell);
|
||||
}
|
||||
|
||||
rows.push(VideoEncoderSelfCheckRow {
|
||||
resolution_id,
|
||||
resolution_label,
|
||||
width: resolution.width,
|
||||
height: resolution.height,
|
||||
cells,
|
||||
});
|
||||
}
|
||||
|
||||
VideoEncoderSelfCheckResponse {
|
||||
current_hardware_encoder: current_hardware_encoder(registry),
|
||||
codecs,
|
||||
rows,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_hardware_self_check_runtime_error() -> VideoEncoderSelfCheckResponse {
|
||||
let codecs = codec_columns();
|
||||
let mut rows = Vec::new();
|
||||
|
||||
for (resolution_id, resolution_label, resolution) in test_resolutions() {
|
||||
let cells = test_codecs()
|
||||
.into_iter()
|
||||
.map(|codec| VideoEncoderSelfCheckCell {
|
||||
codec_id: codec_id(codec),
|
||||
ok: false,
|
||||
elapsed_ms: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
rows.push(VideoEncoderSelfCheckRow {
|
||||
resolution_id,
|
||||
resolution_label,
|
||||
width: resolution.width,
|
||||
height: resolution.height,
|
||||
cells,
|
||||
});
|
||||
}
|
||||
|
||||
VideoEncoderSelfCheckResponse {
|
||||
current_hardware_encoder: "None".to_string(),
|
||||
codecs,
|
||||
rows,
|
||||
}
|
||||
}
|
||||
|
||||
fn codec_columns() -> Vec<VideoEncoderSelfCheckCodec> {
|
||||
test_codecs()
|
||||
.into_iter()
|
||||
.map(|codec| VideoEncoderSelfCheckCodec {
|
||||
id: codec_id(codec),
|
||||
name: match codec {
|
||||
VideoEncoderType::H265 => "H.265",
|
||||
_ => codec.display_name(),
|
||||
},
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn test_codecs() -> [VideoEncoderType; 4] {
|
||||
[
|
||||
VideoEncoderType::H264,
|
||||
VideoEncoderType::H265,
|
||||
VideoEncoderType::VP8,
|
||||
VideoEncoderType::VP9,
|
||||
]
|
||||
}
|
||||
|
||||
fn test_resolutions() -> [(&'static str, &'static str, Resolution); 4] {
|
||||
[
|
||||
("720p", "720p", Resolution::HD720),
|
||||
("1080p", "1080p", Resolution::HD1080),
|
||||
("2k", "2K", Resolution::new(2560, 1440)),
|
||||
("4k", "4K", Resolution::UHD4K),
|
||||
]
|
||||
}
|
||||
|
||||
fn codec_id(codec: VideoEncoderType) -> &'static str {
|
||||
match codec {
|
||||
VideoEncoderType::H264 => "h264",
|
||||
VideoEncoderType::H265 => "h265",
|
||||
VideoEncoderType::VP8 => "vp8",
|
||||
VideoEncoderType::VP9 => "vp9",
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_cell(codec: VideoEncoderType) -> VideoEncoderSelfCheckCell {
|
||||
VideoEncoderSelfCheckCell {
|
||||
codec_id: codec_id(codec),
|
||||
ok: false,
|
||||
elapsed_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn run_single_check(
|
||||
codec: VideoEncoderType,
|
||||
resolution: Resolution,
|
||||
codec_name_ffmpeg: String,
|
||||
) -> VideoEncoderSelfCheckCell {
|
||||
let started = Instant::now();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let thread_codec_name = codec_name_ffmpeg.clone();
|
||||
|
||||
let spawn_result = std::thread::Builder::new()
|
||||
.name(format!(
|
||||
"encoder-self-check-{}-{}x{}",
|
||||
codec_id(codec),
|
||||
resolution.width,
|
||||
resolution.height
|
||||
))
|
||||
.spawn(move || {
|
||||
let _ = tx.send(run_smoke_test(codec, resolution, &thread_codec_name));
|
||||
});
|
||||
|
||||
if let Err(e) = spawn_result {
|
||||
let _ = e;
|
||||
return VideoEncoderSelfCheckCell {
|
||||
codec_id: codec_id(codec),
|
||||
ok: false,
|
||||
elapsed_ms: Some(started.elapsed().as_millis() as u64),
|
||||
};
|
||||
}
|
||||
|
||||
match rx.recv_timeout(SELF_CHECK_TIMEOUT) {
|
||||
Ok(Ok(())) => VideoEncoderSelfCheckCell {
|
||||
codec_id: codec_id(codec),
|
||||
ok: true,
|
||||
elapsed_ms: Some(started.elapsed().as_millis() as u64),
|
||||
},
|
||||
Ok(Err(_)) => VideoEncoderSelfCheckCell {
|
||||
codec_id: codec_id(codec),
|
||||
ok: false,
|
||||
elapsed_ms: Some(started.elapsed().as_millis() as u64),
|
||||
},
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => VideoEncoderSelfCheckCell {
|
||||
codec_id: codec_id(codec),
|
||||
ok: false,
|
||||
elapsed_ms: Some(started.elapsed().as_millis() as u64),
|
||||
},
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => VideoEncoderSelfCheckCell {
|
||||
codec_id: codec_id(codec),
|
||||
ok: false,
|
||||
elapsed_ms: Some(started.elapsed().as_millis() as u64),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn current_hardware_encoder(registry: &EncoderRegistry) -> String {
|
||||
let backends = registry
|
||||
.available_backends()
|
||||
.into_iter()
|
||||
.filter(|backend| backend.is_hardware())
|
||||
.map(|backend| backend.display_name().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if backends.is_empty() {
|
||||
"None".to_string()
|
||||
} else {
|
||||
backends.join("/")
|
||||
}
|
||||
}
|
||||
|
||||
fn run_smoke_test(
|
||||
codec: VideoEncoderType,
|
||||
resolution: Resolution,
|
||||
codec_name_ffmpeg: &str,
|
||||
) -> Result<()> {
|
||||
match codec {
|
||||
VideoEncoderType::H264 => run_h264_smoke_test(resolution, codec_name_ffmpeg),
|
||||
VideoEncoderType::H265 => run_h265_smoke_test(resolution, codec_name_ffmpeg),
|
||||
VideoEncoderType::VP8 => run_vp8_smoke_test(resolution, codec_name_ffmpeg),
|
||||
VideoEncoderType::VP9 => run_vp9_smoke_test(resolution, codec_name_ffmpeg),
|
||||
}
|
||||
}
|
||||
|
||||
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)),
|
||||
codec_name_ffmpeg,
|
||||
)?;
|
||||
encoder.request_keyframe();
|
||||
let frame = build_nv12_test_frame(resolution, encoder.yuv_info().2 as usize);
|
||||
|
||||
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
|
||||
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
|
||||
if frames.iter().any(|frame| !frame.data.is_empty()) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(AppError::VideoError(
|
||||
"Encoder produced no output after multiple frames".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn run_h265_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> {
|
||||
let mut encoder = H265Encoder::with_codec(
|
||||
H265Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)),
|
||||
codec_name_ffmpeg,
|
||||
)?;
|
||||
encoder.request_keyframe();
|
||||
let frame = build_nv12_test_frame(resolution, encoder.buffer_info().2 as usize);
|
||||
|
||||
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
|
||||
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
|
||||
if frames.iter().any(|frame| !frame.data.is_empty()) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(AppError::VideoError(
|
||||
"Encoder produced no output after multiple frames".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn run_vp8_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> {
|
||||
let mut encoder = VP8Encoder::with_codec(
|
||||
VP8Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)),
|
||||
codec_name_ffmpeg,
|
||||
)?;
|
||||
let frame = build_nv12_test_frame(resolution, encoder.buffer_info().2 as usize);
|
||||
|
||||
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
|
||||
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
|
||||
if frames.iter().any(|frame| !frame.data.is_empty()) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(AppError::VideoError(
|
||||
"Encoder produced no output after multiple frames".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn run_vp9_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> {
|
||||
let mut encoder = VP9Encoder::with_codec(
|
||||
VP9Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)),
|
||||
codec_name_ffmpeg,
|
||||
)?;
|
||||
let frame = build_nv12_test_frame(resolution, encoder.buffer_info().2 as usize);
|
||||
|
||||
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
|
||||
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
|
||||
if frames.iter().any(|frame| !frame.data.is_empty()) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(AppError::VideoError(
|
||||
"Encoder produced no output after multiple frames".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn build_nv12_test_frame(resolution: Resolution, buffer_length: usize) -> Vec<u8> {
|
||||
let minimum_length = PixelFormat::Nv12.frame_size(resolution).unwrap_or(0);
|
||||
let mut frame = vec![0x80; buffer_length.max(minimum_length)];
|
||||
let y_plane_len = (resolution.width * resolution.height) as usize;
|
||||
let fill_len = y_plane_len.min(frame.len());
|
||||
frame[..fill_len].fill(0x10);
|
||||
frame
|
||||
}
|
||||
|
||||
fn bitrate_kbps_for_resolution(resolution: Resolution) -> u32 {
|
||||
match resolution.width {
|
||||
0..=1280 => 4_000,
|
||||
1281..=1920 => 8_000,
|
||||
1921..=2560 => 12_000,
|
||||
_ => 20_000,
|
||||
}
|
||||
}
|
||||
|
||||
fn pts_ms(sequence: u64) -> i64 {
|
||||
((sequence * 1000) / 30) as i64
|
||||
}
|
||||
191
src/video/codec/traits.rs
Normal file
191
src/video/codec/traits.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
//! Encoder traits and common types
|
||||
|
||||
use bytes::Bytes;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
/// Defined in `config::schema` (typeshare + serde). Re-export for encoder users.
|
||||
pub use crate::config::BitratePreset;
|
||||
|
||||
/// Encoder configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncoderConfig {
|
||||
/// Target resolution
|
||||
pub resolution: Resolution,
|
||||
/// Input pixel format
|
||||
pub input_format: PixelFormat,
|
||||
/// Output quality (1-100 for JPEG, bitrate kbps for H264)
|
||||
pub quality: u32,
|
||||
/// Target frame rate
|
||||
pub fps: u32,
|
||||
/// Keyframe interval (for H264)
|
||||
pub gop_size: u32,
|
||||
}
|
||||
|
||||
impl Default for EncoderConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
resolution: Resolution::HD1080,
|
||||
input_format: PixelFormat::Yuyv,
|
||||
quality: 80,
|
||||
fps: 30,
|
||||
gop_size: 30,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderConfig {
|
||||
pub fn jpeg(resolution: Resolution, quality: u32) -> Self {
|
||||
Self {
|
||||
resolution,
|
||||
input_format: PixelFormat::Yuyv,
|
||||
quality,
|
||||
fps: 30,
|
||||
gop_size: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn h264(resolution: Resolution, bitrate_kbps: u32) -> Self {
|
||||
Self {
|
||||
resolution,
|
||||
input_format: PixelFormat::Yuyv,
|
||||
quality: bitrate_kbps,
|
||||
fps: 30,
|
||||
gop_size: 30,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encoded frame output
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncodedFrame {
|
||||
/// Encoded data
|
||||
pub data: Bytes,
|
||||
/// Output format (JPEG, H264, etc.)
|
||||
pub format: EncodedFormat,
|
||||
/// Resolution
|
||||
pub resolution: Resolution,
|
||||
/// Whether this is a key frame
|
||||
pub key_frame: bool,
|
||||
/// Frame sequence number
|
||||
pub sequence: u64,
|
||||
/// Encoding timestamp
|
||||
pub timestamp: Instant,
|
||||
/// Presentation timestamp (for video sync)
|
||||
pub pts: u64,
|
||||
/// Decode timestamp (for B-frames)
|
||||
pub dts: u64,
|
||||
}
|
||||
|
||||
impl EncodedFrame {
|
||||
pub fn jpeg(data: Bytes, resolution: Resolution, sequence: u64) -> Self {
|
||||
Self {
|
||||
data,
|
||||
format: EncodedFormat::Jpeg,
|
||||
resolution,
|
||||
key_frame: true,
|
||||
sequence,
|
||||
timestamp: Instant::now(),
|
||||
pts: sequence,
|
||||
dts: sequence,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn h264(
|
||||
data: Bytes,
|
||||
resolution: Resolution,
|
||||
key_frame: bool,
|
||||
sequence: u64,
|
||||
pts: u64,
|
||||
dts: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
data,
|
||||
format: EncodedFormat::H264,
|
||||
resolution,
|
||||
key_frame,
|
||||
sequence,
|
||||
timestamp: Instant::now(),
|
||||
pts,
|
||||
dts,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.data.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Encoded output format
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EncodedFormat {
|
||||
Jpeg,
|
||||
H264,
|
||||
H265,
|
||||
Vp8,
|
||||
Vp9,
|
||||
Av1,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EncodedFormat {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
EncodedFormat::Jpeg => write!(f, "JPEG"),
|
||||
EncodedFormat::H264 => write!(f, "H.264"),
|
||||
EncodedFormat::H265 => write!(f, "H.265"),
|
||||
EncodedFormat::Vp8 => write!(f, "VP8"),
|
||||
EncodedFormat::Vp9 => write!(f, "VP9"),
|
||||
EncodedFormat::Av1 => write!(f, "AV1"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic encoder trait
|
||||
/// Note: Not Sync because some encoders (like turbojpeg) are not thread-safe
|
||||
pub trait Encoder: Send {
|
||||
/// Get encoder name
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Get output format
|
||||
fn output_format(&self) -> EncodedFormat;
|
||||
|
||||
/// Encode a raw frame
|
||||
fn encode(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame>;
|
||||
|
||||
/// Flush any pending frames
|
||||
fn flush(&mut self) -> Result<Vec<EncodedFrame>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
/// Reset encoder state
|
||||
fn reset(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get current configuration
|
||||
fn config(&self) -> &EncoderConfig;
|
||||
|
||||
/// Check if encoder supports the given input format
|
||||
fn supports_format(&self, format: PixelFormat) -> bool;
|
||||
}
|
||||
|
||||
/// Encoder factory for creating encoders
|
||||
pub trait EncoderFactory: Send + Sync {
|
||||
/// Create an encoder with the given configuration
|
||||
fn create(&self, config: EncoderConfig) -> Result<Box<dyn Encoder>>;
|
||||
|
||||
/// Get encoder type name
|
||||
fn encoder_type(&self) -> &str;
|
||||
|
||||
/// Check if this encoder is available on the system
|
||||
fn is_available(&self) -> bool;
|
||||
|
||||
/// Get encoder priority (higher = preferred)
|
||||
fn priority(&self) -> u32;
|
||||
}
|
||||
370
src/video/codec/video_codec.rs
Normal file
370
src/video/codec/video_codec.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
//! WebRTC Video Codec abstraction layer
|
||||
//!
|
||||
//! This module provides a unified interface for video codecs used in WebRTC streaming.
|
||||
//! It supports multiple codec types (H264, VP8, VP9, H265) with a common API.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! ```text
|
||||
//! VideoCodec (trait)
|
||||
//! |
|
||||
//! +-- H264Codec (current implementation)
|
||||
//! +-- VP8Codec (reserved)
|
||||
//! +-- VP9Codec (reserved)
|
||||
//! +-- H265Codec (reserved)
|
||||
//! ```
|
||||
|
||||
use bytes::Bytes;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::video::format::Resolution;
|
||||
|
||||
/// Supported video codec types for WebRTC
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum VideoCodecType {
|
||||
/// H.264/AVC - widely supported, good compression
|
||||
H264,
|
||||
/// VP8 - royalty-free, good browser support
|
||||
VP8,
|
||||
/// VP9 - better compression than VP8
|
||||
VP9,
|
||||
/// H.265/HEVC - best compression, limited browser support
|
||||
H265,
|
||||
}
|
||||
|
||||
impl VideoCodecType {
|
||||
/// Get the codec name for SDP
|
||||
pub fn sdp_name(&self) -> &'static str {
|
||||
match self {
|
||||
VideoCodecType::H264 => "H264",
|
||||
VideoCodecType::VP8 => "VP8",
|
||||
VideoCodecType::VP9 => "VP9",
|
||||
VideoCodecType::H265 => "H265",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default RTP payload type
|
||||
pub fn default_payload_type(&self) -> u8 {
|
||||
match self {
|
||||
VideoCodecType::H264 => 96,
|
||||
VideoCodecType::VP8 => 97,
|
||||
VideoCodecType::VP9 => 98,
|
||||
VideoCodecType::H265 => 99,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the RTP clock rate (always 90000 for video)
|
||||
pub fn clock_rate(&self) -> u32 {
|
||||
90000
|
||||
}
|
||||
|
||||
/// Get the MIME type
|
||||
pub fn mime_type(&self) -> &'static str {
|
||||
match self {
|
||||
VideoCodecType::H264 => "video/H264",
|
||||
VideoCodecType::VP8 => "video/VP8",
|
||||
VideoCodecType::VP9 => "video/VP9",
|
||||
VideoCodecType::H265 => "video/H265",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VideoCodecType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.sdp_name())
|
||||
}
|
||||
}
|
||||
|
||||
/// Encoded video frame for WebRTC transmission
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CodecFrame {
|
||||
/// Encoded data (Annex B format for H264/H265, raw for VP8/VP9)
|
||||
pub data: Bytes,
|
||||
/// Presentation timestamp in milliseconds
|
||||
pub pts_ms: i64,
|
||||
/// Whether this is a keyframe (IDR for H264, key frame for VP8/VP9)
|
||||
pub is_keyframe: bool,
|
||||
/// Codec type
|
||||
pub codec: VideoCodecType,
|
||||
/// Frame sequence number
|
||||
pub sequence: u64,
|
||||
/// Frame duration
|
||||
pub duration: Duration,
|
||||
}
|
||||
|
||||
impl CodecFrame {
|
||||
/// Create a new H264 frame
|
||||
pub fn h264(data: Bytes, pts_ms: i64, is_keyframe: bool, sequence: u64, fps: u32) -> Self {
|
||||
Self {
|
||||
data,
|
||||
pts_ms,
|
||||
is_keyframe,
|
||||
codec: VideoCodecType::H264,
|
||||
sequence,
|
||||
duration: Duration::from_millis(1000 / fps as u64),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new VP8 frame
|
||||
pub fn vp8(data: Bytes, pts_ms: i64, is_keyframe: bool, sequence: u64, fps: u32) -> Self {
|
||||
Self {
|
||||
data,
|
||||
pts_ms,
|
||||
is_keyframe,
|
||||
codec: VideoCodecType::VP8,
|
||||
sequence,
|
||||
duration: Duration::from_millis(1000 / fps as u64),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new VP9 frame
|
||||
pub fn vp9(data: Bytes, pts_ms: i64, is_keyframe: bool, sequence: u64, fps: u32) -> Self {
|
||||
Self {
|
||||
data,
|
||||
pts_ms,
|
||||
is_keyframe,
|
||||
codec: VideoCodecType::VP9,
|
||||
sequence,
|
||||
duration: Duration::from_millis(1000 / fps as u64),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new H265 frame
|
||||
pub fn h265(data: Bytes, pts_ms: i64, is_keyframe: bool, sequence: u64, fps: u32) -> Self {
|
||||
Self {
|
||||
data,
|
||||
pts_ms,
|
||||
is_keyframe,
|
||||
codec: VideoCodecType::H265,
|
||||
sequence,
|
||||
duration: Duration::from_millis(1000 / fps as u64),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get frame size in bytes
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
/// Check if frame is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.data.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Video codec configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VideoCodecConfig {
|
||||
/// Codec type
|
||||
pub codec: VideoCodecType,
|
||||
/// Target resolution
|
||||
pub resolution: Resolution,
|
||||
/// Target bitrate in kbps
|
||||
pub bitrate_kbps: u32,
|
||||
/// Target FPS
|
||||
pub fps: u32,
|
||||
/// GOP size (keyframe interval in frames)
|
||||
pub gop_size: u32,
|
||||
/// Profile (codec-specific)
|
||||
pub profile: Option<String>,
|
||||
/// Level (codec-specific)
|
||||
pub level: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for VideoCodecConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
codec: VideoCodecType::H264,
|
||||
resolution: Resolution::HD720,
|
||||
bitrate_kbps: 8000,
|
||||
fps: 30,
|
||||
gop_size: 30,
|
||||
profile: None,
|
||||
level: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoCodecConfig {
|
||||
/// Create H264 config with common settings
|
||||
pub fn h264(resolution: Resolution, bitrate_kbps: u32, fps: u32) -> Self {
|
||||
Self {
|
||||
codec: VideoCodecType::H264,
|
||||
resolution,
|
||||
bitrate_kbps,
|
||||
fps,
|
||||
gop_size: fps, // 1 second GOP
|
||||
profile: Some("baseline".to_string()),
|
||||
level: Some("3.1".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create VP8 config
|
||||
pub fn vp8(resolution: Resolution, bitrate_kbps: u32, fps: u32) -> Self {
|
||||
Self {
|
||||
codec: VideoCodecType::VP8,
|
||||
resolution,
|
||||
bitrate_kbps,
|
||||
fps,
|
||||
gop_size: fps,
|
||||
profile: None,
|
||||
level: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create VP9 config
|
||||
pub fn vp9(resolution: Resolution, bitrate_kbps: u32, fps: u32) -> Self {
|
||||
Self {
|
||||
codec: VideoCodecType::VP9,
|
||||
resolution,
|
||||
bitrate_kbps,
|
||||
fps,
|
||||
gop_size: fps,
|
||||
profile: None,
|
||||
level: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create H265 config
|
||||
pub fn h265(resolution: Resolution, bitrate_kbps: u32, fps: u32) -> Self {
|
||||
Self {
|
||||
codec: VideoCodecType::H265,
|
||||
resolution,
|
||||
bitrate_kbps,
|
||||
fps,
|
||||
gop_size: fps,
|
||||
profile: Some("main".to_string()),
|
||||
level: Some("4.0".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WebRTC video codec trait
|
||||
///
|
||||
/// This trait defines the interface for video codecs used in WebRTC streaming.
|
||||
/// Implementations should handle format conversion internally if needed.
|
||||
pub trait VideoCodec: Send {
|
||||
/// Get codec type
|
||||
fn codec_type(&self) -> VideoCodecType;
|
||||
|
||||
/// Get codec name for display
|
||||
fn codec_name(&self) -> &'static str;
|
||||
|
||||
/// Get RTP payload type
|
||||
fn payload_type(&self) -> u8 {
|
||||
self.codec_type().default_payload_type()
|
||||
}
|
||||
|
||||
/// Get SDP fmtp parameters (codec-specific)
|
||||
///
|
||||
/// For H264: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=<sps>"
|
||||
/// For VP8/VP9: None or empty
|
||||
fn sdp_fmtp(&self) -> Option<String>;
|
||||
|
||||
/// Encode a raw frame (NV12 format expected)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `frame` - Raw frame data in NV12 format
|
||||
/// * `pts_ms` - Presentation timestamp in milliseconds
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(Some(frame))` - Encoded frame
|
||||
/// * `Ok(None)` - Encoder is buffering (no output yet)
|
||||
/// * `Err(e)` - Encoding error
|
||||
fn encode(&mut self, frame: &[u8], pts_ms: i64) -> Result<Option<CodecFrame>>;
|
||||
|
||||
/// Set target bitrate dynamically
|
||||
fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()>;
|
||||
|
||||
/// Request a keyframe on next encode
|
||||
fn request_keyframe(&mut self);
|
||||
|
||||
/// Get current configuration
|
||||
fn config(&self) -> &VideoCodecConfig;
|
||||
|
||||
/// Flush any pending frames
|
||||
fn flush(&mut self) -> Result<Vec<CodecFrame>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
/// Reset encoder state
|
||||
fn reset(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Video codec factory trait
|
||||
///
|
||||
/// Used to create codec instances and query available codecs.
|
||||
pub trait VideoCodecFactory: Send + Sync {
|
||||
/// Create a codec with the given configuration
|
||||
fn create(&self, config: VideoCodecConfig) -> Result<Box<dyn VideoCodec>>;
|
||||
|
||||
/// Get supported codec types
|
||||
fn supported_codecs(&self) -> Vec<VideoCodecType>;
|
||||
|
||||
/// Check if a specific codec is available
|
||||
fn is_codec_available(&self, codec: VideoCodecType) -> bool {
|
||||
self.supported_codecs().contains(&codec)
|
||||
}
|
||||
|
||||
/// Get the best available codec (based on priority)
|
||||
fn best_codec(&self) -> Option<VideoCodecType> {
|
||||
// Priority: H264 > VP8 > VP9 > H265
|
||||
let supported = self.supported_codecs();
|
||||
if supported.contains(&VideoCodecType::H264) {
|
||||
Some(VideoCodecType::H264)
|
||||
} else if supported.contains(&VideoCodecType::VP8) {
|
||||
Some(VideoCodecType::VP8)
|
||||
} else if supported.contains(&VideoCodecType::VP9) {
|
||||
Some(VideoCodecType::VP9)
|
||||
} else if supported.contains(&VideoCodecType::H265) {
|
||||
Some(VideoCodecType::H265)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_codec_type_properties() {
|
||||
assert_eq!(VideoCodecType::H264.sdp_name(), "H264");
|
||||
assert_eq!(VideoCodecType::H264.default_payload_type(), 96);
|
||||
assert_eq!(VideoCodecType::H264.clock_rate(), 90000);
|
||||
assert_eq!(VideoCodecType::H264.mime_type(), "video/H264");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec_frame_creation() {
|
||||
let data = Bytes::from(vec![0x00, 0x00, 0x00, 0x01, 0x65]);
|
||||
let frame = CodecFrame::h264(data.clone(), 1000, true, 1, 30);
|
||||
|
||||
assert_eq!(frame.codec, VideoCodecType::H264);
|
||||
assert!(frame.is_keyframe);
|
||||
assert_eq!(frame.pts_ms, 1000);
|
||||
assert_eq!(frame.sequence, 1);
|
||||
assert_eq!(frame.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec_config_default() {
|
||||
let config = VideoCodecConfig::default();
|
||||
assert_eq!(config.codec, VideoCodecType::H264);
|
||||
assert_eq!(config.bitrate_kbps, 8000);
|
||||
assert_eq!(config.fps, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec_config_h264() {
|
||||
let config = VideoCodecConfig::h264(Resolution::HD1080, 4000, 60);
|
||||
assert_eq!(config.codec, VideoCodecType::H264);
|
||||
assert_eq!(config.bitrate_kbps, 4000);
|
||||
assert_eq!(config.fps, 60);
|
||||
assert_eq!(config.gop_size, 60);
|
||||
}
|
||||
}
|
||||
483
src/video/codec/vp8.rs
Normal file
483
src/video/codec/vp8.rs
Normal file
@@ -0,0 +1,483 @@
|
||||
//! VP8 encoder using hwcodec (FFmpeg wrapper)
|
||||
//!
|
||||
//! Supports both hardware and software encoding:
|
||||
//! - Hardware: VAAPI (Intel on Linux)
|
||||
//! - Software: libvpx (CPU-based, high CPU usage)
|
||||
//!
|
||||
//! Hardware encoding is preferred when available for better performance.
|
||||
|
||||
use bytes::Bytes;
|
||||
use std::sync::Once;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
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;
|
||||
|
||||
use super::detect_best_codec_for_format;
|
||||
use super::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType};
|
||||
use super::traits::{EncodedFormat, EncodedFrame, Encoder, EncoderConfig};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
static INIT_LOGGING: Once = Once::new();
|
||||
|
||||
/// Initialize hwcodec logging (only once)
|
||||
fn init_hwcodec_logging() {
|
||||
INIT_LOGGING.call_once(|| {
|
||||
debug!("hwcodec logging initialized for VP8");
|
||||
});
|
||||
}
|
||||
|
||||
/// VP8 encoder type (detected from hwcodec)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum VP8EncoderType {
|
||||
/// VAAPI (Intel on Linux)
|
||||
Vaapi,
|
||||
/// Software encoder (libvpx)
|
||||
Software,
|
||||
/// No encoder available
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VP8EncoderType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
VP8EncoderType::Vaapi => write!(f, "VAAPI"),
|
||||
VP8EncoderType::Software => write!(f, "Software"),
|
||||
VP8EncoderType::None => write!(f, "None"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EncoderBackend> for VP8EncoderType {
|
||||
fn from(backend: EncoderBackend) -> Self {
|
||||
match backend {
|
||||
EncoderBackend::Vaapi => VP8EncoderType::Vaapi,
|
||||
EncoderBackend::Software => VP8EncoderType::Software,
|
||||
_ => VP8EncoderType::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Input pixel format for VP8 encoder
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum VP8InputFormat {
|
||||
/// YUV420P (I420) - planar Y, U, V
|
||||
Yuv420p,
|
||||
/// NV12 - Y plane + interleaved UV plane
|
||||
#[default]
|
||||
Nv12,
|
||||
}
|
||||
|
||||
/// VP8 encoder configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VP8Config {
|
||||
/// Base encoder config
|
||||
pub base: EncoderConfig,
|
||||
/// Target bitrate in kbps
|
||||
pub bitrate_kbps: u32,
|
||||
/// GOP size (keyframe interval)
|
||||
pub gop_size: u32,
|
||||
/// Frame rate
|
||||
pub fps: u32,
|
||||
/// Input pixel format
|
||||
pub input_format: VP8InputFormat,
|
||||
}
|
||||
|
||||
impl Default for VP8Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base: EncoderConfig::default(),
|
||||
bitrate_kbps: 8000,
|
||||
gop_size: 30,
|
||||
fps: 30,
|
||||
input_format: VP8InputFormat::Nv12,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VP8Config {
|
||||
/// Create config for low latency streaming with NV12 input
|
||||
pub fn low_latency(resolution: Resolution, bitrate_kbps: u32) -> Self {
|
||||
Self {
|
||||
base: EncoderConfig {
|
||||
resolution,
|
||||
input_format: PixelFormat::Nv12,
|
||||
quality: bitrate_kbps,
|
||||
fps: 30,
|
||||
gop_size: 30,
|
||||
},
|
||||
bitrate_kbps,
|
||||
gop_size: 30,
|
||||
fps: 30,
|
||||
input_format: VP8InputFormat::Nv12,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set input format
|
||||
pub fn with_input_format(mut self, format: VP8InputFormat) -> Self {
|
||||
self.input_format = format;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Get available VP8 hardware encoders from hwcodec
|
||||
pub fn get_available_vp8_encoders(width: u32, height: u32) -> Vec<CodecInfo> {
|
||||
init_hwcodec_logging();
|
||||
|
||||
let ctx = EncodeContext {
|
||||
name: String::new(),
|
||||
mc_name: None,
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
|
||||
align: 1,
|
||||
fps: 30,
|
||||
gop: 30,
|
||||
rc: RateControl::RC_CBR,
|
||||
quality: Quality::Quality_Default,
|
||||
kbs: 2000,
|
||||
q: 23,
|
||||
thread_count: 1,
|
||||
};
|
||||
|
||||
let all_encoders = HwEncoder::available_encoders(ctx, None);
|
||||
|
||||
// Include both hardware and software VP8 encoders
|
||||
all_encoders
|
||||
.into_iter()
|
||||
.filter(|e| e.format == DataFormat::VP8)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Detect best available VP8 encoder (hardware preferred, software fallback)
|
||||
pub fn detect_best_vp8_encoder(width: u32, height: u32) -> (VP8EncoderType, Option<String>) {
|
||||
let encoders = get_available_vp8_encoders(width, height);
|
||||
|
||||
// Prefer hardware encoders (VAAPI) over software (libvpx)
|
||||
if let Some((encoder_type, codec_name)) =
|
||||
detect_best_codec_for_format(&encoders, DataFormat::VP8, |codec| {
|
||||
codec.name.contains("vaapi")
|
||||
})
|
||||
{
|
||||
info!("Selected VP8 encoder: {} ({})", codec_name, encoder_type);
|
||||
(encoder_type, Some(codec_name))
|
||||
} else {
|
||||
warn!("No VP8 encoders available");
|
||||
(VP8EncoderType::None, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if VP8 hardware encoding is available
|
||||
pub fn is_vp8_available() -> bool {
|
||||
let registry = EncoderRegistry::global();
|
||||
registry.is_codec_available(VideoEncoderType::VP8)
|
||||
}
|
||||
|
||||
/// Encoded frame from hwcodec (cloned for ownership)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HwEncodeFrame {
|
||||
pub data: Vec<u8>,
|
||||
pub pts: i64,
|
||||
pub key: i32,
|
||||
}
|
||||
|
||||
/// VP8 encoder using hwcodec
|
||||
pub struct VP8Encoder {
|
||||
/// hwcodec encoder instance
|
||||
inner: HwEncoder,
|
||||
/// Encoder configuration
|
||||
config: VP8Config,
|
||||
/// Detected encoder type
|
||||
encoder_type: VP8EncoderType,
|
||||
/// Codec name
|
||||
codec_name: String,
|
||||
/// Frame counter
|
||||
frame_count: u64,
|
||||
/// Required buffer length from hwcodec
|
||||
buffer_length: i32,
|
||||
}
|
||||
|
||||
impl VP8Encoder {
|
||||
/// Create a new VP8 encoder with automatic hardware codec detection
|
||||
///
|
||||
/// Returns an error if no hardware encoder is available.
|
||||
/// VP8 hardware encoding requires Intel VAAPI support.
|
||||
pub fn new(config: VP8Config) -> Result<Self> {
|
||||
init_hwcodec_logging();
|
||||
|
||||
let width = config.base.resolution.width;
|
||||
let height = config.base.resolution.height;
|
||||
|
||||
let (encoder_type, codec_name) = detect_best_vp8_encoder(width, height);
|
||||
|
||||
if encoder_type == VP8EncoderType::None {
|
||||
return Err(AppError::VideoError(
|
||||
"No VP8 encoder available. Please ensure FFmpeg is built with libvpx support."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let codec_name = codec_name.unwrap();
|
||||
Self::with_codec(config, &codec_name)
|
||||
}
|
||||
|
||||
/// Create encoder with specific codec name
|
||||
pub fn with_codec(config: VP8Config, codec_name: &str) -> Result<Self> {
|
||||
init_hwcodec_logging();
|
||||
|
||||
// Determine if this is a software encoder
|
||||
let is_software = codec_name.contains("libvpx");
|
||||
|
||||
// Warn about software encoder performance
|
||||
if is_software {
|
||||
warn!(
|
||||
"Using software VP8 encoder (libvpx) - high CPU usage expected. \
|
||||
Hardware encoder is recommended for better performance."
|
||||
);
|
||||
}
|
||||
|
||||
let width = config.base.resolution.width;
|
||||
let height = config.base.resolution.height;
|
||||
|
||||
// Software encoders (libvpx) require YUV420P, hardware (VAAPI) uses NV12
|
||||
let (pixfmt_name, pixfmt_fallback, actual_input_format) = if is_software {
|
||||
(
|
||||
"yuv420p",
|
||||
AVPixelFormat::AV_PIX_FMT_YUV420P,
|
||||
VP8InputFormat::Yuv420p,
|
||||
)
|
||||
} else {
|
||||
match config.input_format {
|
||||
VP8InputFormat::Nv12 => {
|
||||
("nv12", AVPixelFormat::AV_PIX_FMT_NV12, VP8InputFormat::Nv12)
|
||||
}
|
||||
VP8InputFormat::Yuv420p => (
|
||||
"yuv420p",
|
||||
AVPixelFormat::AV_PIX_FMT_YUV420P,
|
||||
VP8InputFormat::Yuv420p,
|
||||
),
|
||||
}
|
||||
};
|
||||
let pixfmt = resolve_pixel_format(pixfmt_name, pixfmt_fallback);
|
||||
|
||||
info!(
|
||||
"Creating VP8 encoder: {} at {}x{} @ {} kbps (input: {:?})",
|
||||
codec_name, width, height, config.bitrate_kbps, actual_input_format
|
||||
);
|
||||
|
||||
let ctx = EncodeContext {
|
||||
name: codec_name.to_string(),
|
||||
mc_name: None,
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
pixfmt,
|
||||
align: 1,
|
||||
fps: config.fps as i32,
|
||||
gop: config.gop_size as i32,
|
||||
rc: RateControl::RC_CBR,
|
||||
quality: Quality::Quality_Default,
|
||||
kbs: config.bitrate_kbps as i32,
|
||||
q: 23,
|
||||
thread_count: 1,
|
||||
};
|
||||
|
||||
let inner = HwEncoder::new(ctx).map_err(|_| {
|
||||
AppError::VideoError(format!("Failed to create VP8 encoder: {}", codec_name))
|
||||
})?;
|
||||
|
||||
let buffer_length = inner.length;
|
||||
let backend = EncoderBackend::from_codec_name(codec_name);
|
||||
let encoder_type = VP8EncoderType::from(backend);
|
||||
|
||||
// Update config to reflect actual input format used
|
||||
let mut config = config;
|
||||
config.input_format = actual_input_format;
|
||||
|
||||
info!(
|
||||
"VP8 encoder created: {} (type: {}, buffer_length: {})",
|
||||
codec_name, encoder_type, buffer_length
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
config,
|
||||
encoder_type,
|
||||
codec_name: codec_name.to_string(),
|
||||
frame_count: 0,
|
||||
buffer_length,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create with auto-detected encoder
|
||||
pub fn auto(resolution: Resolution, bitrate_kbps: u32) -> Result<Self> {
|
||||
let config = VP8Config::low_latency(resolution, bitrate_kbps);
|
||||
Self::new(config)
|
||||
}
|
||||
|
||||
/// Get encoder type
|
||||
pub fn encoder_type(&self) -> &VP8EncoderType {
|
||||
&self.encoder_type
|
||||
}
|
||||
|
||||
/// Get codec name
|
||||
pub fn codec_name(&self) -> &str {
|
||||
&self.codec_name
|
||||
}
|
||||
|
||||
/// Update bitrate dynamically
|
||||
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
|
||||
self.inner
|
||||
.set_bitrate(bitrate_kbps as i32)
|
||||
.map_err(|_| AppError::VideoError("Failed to set VP8 bitrate".to_string()))?;
|
||||
self.config.bitrate_kbps = bitrate_kbps;
|
||||
debug!("VP8 bitrate updated to {} kbps", bitrate_kbps);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encode raw frame data
|
||||
pub fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
|
||||
if data.len() < self.buffer_length as usize {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"Frame data too small: {} < {}",
|
||||
data.len(),
|
||||
self.buffer_length
|
||||
)));
|
||||
}
|
||||
|
||||
self.frame_count += 1;
|
||||
|
||||
match self.inner.encode(data, pts_ms) {
|
||||
Ok(frames) => {
|
||||
// Zero-copy: drain frames from hwcodec buffer instead of cloning
|
||||
let owned_frames: Vec<HwEncodeFrame> = frames
|
||||
.drain(..)
|
||||
.map(|f| HwEncodeFrame {
|
||||
data: f.data, // Move, not clone
|
||||
pts: f.pts,
|
||||
key: f.key,
|
||||
})
|
||||
.collect();
|
||||
Ok(owned_frames)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("VP8 encode failed: {}", e);
|
||||
Err(AppError::VideoError(format!("VP8 encode failed: {}", e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode NV12 data
|
||||
pub fn encode_nv12(&mut self, nv12_data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
|
||||
self.encode_raw(nv12_data, pts_ms)
|
||||
}
|
||||
|
||||
/// Get input format
|
||||
pub fn input_format(&self) -> VP8InputFormat {
|
||||
self.config.input_format
|
||||
}
|
||||
|
||||
/// Get buffer info
|
||||
pub fn buffer_info(&self) -> (Vec<i32>, Vec<i32>, i32) {
|
||||
(
|
||||
self.inner.linesize.clone(),
|
||||
self.inner.offset.clone(),
|
||||
self.inner.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: VP8Encoder contains hwcodec::ffmpeg_ram::encode::Encoder which has raw pointers
|
||||
// that are not Send by default. However, we ensure that VP8Encoder is only used from
|
||||
// a single task/thread at a time (encoding is sequential), so this is safe.
|
||||
unsafe impl Send for VP8Encoder {}
|
||||
|
||||
impl Encoder for VP8Encoder {
|
||||
fn name(&self) -> &str {
|
||||
&self.codec_name
|
||||
}
|
||||
|
||||
fn output_format(&self) -> EncodedFormat {
|
||||
EncodedFormat::Vp8
|
||||
}
|
||||
|
||||
fn encode(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
|
||||
let pts_ms = (sequence * 1000 / self.config.fps as u64) as i64;
|
||||
|
||||
let mut frames = self.encode_raw(data, pts_ms)?;
|
||||
|
||||
if frames.is_empty() {
|
||||
warn!("VP8 encoder returned no frames");
|
||||
return Err(AppError::VideoError(
|
||||
"VP8 encoder returned no frames".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Take ownership of the first frame (zero-copy)
|
||||
let frame = frames.remove(0);
|
||||
let key_frame = frame.key == 1;
|
||||
|
||||
Ok(EncodedFrame {
|
||||
data: Bytes::from(frame.data), // Move Vec into Bytes (zero-copy)
|
||||
format: EncodedFormat::Vp8,
|
||||
resolution: self.config.base.resolution,
|
||||
key_frame,
|
||||
sequence,
|
||||
timestamp: std::time::Instant::now(),
|
||||
pts: frame.pts as u64,
|
||||
dts: frame.pts as u64,
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<Vec<EncodedFrame>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> Result<()> {
|
||||
self.frame_count = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn config(&self) -> &EncoderConfig {
|
||||
&self.config.base
|
||||
}
|
||||
|
||||
fn supports_format(&self, format: PixelFormat) -> bool {
|
||||
match self.config.input_format {
|
||||
VP8InputFormat::Nv12 => matches!(format, PixelFormat::Nv12),
|
||||
VP8InputFormat::Yuv420p => matches!(format, PixelFormat::Yuv420),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_detect_vp8_encoder() {
|
||||
let (encoder_type, codec_name) = detect_best_vp8_encoder(1280, 720);
|
||||
println!(
|
||||
"Detected VP8 encoder: {:?} ({:?})",
|
||||
encoder_type, codec_name
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_available_vp8_encoders() {
|
||||
let encoders = get_available_vp8_encoders(1280, 720);
|
||||
println!("Available VP8 hardware encoders:");
|
||||
for enc in &encoders {
|
||||
println!(" - {} ({:?})", enc.name, enc.format);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vp8_availability() {
|
||||
let available = is_vp8_available();
|
||||
println!("VP8 hardware encoding available: {}", available);
|
||||
}
|
||||
}
|
||||
483
src/video/codec/vp9.rs
Normal file
483
src/video/codec/vp9.rs
Normal file
@@ -0,0 +1,483 @@
|
||||
//! VP9 encoder using hwcodec (FFmpeg wrapper)
|
||||
//!
|
||||
//! Supports both hardware and software encoding:
|
||||
//! - Hardware: VAAPI (Intel on Linux)
|
||||
//! - Software: libvpx-vp9 (CPU-based, high CPU usage)
|
||||
//!
|
||||
//! Hardware encoding is preferred when available for better performance.
|
||||
|
||||
use bytes::Bytes;
|
||||
use std::sync::Once;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
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;
|
||||
|
||||
use super::detect_best_codec_for_format;
|
||||
use super::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType};
|
||||
use super::traits::{EncodedFormat, EncodedFrame, Encoder, EncoderConfig};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
static INIT_LOGGING: Once = Once::new();
|
||||
|
||||
/// Initialize hwcodec logging (only once)
|
||||
fn init_hwcodec_logging() {
|
||||
INIT_LOGGING.call_once(|| {
|
||||
debug!("hwcodec logging initialized for VP9");
|
||||
});
|
||||
}
|
||||
|
||||
/// VP9 encoder type (detected from hwcodec)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum VP9EncoderType {
|
||||
/// VAAPI (Intel on Linux)
|
||||
Vaapi,
|
||||
/// Software encoder (libvpx-vp9)
|
||||
Software,
|
||||
/// No encoder available
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VP9EncoderType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
VP9EncoderType::Vaapi => write!(f, "VAAPI"),
|
||||
VP9EncoderType::Software => write!(f, "Software"),
|
||||
VP9EncoderType::None => write!(f, "None"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EncoderBackend> for VP9EncoderType {
|
||||
fn from(backend: EncoderBackend) -> Self {
|
||||
match backend {
|
||||
EncoderBackend::Vaapi => VP9EncoderType::Vaapi,
|
||||
EncoderBackend::Software => VP9EncoderType::Software,
|
||||
_ => VP9EncoderType::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Input pixel format for VP9 encoder
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum VP9InputFormat {
|
||||
/// YUV420P (I420) - planar Y, U, V
|
||||
Yuv420p,
|
||||
/// NV12 - Y plane + interleaved UV plane
|
||||
#[default]
|
||||
Nv12,
|
||||
}
|
||||
|
||||
/// VP9 encoder configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VP9Config {
|
||||
/// Base encoder config
|
||||
pub base: EncoderConfig,
|
||||
/// Target bitrate in kbps
|
||||
pub bitrate_kbps: u32,
|
||||
/// GOP size (keyframe interval)
|
||||
pub gop_size: u32,
|
||||
/// Frame rate
|
||||
pub fps: u32,
|
||||
/// Input pixel format
|
||||
pub input_format: VP9InputFormat,
|
||||
}
|
||||
|
||||
impl Default for VP9Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base: EncoderConfig::default(),
|
||||
bitrate_kbps: 8000,
|
||||
gop_size: 30,
|
||||
fps: 30,
|
||||
input_format: VP9InputFormat::Nv12,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VP9Config {
|
||||
/// Create config for low latency streaming with NV12 input
|
||||
pub fn low_latency(resolution: Resolution, bitrate_kbps: u32) -> Self {
|
||||
Self {
|
||||
base: EncoderConfig {
|
||||
resolution,
|
||||
input_format: PixelFormat::Nv12,
|
||||
quality: bitrate_kbps,
|
||||
fps: 30,
|
||||
gop_size: 30,
|
||||
},
|
||||
bitrate_kbps,
|
||||
gop_size: 30,
|
||||
fps: 30,
|
||||
input_format: VP9InputFormat::Nv12,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set input format
|
||||
pub fn with_input_format(mut self, format: VP9InputFormat) -> Self {
|
||||
self.input_format = format;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Get available VP9 hardware encoders from hwcodec
|
||||
pub fn get_available_vp9_encoders(width: u32, height: u32) -> Vec<CodecInfo> {
|
||||
init_hwcodec_logging();
|
||||
|
||||
let ctx = EncodeContext {
|
||||
name: String::new(),
|
||||
mc_name: None,
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
|
||||
align: 1,
|
||||
fps: 30,
|
||||
gop: 30,
|
||||
rc: RateControl::RC_CBR,
|
||||
quality: Quality::Quality_Default,
|
||||
kbs: 2000,
|
||||
q: 23,
|
||||
thread_count: 1,
|
||||
};
|
||||
|
||||
let all_encoders = HwEncoder::available_encoders(ctx, None);
|
||||
|
||||
// Include both hardware and software VP9 encoders
|
||||
all_encoders
|
||||
.into_iter()
|
||||
.filter(|e| e.format == DataFormat::VP9)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Detect best available VP9 encoder (hardware preferred, software fallback)
|
||||
pub fn detect_best_vp9_encoder(width: u32, height: u32) -> (VP9EncoderType, Option<String>) {
|
||||
let encoders = get_available_vp9_encoders(width, height);
|
||||
|
||||
// Prefer hardware encoders (VAAPI) over software (libvpx-vp9)
|
||||
if let Some((encoder_type, codec_name)) =
|
||||
detect_best_codec_for_format(&encoders, DataFormat::VP9, |codec| {
|
||||
codec.name.contains("vaapi")
|
||||
})
|
||||
{
|
||||
info!("Selected VP9 encoder: {} ({})", codec_name, encoder_type);
|
||||
(encoder_type, Some(codec_name))
|
||||
} else {
|
||||
warn!("No VP9 encoders available");
|
||||
(VP9EncoderType::None, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if VP9 hardware encoding is available
|
||||
pub fn is_vp9_available() -> bool {
|
||||
let registry = EncoderRegistry::global();
|
||||
registry.is_codec_available(VideoEncoderType::VP9)
|
||||
}
|
||||
|
||||
/// Encoded frame from hwcodec (cloned for ownership)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HwEncodeFrame {
|
||||
pub data: Vec<u8>,
|
||||
pub pts: i64,
|
||||
pub key: i32,
|
||||
}
|
||||
|
||||
/// VP9 encoder using hwcodec
|
||||
pub struct VP9Encoder {
|
||||
/// hwcodec encoder instance
|
||||
inner: HwEncoder,
|
||||
/// Encoder configuration
|
||||
config: VP9Config,
|
||||
/// Detected encoder type
|
||||
encoder_type: VP9EncoderType,
|
||||
/// Codec name
|
||||
codec_name: String,
|
||||
/// Frame counter
|
||||
frame_count: u64,
|
||||
/// Required buffer length from hwcodec
|
||||
buffer_length: i32,
|
||||
}
|
||||
|
||||
impl VP9Encoder {
|
||||
/// Create a new VP9 encoder with automatic hardware codec detection
|
||||
///
|
||||
/// Returns an error if no hardware encoder is available.
|
||||
/// VP9 hardware encoding requires Intel VAAPI support.
|
||||
pub fn new(config: VP9Config) -> Result<Self> {
|
||||
init_hwcodec_logging();
|
||||
|
||||
let width = config.base.resolution.width;
|
||||
let height = config.base.resolution.height;
|
||||
|
||||
let (encoder_type, codec_name) = detect_best_vp9_encoder(width, height);
|
||||
|
||||
if encoder_type == VP9EncoderType::None {
|
||||
return Err(AppError::VideoError(
|
||||
"No VP9 encoder available. Please ensure FFmpeg is built with libvpx support."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let codec_name = codec_name.unwrap();
|
||||
Self::with_codec(config, &codec_name)
|
||||
}
|
||||
|
||||
/// Create encoder with specific codec name
|
||||
pub fn with_codec(config: VP9Config, codec_name: &str) -> Result<Self> {
|
||||
init_hwcodec_logging();
|
||||
|
||||
// Determine if this is a software encoder
|
||||
let is_software = codec_name.contains("libvpx");
|
||||
|
||||
// Warn about software encoder performance
|
||||
if is_software {
|
||||
warn!(
|
||||
"Using software VP9 encoder (libvpx-vp9) - high CPU usage expected. \
|
||||
Hardware encoder is recommended for better performance."
|
||||
);
|
||||
}
|
||||
|
||||
let width = config.base.resolution.width;
|
||||
let height = config.base.resolution.height;
|
||||
|
||||
// Software encoders (libvpx-vp9) require YUV420P, hardware (VAAPI) uses NV12
|
||||
let (pixfmt_name, pixfmt_fallback, actual_input_format) = if is_software {
|
||||
(
|
||||
"yuv420p",
|
||||
AVPixelFormat::AV_PIX_FMT_YUV420P,
|
||||
VP9InputFormat::Yuv420p,
|
||||
)
|
||||
} else {
|
||||
match config.input_format {
|
||||
VP9InputFormat::Nv12 => {
|
||||
("nv12", AVPixelFormat::AV_PIX_FMT_NV12, VP9InputFormat::Nv12)
|
||||
}
|
||||
VP9InputFormat::Yuv420p => (
|
||||
"yuv420p",
|
||||
AVPixelFormat::AV_PIX_FMT_YUV420P,
|
||||
VP9InputFormat::Yuv420p,
|
||||
),
|
||||
}
|
||||
};
|
||||
let pixfmt = resolve_pixel_format(pixfmt_name, pixfmt_fallback);
|
||||
|
||||
info!(
|
||||
"Creating VP9 encoder: {} at {}x{} @ {} kbps (input: {:?})",
|
||||
codec_name, width, height, config.bitrate_kbps, actual_input_format
|
||||
);
|
||||
|
||||
let ctx = EncodeContext {
|
||||
name: codec_name.to_string(),
|
||||
mc_name: None,
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
pixfmt,
|
||||
align: 1,
|
||||
fps: config.fps as i32,
|
||||
gop: config.gop_size as i32,
|
||||
rc: RateControl::RC_CBR,
|
||||
quality: Quality::Quality_Default,
|
||||
kbs: config.bitrate_kbps as i32,
|
||||
q: 31,
|
||||
thread_count: 4, // VP9 benefits from multi-threading
|
||||
};
|
||||
|
||||
let inner = HwEncoder::new(ctx).map_err(|_| {
|
||||
AppError::VideoError(format!("Failed to create VP9 encoder: {}", codec_name))
|
||||
})?;
|
||||
|
||||
let buffer_length = inner.length;
|
||||
let backend = EncoderBackend::from_codec_name(codec_name);
|
||||
let encoder_type = VP9EncoderType::from(backend);
|
||||
|
||||
// Update config to reflect actual input format used
|
||||
let mut config = config;
|
||||
config.input_format = actual_input_format;
|
||||
|
||||
info!(
|
||||
"VP9 encoder created: {} (type: {}, buffer_length: {})",
|
||||
codec_name, encoder_type, buffer_length
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
config,
|
||||
encoder_type,
|
||||
codec_name: codec_name.to_string(),
|
||||
frame_count: 0,
|
||||
buffer_length,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create with auto-detected encoder
|
||||
pub fn auto(resolution: Resolution, bitrate_kbps: u32) -> Result<Self> {
|
||||
let config = VP9Config::low_latency(resolution, bitrate_kbps);
|
||||
Self::new(config)
|
||||
}
|
||||
|
||||
/// Get encoder type
|
||||
pub fn encoder_type(&self) -> &VP9EncoderType {
|
||||
&self.encoder_type
|
||||
}
|
||||
|
||||
/// Get codec name
|
||||
pub fn codec_name(&self) -> &str {
|
||||
&self.codec_name
|
||||
}
|
||||
|
||||
/// Update bitrate dynamically
|
||||
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
|
||||
self.inner
|
||||
.set_bitrate(bitrate_kbps as i32)
|
||||
.map_err(|_| AppError::VideoError("Failed to set VP9 bitrate".to_string()))?;
|
||||
self.config.bitrate_kbps = bitrate_kbps;
|
||||
debug!("VP9 bitrate updated to {} kbps", bitrate_kbps);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encode raw frame data
|
||||
pub fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
|
||||
if data.len() < self.buffer_length as usize {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"Frame data too small: {} < {}",
|
||||
data.len(),
|
||||
self.buffer_length
|
||||
)));
|
||||
}
|
||||
|
||||
self.frame_count += 1;
|
||||
|
||||
match self.inner.encode(data, pts_ms) {
|
||||
Ok(frames) => {
|
||||
// Zero-copy: drain frames from hwcodec buffer instead of cloning
|
||||
let owned_frames: Vec<HwEncodeFrame> = frames
|
||||
.drain(..)
|
||||
.map(|f| HwEncodeFrame {
|
||||
data: f.data, // Move, not clone
|
||||
pts: f.pts,
|
||||
key: f.key,
|
||||
})
|
||||
.collect();
|
||||
Ok(owned_frames)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("VP9 encode failed: {}", e);
|
||||
Err(AppError::VideoError(format!("VP9 encode failed: {}", e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode NV12 data
|
||||
pub fn encode_nv12(&mut self, nv12_data: &[u8], pts_ms: i64) -> Result<Vec<HwEncodeFrame>> {
|
||||
self.encode_raw(nv12_data, pts_ms)
|
||||
}
|
||||
|
||||
/// Get input format
|
||||
pub fn input_format(&self) -> VP9InputFormat {
|
||||
self.config.input_format
|
||||
}
|
||||
|
||||
/// Get buffer info
|
||||
pub fn buffer_info(&self) -> (Vec<i32>, Vec<i32>, i32) {
|
||||
(
|
||||
self.inner.linesize.clone(),
|
||||
self.inner.offset.clone(),
|
||||
self.inner.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: VP9Encoder contains hwcodec::ffmpeg_ram::encode::Encoder which has raw pointers
|
||||
// that are not Send by default. However, we ensure that VP9Encoder is only used from
|
||||
// a single task/thread at a time (encoding is sequential), so this is safe.
|
||||
unsafe impl Send for VP9Encoder {}
|
||||
|
||||
impl Encoder for VP9Encoder {
|
||||
fn name(&self) -> &str {
|
||||
&self.codec_name
|
||||
}
|
||||
|
||||
fn output_format(&self) -> EncodedFormat {
|
||||
EncodedFormat::Vp9
|
||||
}
|
||||
|
||||
fn encode(&mut self, data: &[u8], sequence: u64) -> Result<EncodedFrame> {
|
||||
let pts_ms = (sequence * 1000 / self.config.fps as u64) as i64;
|
||||
|
||||
let mut frames = self.encode_raw(data, pts_ms)?;
|
||||
|
||||
if frames.is_empty() {
|
||||
warn!("VP9 encoder returned no frames");
|
||||
return Err(AppError::VideoError(
|
||||
"VP9 encoder returned no frames".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Take ownership of the first frame (zero-copy)
|
||||
let frame = frames.remove(0);
|
||||
let key_frame = frame.key == 1;
|
||||
|
||||
Ok(EncodedFrame {
|
||||
data: Bytes::from(frame.data), // Move Vec into Bytes (zero-copy)
|
||||
format: EncodedFormat::Vp9,
|
||||
resolution: self.config.base.resolution,
|
||||
key_frame,
|
||||
sequence,
|
||||
timestamp: std::time::Instant::now(),
|
||||
pts: frame.pts as u64,
|
||||
dts: frame.pts as u64,
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<Vec<EncodedFrame>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> Result<()> {
|
||||
self.frame_count = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn config(&self) -> &EncoderConfig {
|
||||
&self.config.base
|
||||
}
|
||||
|
||||
fn supports_format(&self, format: PixelFormat) -> bool {
|
||||
match self.config.input_format {
|
||||
VP9InputFormat::Nv12 => matches!(format, PixelFormat::Nv12),
|
||||
VP9InputFormat::Yuv420p => matches!(format, PixelFormat::Yuv420),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_detect_vp9_encoder() {
|
||||
let (encoder_type, codec_name) = detect_best_vp9_encoder(1280, 720);
|
||||
println!(
|
||||
"Detected VP9 encoder: {:?} ({:?})",
|
||||
encoder_type, codec_name
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_available_vp9_encoders() {
|
||||
let encoders = get_available_vp9_encoders(1280, 720);
|
||||
println!("Available VP9 hardware encoders:");
|
||||
for enc in &encoders {
|
||||
println!(" - {} ({:?})", enc.name, enc.format);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vp9_availability() {
|
||||
let available = is_vp9_available();
|
||||
println!("VP9 hardware encoding available: {}", available);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user