mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 11:42:02 +08:00
init
This commit is contained in:
370
src/video/encoder/codec.rs
Normal file
370
src/video/encoder/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=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
532
src/video/encoder/h264.rs
Normal 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
577
src/video/encoder/h265.rs
Normal 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
226
src/video/encoder/jpeg.rs
Normal 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
43
src/video/encoder/mod.rs
Normal 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;
|
||||
531
src/video/encoder/registry.rs
Normal file
531
src/video/encoder/registry.rs
Normal 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
188
src/video/encoder/traits.rs
Normal 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
488
src/video/encoder/vp8.rs
Normal 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
488
src/video/encoder/vp9.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user