This commit is contained in:
mofeng-git
2025-12-28 18:19:16 +08:00
commit d143d158e4
771 changed files with 220548 additions and 0 deletions

370
src/video/encoder/codec.rs Normal file
View 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=42e01f"
/// 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, 2000);
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);
}
}

532
src/video/encoder/h264.rs Normal file
View File

@@ -0,0 +1,532 @@
//! 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::AVPixelFormat;
use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder};
use hwcodec::ffmpeg_ram::CodecInfo;
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)]
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
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"),
}
}
}
impl Default for H264EncoderType {
fn default() -> Self {
Self::None
}
}
/// Map codec name to encoder type
fn codec_name_to_type(name: &str) -> H264EncoderType {
if name.contains("nvenc") {
H264EncoderType::Nvenc
} else if name.contains("qsv") {
H264EncoderType::Qsv
} else if name.contains("amf") {
H264EncoderType::Amf
} else if name.contains("vaapi") {
H264EncoderType::Vaapi
} else if name.contains("rkmpp") {
H264EncoderType::Rkmpp
} else if name.contains("v4l2m2m") {
H264EncoderType::V4l2M2m
} else {
H264EncoderType::Software
}
}
/// Input pixel format for H264 encoder
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum H264InputFormat {
/// YUV420P (I420) - planar Y, U, V
Yuv420p,
/// NV12 - Y plane + interleaved UV plane (optimal for VAAPI)
Nv12,
}
impl Default for H264InputFormat {
fn default() -> Self {
Self::Nv12 // Default to NV12 for VAAPI compatibility
}
}
/// 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: 8000,
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 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: 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 encoders.is_empty() {
warn!("No H.264 encoders available from hwcodec");
return (H264EncoderType::None, None);
}
// Find H264 encoder (not H265)
for codec in &encoders {
if codec.format == hwcodec::common::DataFormat::H264 {
let encoder_type = codec_name_to_type(&codec.name);
info!("Best H.264 encoder: {} ({})", codec.name, encoder_type);
return (encoder_type, Some(codec.name.clone()));
}
}
(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,
/// YUV420P buffer for input (reserved for future use)
#[allow(dead_code)]
yuv_buffer: Vec<u8>,
/// 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 = match config.input_format {
H264InputFormat::Nv12 => AVPixelFormat::AV_PIX_FMT_NV12,
H264InputFormat::Yuv420p => AVPixelFormat::AV_PIX_FMT_YUV420P,
};
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 = codec_name_to_type(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_buffer: vec![0u8; yuv_length as usize],
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) => {
// Copy frame data to owned HwEncodeFrame
let owned_frames: Vec<HwEncodeFrame> = frames
.iter()
.map(|f| HwEncodeFrame {
data: f.data.clone(),
pts: f.pts,
key: f.key,
})
.collect();
Ok(owned_frames)
}
Err(e) => {
// For the first ~30 frames, x264 may fail due to initialization
// Log as warning instead of error to avoid alarming users
if self.frame_count <= 30 {
warn!(
"Encode failed during initialization (frame {}): {} - this is normal for x264",
self.frame_count, e
);
} else {
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 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 the first frame
let frame = &frames[0];
let key_frame = frame.key == 1;
Ok(EncodedFrame::h264(
Bytes::from(frame.data.clone()),
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::Yuv420p => matches!(format, PixelFormat::Yuv420),
}
}
}
/// Encoder statistics
#[derive(Debug, Clone, Default)]
pub struct EncoderStats {
/// Total frames encoded
pub frames_encoded: u64,
/// Total bytes output
pub bytes_output: u64,
/// Current encoding FPS
pub fps: f32,
/// Average encoding time per frame (ms)
pub avg_encode_time_ms: f32,
/// Keyframes encoded
pub keyframes: u64,
}
#[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);
}
}
}
}

577
src/video/encoder/h265.rs Normal file
View File

@@ -0,0 +1,577 @@
//! 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::AVPixelFormat;
use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder};
use hwcodec::ffmpeg_ram::CodecInfo;
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)]
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
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 Default for H265EncoderType {
fn default() -> Self {
Self::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)]
pub enum H265InputFormat {
/// YUV420P (I420) - planar Y, U, V
Yuv420p,
/// NV12 - Y plane + interleaved UV plane (optimal for hardware encoders)
Nv12,
}
impl Default for H265InputFormat {
fn default() -> Self {
Self::Nv12 // Default to NV12 for hardware encoder compatibility
}
}
/// 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 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: 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);
if encoders.is_empty() {
warn!("No H.265 encoders available");
return (H265EncoderType::None, None);
}
// Prefer hardware encoders over software (libx265)
// Hardware priority: NVENC > QSV > AMF > VAAPI > RKMPP > V4L2 M2M > Software
let codec = encoders
.iter()
.find(|e| !e.name.contains("libx265"))
.or_else(|| encoders.first())
.unwrap();
let encoder_type = if codec.name.contains("nvenc") {
H265EncoderType::Nvenc
} else if codec.name.contains("qsv") {
H265EncoderType::Qsv
} else if codec.name.contains("amf") {
H265EncoderType::Amf
} else if codec.name.contains("vaapi") {
H265EncoderType::Vaapi
} else if codec.name.contains("rkmpp") {
H265EncoderType::Rkmpp
} else if codec.name.contains("v4l2m2m") {
H265EncoderType::V4l2M2m
} else if codec.name.contains("libx265") {
H265EncoderType::Software
} else {
H265EncoderType::Software // Default to software for unknown
};
info!(
"Selected H.265 encoder: {} ({})",
codec.name, encoder_type
);
(encoder_type, Some(codec.name.clone()))
}
/// Check if H265 hardware encoding is available
pub fn is_h265_available() -> bool {
let registry = EncoderRegistry::global();
registry.is_format_available(VideoEncoderType::H265, true)
}
/// 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 (hardware only)
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
let (pixfmt, actual_input_format) = if is_software {
(AVPixelFormat::AV_PIX_FMT_YUV420P, H265InputFormat::Yuv420p)
} else {
match config.input_format {
H265InputFormat::Nv12 => (AVPixelFormat::AV_PIX_FMT_NV12, H265InputFormat::Nv12),
H265InputFormat::Yuv420p => (AVPixelFormat::AV_PIX_FMT_YUV420P, H265InputFormat::Yuv420p),
}
};
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) => {
let owned_frames: Vec<HwEncodeFrame> = frames
.iter()
.map(|f| HwEncodeFrame {
data: f.data.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 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(),
));
}
let frame = &frames[0];
let key_frame = frame.key == 1;
Ok(EncodedFrame {
data: Bytes::from(frame.data.clone()),
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::Yuv420p => matches!(format, PixelFormat::Yuv420),
}
}
}
#[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);
}
}

226
src/video/encoder/jpeg.rs Normal file
View File

@@ -0,0 +1,226 @@
//! JPEG encoder implementation
//!
//! Provides JPEG encoding for raw video frames (YUYV, NV12, 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/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>,
}
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 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],
})
}
/// 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 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)
}
/// 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 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::encoder::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 | PixelFormat::Yvyu => self.encode_yuyv(data, sequence),
PixelFormat::Nv12 => self.encode_nv12(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::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);
}
}

43
src/video/encoder/mod.rs Normal file
View File

@@ -0,0 +1,43 @@
//! Video encoder implementations
//!
//! This module provides video encoding capabilities including:
//! - JPEG encoding for raw frames (YUYV, NV12, etc.)
//! - H264 encoding (hardware + software)
//! - H265 encoding (hardware only)
//! - VP8 encoding (hardware only - VAAPI)
//! - VP9 encoding (hardware only - VAAPI)
//! - WebRTC video codec abstraction
//! - Encoder registry for automatic detection
pub mod codec;
pub mod h264;
pub mod h265;
pub mod jpeg;
pub mod registry;
pub mod traits;
pub mod vp8;
pub mod vp9;
// Core traits and types
pub use traits::{EncodedFormat, EncodedFrame, Encoder, EncoderConfig, EncoderFactory};
// WebRTC codec abstraction
pub use codec::{CodecFrame, VideoCodec, VideoCodecConfig, VideoCodecFactory, VideoCodecType};
// Encoder registry
pub use registry::{AvailableEncoder, EncoderBackend, EncoderRegistry, VideoEncoderType};
// H264 encoder
pub use h264::{H264Config, H264Encoder, H264EncoderType, H264InputFormat};
// H265 encoder (hardware only)
pub use h265::{H265Config, H265Encoder, H265EncoderType, H265InputFormat};
// VP8 encoder (hardware only)
pub use vp8::{VP8Config, VP8Encoder, VP8EncoderType, VP8InputFormat};
// VP9 encoder (hardware only)
pub use vp9::{VP9Config, VP9Encoder, VP9EncoderType, VP9InputFormat};
// JPEG encoder
pub use jpeg::JpegEncoder;

View File

@@ -0,0 +1,531 @@
//! 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 tracing::{debug, info};
use hwcodec::common::{DataFormat, Quality, RateControl};
use hwcodec::ffmpeg::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 {
/// 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",
}
}
/// Check if this format requires hardware-only encoding
/// H264 supports software fallback, others require hardware
pub fn hardware_only(&self) -> bool {
match self {
VideoEncoderType::H264 => false,
VideoEncoderType::H265 => true,
VideoEncoderType::VP8 => true,
VideoEncoderType::VP9 => true,
}
}
}
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)
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 {
/// Get the global registry instance
///
/// The registry is initialized lazily on first access with 1920x1080 detection.
pub fn global() -> &'static Self {
static INSTANCE: OnceLock<EncoderRegistry> = OnceLock::new();
INSTANCE.get_or_init(|| {
let mut registry = EncoderRegistry::new();
registry.detect_encoders(1920, 1080);
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: 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,
};
// Get all available encoders from hwcodec
let all_encoders = HwEncoder::available_encoders(ctx, None);
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);
}
// Register software encoders as fallback
info!("Registering software encoders...");
let software_encoders = [
(VideoEncoderType::H264, "libx264", 100),
(VideoEncoderType::H265, "libx265", 100),
(VideoEncoderType::VP8, "libvpx", 100),
(VideoEncoderType::VP9, "libvpx-vp9", 100),
];
for (format, codec_name, priority) in software_encoders {
self.encoders
.entry(format)
.or_default()
.push(AvailableEncoder {
format,
codec_name: codec_name.to_string(),
backend: EncoderBackend::Software,
priority,
is_hardware: false,
});
debug!(
"Registered software encoder: {} for {} (priority: {})",
codec_name, format, priority
);
}
// 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
}
})
}
/// 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()
}
/// Get available formats for user selection
///
/// Returns formats that are actually usable based on their requirements:
/// - H264: Available if any encoder exists (hardware or software)
/// - H265/VP8/VP9: Available only if hardware encoder exists
pub fn selectable_formats(&self) -> Vec<VideoEncoderType> {
let mut formats = Vec::new();
// H264 - supports software fallback
if self.is_format_available(VideoEncoderType::H264, false) {
formats.push(VideoEncoderType::H264);
}
// H265/VP8/VP9 - hardware only
for format in [
VideoEncoderType::H265,
VideoEncoderType::VP8,
VideoEncoderType::VP9,
] {
if self.is_format_available(format, true) {
formats.push(format);
}
}
formats
}
/// 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_hardware_only_requirement() {
assert!(!VideoEncoderType::H264.hardware_only());
assert!(VideoEncoderType::H265.hardware_only());
assert!(VideoEncoderType::VP8.hardware_only());
assert!(VideoEncoderType::VP9.hardware_only());
}
#[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()
);
}
}

188
src/video/encoder/traits.rs Normal file
View File

@@ -0,0 +1,188 @@
//! Encoder traits and common types
use bytes::Bytes;
use std::time::Instant;
use crate::video::format::{PixelFormat, Resolution};
use crate::error::Result;
/// 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;
}

488
src/video/encoder/vp8.rs Normal file
View File

@@ -0,0 +1,488 @@
//! 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::AVPixelFormat;
use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder};
use hwcodec::ffmpeg_ram::CodecInfo;
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)]
pub enum VP8EncoderType {
/// VAAPI (Intel on Linux)
Vaapi,
/// Software encoder (libvpx)
Software,
/// No encoder available
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 Default for VP8EncoderType {
fn default() -> Self {
Self::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)]
pub enum VP8InputFormat {
/// YUV420P (I420) - planar Y, U, V
Yuv420p,
/// NV12 - Y plane + interleaved UV plane
Nv12,
}
impl Default for VP8InputFormat {
fn default() -> Self {
Self::Nv12 // Default to NV12 for VAAPI compatibility
}
}
/// 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: 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);
if encoders.is_empty() {
warn!("No VP8 encoders available");
return (VP8EncoderType::None, None);
}
// Prefer hardware encoders (VAAPI) over software (libvpx)
let codec = encoders
.iter()
.find(|e| e.name.contains("vaapi"))
.or_else(|| encoders.first())
.unwrap();
let encoder_type = if codec.name.contains("vaapi") {
VP8EncoderType::Vaapi
} else if codec.name.contains("libvpx") {
VP8EncoderType::Software
} else {
VP8EncoderType::Software // Default to software for unknown
};
info!(
"Selected VP8 encoder: {} ({})",
codec.name, encoder_type
);
(encoder_type, Some(codec.name.clone()))
}
/// Check if VP8 hardware encoding is available
pub fn is_vp8_available() -> bool {
let registry = EncoderRegistry::global();
registry.is_format_available(VideoEncoderType::VP8, true)
}
/// 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 (hardware only - VAAPI)
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, actual_input_format) = if is_software {
(AVPixelFormat::AV_PIX_FMT_YUV420P, VP8InputFormat::Yuv420p)
} else {
match config.input_format {
VP8InputFormat::Nv12 => (AVPixelFormat::AV_PIX_FMT_NV12, VP8InputFormat::Nv12),
VP8InputFormat::Yuv420p => (AVPixelFormat::AV_PIX_FMT_YUV420P, VP8InputFormat::Yuv420p),
}
};
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) => {
let owned_frames: Vec<HwEncodeFrame> = frames
.iter()
.map(|f| HwEncodeFrame {
data: f.data.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 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(),
));
}
let frame = &frames[0];
let key_frame = frame.key == 1;
Ok(EncodedFrame {
data: Bytes::from(frame.data.clone()),
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);
}
}

488
src/video/encoder/vp9.rs Normal file
View File

@@ -0,0 +1,488 @@
//! 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::AVPixelFormat;
use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder};
use hwcodec::ffmpeg_ram::CodecInfo;
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)]
pub enum VP9EncoderType {
/// VAAPI (Intel on Linux)
Vaapi,
/// Software encoder (libvpx-vp9)
Software,
/// No encoder available
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 Default for VP9EncoderType {
fn default() -> Self {
Self::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)]
pub enum VP9InputFormat {
/// YUV420P (I420) - planar Y, U, V
Yuv420p,
/// NV12 - Y plane + interleaved UV plane
Nv12,
}
impl Default for VP9InputFormat {
fn default() -> Self {
Self::Nv12 // Default to NV12 for VAAPI compatibility
}
}
/// 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: 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);
if encoders.is_empty() {
warn!("No VP9 encoders available");
return (VP9EncoderType::None, None);
}
// Prefer hardware encoders (VAAPI) over software (libvpx-vp9)
let codec = encoders
.iter()
.find(|e| e.name.contains("vaapi"))
.or_else(|| encoders.first())
.unwrap();
let encoder_type = if codec.name.contains("vaapi") {
VP9EncoderType::Vaapi
} else if codec.name.contains("libvpx") {
VP9EncoderType::Software
} else {
VP9EncoderType::Software // Default to software for unknown
};
info!(
"Selected VP9 encoder: {} ({})",
codec.name, encoder_type
);
(encoder_type, Some(codec.name.clone()))
}
/// Check if VP9 hardware encoding is available
pub fn is_vp9_available() -> bool {
let registry = EncoderRegistry::global();
registry.is_format_available(VideoEncoderType::VP9, true)
}
/// 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 (hardware only - VAAPI)
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, actual_input_format) = if is_software {
(AVPixelFormat::AV_PIX_FMT_YUV420P, VP9InputFormat::Yuv420p)
} else {
match config.input_format {
VP9InputFormat::Nv12 => (AVPixelFormat::AV_PIX_FMT_NV12, VP9InputFormat::Nv12),
VP9InputFormat::Yuv420p => (AVPixelFormat::AV_PIX_FMT_YUV420P, VP9InputFormat::Yuv420p),
}
};
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) => {
let owned_frames: Vec<HwEncodeFrame> = frames
.iter()
.map(|f| HwEncodeFrame {
data: f.data.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 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(),
));
}
let frame = &frames[0];
let key_frame = frame.key == 1;
Ok(EncodedFrame {
data: Bytes::from(frame.data.clone()),
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);
}
}