mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 11:04:25 +08:00
feat: 新增安卓平台支持
This commit is contained in:
122
src/video/codec/android_mediacodec.rs
Normal file
122
src/video/codec/android_mediacodec.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
//! Android FFmpeg/MediaCodec encoder glue.
|
||||
|
||||
use bytes::Bytes;
|
||||
use hwcodec::common::{Quality, RateControl};
|
||||
use hwcodec::ffmpeg::{resolve_pixel_format, AVPixelFormat};
|
||||
use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
pub struct AndroidMediaCodecH264Encoder {
|
||||
inner: HwEncoder,
|
||||
resolution: Resolution,
|
||||
input_format: PixelFormat,
|
||||
bitrate_kbps: u32,
|
||||
}
|
||||
|
||||
impl AndroidMediaCodecH264Encoder {
|
||||
pub fn new(
|
||||
resolution: Resolution,
|
||||
input_format: PixelFormat,
|
||||
fps: u32,
|
||||
bitrate_kbps: u32,
|
||||
) -> Result<Self> {
|
||||
let pixfmt = match input_format {
|
||||
PixelFormat::Nv12 => resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
|
||||
PixelFormat::Yuv420 => {
|
||||
resolve_pixel_format("yuv420p", AVPixelFormat::AV_PIX_FMT_YUV420P)
|
||||
}
|
||||
other => {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"FFmpeg h264_mediacodec accepts NV12/YUV420P memory frames; {other} requires conversion first"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let ctx = EncodeContext {
|
||||
name: "h264_mediacodec".to_string(),
|
||||
mc_name: None,
|
||||
width: resolution.width as i32,
|
||||
height: resolution.height as i32,
|
||||
pixfmt,
|
||||
align: 1,
|
||||
fps: fps.max(1) as i32,
|
||||
gop: fps.max(1) as i32,
|
||||
rc: RateControl::RC_CBR,
|
||||
quality: Quality::Quality_Low,
|
||||
kbs: bitrate_kbps.max(1) as i32,
|
||||
q: 23,
|
||||
thread_count: 1,
|
||||
};
|
||||
|
||||
let inner = HwEncoder::new(ctx).map_err(|_| {
|
||||
AppError::VideoError("Failed to create FFmpeg h264_mediacodec encoder".to_string())
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
resolution,
|
||||
input_format,
|
||||
bitrate_kbps: bitrate_kbps.max(1),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result<Vec<AndroidH264Packet>> {
|
||||
let min_len = self
|
||||
.input_format
|
||||
.frame_size(self.resolution)
|
||||
.ok_or_else(|| AppError::VideoError("MediaCodec input must be raw YUV".to_string()))?;
|
||||
if data.len() < min_len {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"MediaCodec {} frame too small: {} < {}",
|
||||
self.input_format,
|
||||
data.len(),
|
||||
min_len
|
||||
)));
|
||||
}
|
||||
|
||||
let packets = self
|
||||
.inner
|
||||
.encode_bytes(data, pts_ms)
|
||||
.map_err(|err| AppError::VideoError(format!("h264_mediacodec encode failed: {err}")))?;
|
||||
|
||||
Ok(packets
|
||||
.into_iter()
|
||||
.map(|packet| AndroidH264Packet {
|
||||
data: packet.data,
|
||||
pts: packet.pts,
|
||||
key_frame: packet.key == 1,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
|
||||
self.inner
|
||||
.set_bitrate(bitrate_kbps.max(1) as i32)
|
||||
.map_err(|_| AppError::VideoError("Failed to set MediaCodec bitrate".to_string()))?;
|
||||
self.bitrate_kbps = bitrate_kbps.max(1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn request_keyframe(&mut self) {
|
||||
self.inner.request_keyframe();
|
||||
}
|
||||
|
||||
pub fn codec_name(&self) -> &str {
|
||||
"h264_mediacodec"
|
||||
}
|
||||
|
||||
pub fn input_format(&self) -> PixelFormat {
|
||||
self.input_format
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for AndroidMediaCodecH264Encoder {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AndroidH264Packet {
|
||||
pub data: Bytes,
|
||||
pub pts: i64,
|
||||
pub key_frame: bool,
|
||||
}
|
||||
137
src/video/codec/android_mjpeg.rs
Normal file
137
src/video/codec/android_mjpeg.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
//! Android FFmpeg/MediaCodec MJPEG decoder glue.
|
||||
|
||||
use hwcodec::ffmpeg::AVPixelFormat;
|
||||
use hwcodec::ffmpeg_ram::decode::{DecodeContext, Decoder};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::codec::convert::Nv12Converter;
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
pub struct AndroidMediaCodecMjpegDecoder {
|
||||
decoder: Decoder,
|
||||
resolution: Resolution,
|
||||
nv12_converter: Option<Nv12Converter>,
|
||||
last_output_format: Option<PixelFormat>,
|
||||
pending_frames: u32,
|
||||
}
|
||||
|
||||
impl AndroidMediaCodecMjpegDecoder {
|
||||
pub fn new(resolution: Resolution) -> Result<Self> {
|
||||
let ctx = DecodeContext {
|
||||
name: "mjpeg_mediacodec".to_string(),
|
||||
width: resolution.width as i32,
|
||||
height: resolution.height as i32,
|
||||
sw_pixfmt: AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
thread_count: 1,
|
||||
};
|
||||
let decoder = Decoder::new(ctx).map_err(|_| {
|
||||
AppError::VideoError("Failed to create FFmpeg mjpeg_mediacodec decoder".to_string())
|
||||
})?;
|
||||
Ok(Self {
|
||||
decoder,
|
||||
resolution,
|
||||
nv12_converter: None,
|
||||
last_output_format: None,
|
||||
pending_frames: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decode_to_nv12(&mut self, mjpeg: &[u8]) -> Result<Vec<u8>> {
|
||||
let frames = match self.decoder.decode(mjpeg) {
|
||||
Ok(frames) => frames,
|
||||
Err(err) if err == -11 => {
|
||||
self.pending_frames += 1;
|
||||
if self.pending_frames <= 3 {
|
||||
return Err(AppError::VideoError(
|
||||
"mjpeg_mediacodec decode needs more input".to_string(),
|
||||
));
|
||||
}
|
||||
return Err(AppError::VideoError(
|
||||
"mjpeg_mediacodec decoder did not output after 3 frames".to_string(),
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"mjpeg_mediacodec decode failed: {err}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
if frames.is_empty() {
|
||||
self.pending_frames += 1;
|
||||
if self.pending_frames <= 3 {
|
||||
return Err(AppError::VideoError(
|
||||
"mjpeg_mediacodec decode needs more input".to_string(),
|
||||
));
|
||||
}
|
||||
return Err(AppError::VideoError(
|
||||
"mjpeg_mediacodec decoder did not output after 3 frames".to_string(),
|
||||
));
|
||||
}
|
||||
self.pending_frames = 0;
|
||||
if frames.len() > 1 {
|
||||
warn!(
|
||||
"mjpeg_mediacodec decode returned {} frames, using last",
|
||||
frames.len()
|
||||
);
|
||||
}
|
||||
|
||||
let frame = frames.pop().ok_or_else(|| {
|
||||
AppError::VideoError("mjpeg_mediacodec decode returned empty".to_string())
|
||||
})?;
|
||||
|
||||
if frame.width as u32 != self.resolution.width
|
||||
|| frame.height as u32 != self.resolution.height
|
||||
{
|
||||
warn!(
|
||||
"mjpeg_mediacodec output size {}x{} differs from expected {}x{}",
|
||||
frame.width, frame.height, self.resolution.width, self.resolution.height
|
||||
);
|
||||
}
|
||||
|
||||
let output_format = pixel_format_from_av(frame.pixfmt).ok_or_else(|| {
|
||||
AppError::VideoError(format!(
|
||||
"mjpeg_mediacodec output pixfmt {:?} is not supported",
|
||||
frame.pixfmt
|
||||
))
|
||||
})?;
|
||||
|
||||
if self.last_output_format != Some(output_format) {
|
||||
info!("mjpeg_mediacodec output format: {}", output_format);
|
||||
self.last_output_format = Some(output_format);
|
||||
}
|
||||
|
||||
match output_format {
|
||||
PixelFormat::Nv12 => Ok(frame.data),
|
||||
PixelFormat::Nv21 => {
|
||||
let converter = self
|
||||
.nv12_converter
|
||||
.get_or_insert_with(|| Nv12Converter::nv21_to_nv12(self.resolution));
|
||||
Ok(converter.convert(&frame.data)?.to_vec())
|
||||
}
|
||||
PixelFormat::Yuv420 => {
|
||||
let converter = self
|
||||
.nv12_converter
|
||||
.get_or_insert_with(|| Nv12Converter::yuv420_to_nv12(self.resolution));
|
||||
Ok(converter.convert(&frame.data)?.to_vec())
|
||||
}
|
||||
other => Err(AppError::VideoError(format!(
|
||||
"mjpeg_mediacodec output {} cannot be converted to NV12",
|
||||
other
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pixel_format_from_av(format: AVPixelFormat) -> Option<PixelFormat> {
|
||||
match format {
|
||||
AVPixelFormat::AV_PIX_FMT_NV12 => Some(PixelFormat::Nv12),
|
||||
AVPixelFormat::AV_PIX_FMT_NV21 => Some(PixelFormat::Nv21),
|
||||
AVPixelFormat::AV_PIX_FMT_YUV420P | AVPixelFormat::AV_PIX_FMT_YUVJ420P => {
|
||||
Some(PixelFormat::Yuv420)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for AndroidMediaCodecMjpegDecoder {}
|
||||
@@ -539,8 +539,46 @@ pub struct Nv12Converter {
|
||||
resolution: Resolution,
|
||||
/// Output buffer (reused across conversions)
|
||||
output_buffer: Nv12Buffer,
|
||||
/// Optional I420 buffer for intermediate conversions
|
||||
i420_buffer: Option<Yuv420pBuffer>,
|
||||
}
|
||||
|
||||
/// MJPEG decoder that writes NV12 directly using libyuv.
|
||||
pub struct MjpegToNv12Decoder {
|
||||
resolution: Resolution,
|
||||
output_buffer: Nv12Buffer,
|
||||
size_checked: bool,
|
||||
}
|
||||
|
||||
impl MjpegToNv12Decoder {
|
||||
pub fn new(resolution: Resolution) -> Self {
|
||||
Self {
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
size_checked: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode(&mut self, input: &[u8]) -> Result<&[u8]> {
|
||||
let width = self.resolution.width as i32;
|
||||
let height = self.resolution.height as i32;
|
||||
|
||||
if !self.size_checked {
|
||||
let (src_width, src_height) = libyuv::mjpg_size(input).map_err(|e| {
|
||||
AppError::VideoError(format!("libyuv MJPEG header read failed: {}", e))
|
||||
})?;
|
||||
if src_width != width || src_height != height {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"libyuv MJPEG size mismatch: {}x{} (expected {}x{})",
|
||||
src_width, src_height, width, height
|
||||
)));
|
||||
}
|
||||
self.size_checked = true;
|
||||
}
|
||||
|
||||
libyuv::mjpg_to_nv12(input, self.output_buffer.as_bytes_mut(), width, height)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv MJPEG->NV12 failed: {}", e)))?;
|
||||
|
||||
Ok(self.output_buffer.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl Nv12Converter {
|
||||
@@ -550,7 +588,6 @@ impl Nv12Converter {
|
||||
src_format: PixelFormat::Bgr24,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,7 +597,6 @@ impl Nv12Converter {
|
||||
src_format: PixelFormat::Rgb24,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,7 +606,6 @@ impl Nv12Converter {
|
||||
src_format: PixelFormat::Yuyv,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,7 +615,6 @@ impl Nv12Converter {
|
||||
src_format: PixelFormat::Yuv420,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,7 +624,6 @@ impl Nv12Converter {
|
||||
src_format: PixelFormat::Nv21,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: Some(Yuv420pBuffer::new(resolution)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,7 +633,6 @@ impl Nv12Converter {
|
||||
src_format: PixelFormat::Nv16,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,7 +642,6 @@ impl Nv12Converter {
|
||||
src_format: PixelFormat::Nv24,
|
||||
resolution,
|
||||
output_buffer: Nv12Buffer::new(resolution),
|
||||
i420_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,23 +652,6 @@ impl Nv12Converter {
|
||||
|
||||
// Handle formats that need custom conversion without holding dst borrow
|
||||
match self.src_format {
|
||||
PixelFormat::Nv21 => {
|
||||
let mut i420 = self.i420_buffer.take().ok_or_else(|| {
|
||||
AppError::VideoError("NV21 I420 buffer not initialized".to_string())
|
||||
})?;
|
||||
{
|
||||
let dst = self.output_buffer.as_bytes_mut();
|
||||
Self::convert_nv21_to_nv12_with_dims(
|
||||
self.resolution.width as usize,
|
||||
self.resolution.height as usize,
|
||||
input,
|
||||
dst,
|
||||
&mut i420,
|
||||
)?;
|
||||
}
|
||||
self.i420_buffer = Some(i420);
|
||||
return Ok(self.output_buffer.as_bytes());
|
||||
}
|
||||
PixelFormat::Nv16 => {
|
||||
let dst = self.output_buffer.as_bytes_mut();
|
||||
Self::convert_nv16_to_nv12_with_dims(
|
||||
@@ -667,6 +681,7 @@ impl Nv12Converter {
|
||||
PixelFormat::Rgb24 => libyuv::rgb24_to_nv12(input, dst, width, height),
|
||||
PixelFormat::Yuyv => libyuv::yuy2_to_nv12(input, dst, width, height),
|
||||
PixelFormat::Yuv420 => libyuv::i420_to_nv12(input, dst, width, height),
|
||||
PixelFormat::Nv21 => libyuv::nv21_to_nv12(input, dst, width, height),
|
||||
_ => {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"Unsupported conversion to NV12: {}",
|
||||
@@ -680,21 +695,6 @@ impl Nv12Converter {
|
||||
Ok(self.output_buffer.as_bytes())
|
||||
}
|
||||
|
||||
fn convert_nv21_to_nv12_with_dims(
|
||||
width: usize,
|
||||
height: usize,
|
||||
input: &[u8],
|
||||
dst: &mut [u8],
|
||||
yuv: &mut Yuv420pBuffer,
|
||||
) -> Result<()> {
|
||||
libyuv::nv21_to_i420(input, yuv.as_bytes_mut(), width as i32, height as i32)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv NV21->I420 failed: {}", e)))?;
|
||||
libyuv::i420_to_nv12(yuv.as_bytes(), dst, width as i32, height as i32)
|
||||
.map_err(|e| AppError::VideoError(format!("libyuv I420->NV12 failed: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_nv16_to_nv12_with_dims(
|
||||
width: usize,
|
||||
height: usize,
|
||||
|
||||
@@ -48,6 +48,8 @@ pub enum H264EncoderType {
|
||||
Rkmpp,
|
||||
/// V4L2 M2M (ARM generic) - requires hwcodec extension
|
||||
V4l2M2m,
|
||||
/// Android MediaCodec via FFmpeg
|
||||
MediaCodec,
|
||||
/// Software encoding (libx264/openh264)
|
||||
Software,
|
||||
/// No encoder available
|
||||
@@ -64,6 +66,7 @@ impl std::fmt::Display for H264EncoderType {
|
||||
H264EncoderType::Vaapi => write!(f, "VAAPI"),
|
||||
H264EncoderType::Rkmpp => write!(f, "RKMPP"),
|
||||
H264EncoderType::V4l2M2m => write!(f, "V4L2 M2M"),
|
||||
H264EncoderType::MediaCodec => write!(f, "MediaCodec"),
|
||||
H264EncoderType::Software => write!(f, "Software"),
|
||||
H264EncoderType::None => write!(f, "None"),
|
||||
}
|
||||
@@ -80,6 +83,7 @@ impl From<EncoderBackend> for H264EncoderType {
|
||||
EncoderBackend::Vaapi => H264EncoderType::Vaapi,
|
||||
EncoderBackend::Rkmpp => H264EncoderType::Rkmpp,
|
||||
EncoderBackend::V4l2m2m => H264EncoderType::V4l2M2m,
|
||||
EncoderBackend::MediaCodec => H264EncoderType::MediaCodec,
|
||||
EncoderBackend::Software => H264EncoderType::Software,
|
||||
}
|
||||
}
|
||||
@@ -224,10 +228,10 @@ pub fn detect_best_encoder(width: u32, height: u32) -> (H264EncoderType, Option<
|
||||
}
|
||||
}
|
||||
|
||||
/// Encoded frame from hwcodec (cloned for ownership)
|
||||
/// Encoded frame from hwcodec.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HwEncodeFrame {
|
||||
pub data: Vec<u8>,
|
||||
pub data: Bytes,
|
||||
pub pts: i64,
|
||||
pub key: i32,
|
||||
}
|
||||
@@ -372,14 +376,12 @@ impl H264Encoder {
|
||||
|
||||
self.frame_count += 1;
|
||||
|
||||
match self.inner.encode(data, pts_ms) {
|
||||
match self.inner.encode_bytes(data, pts_ms) {
|
||||
Ok(frames) => {
|
||||
// Zero-copy: drain frames from hwcodec buffer instead of cloning
|
||||
// hwcodec returns &mut Vec, so we can take ownership via drain
|
||||
let owned_frames: Vec<HwEncodeFrame> = frames
|
||||
.drain(..)
|
||||
.into_iter()
|
||||
.map(|f| HwEncodeFrame {
|
||||
data: f.data, // Move, not clone
|
||||
data: f.data,
|
||||
pts: f.pts,
|
||||
key: f.key,
|
||||
})
|
||||
|
||||
@@ -45,6 +45,8 @@ pub enum H265EncoderType {
|
||||
Rkmpp,
|
||||
/// V4L2 M2M (ARM generic)
|
||||
V4l2M2m,
|
||||
/// Android MediaCodec via FFmpeg
|
||||
MediaCodec,
|
||||
/// Software encoder (libx265)
|
||||
Software,
|
||||
/// No encoder available
|
||||
@@ -61,6 +63,7 @@ impl std::fmt::Display for H265EncoderType {
|
||||
H265EncoderType::Vaapi => write!(f, "VAAPI"),
|
||||
H265EncoderType::Rkmpp => write!(f, "RKMPP"),
|
||||
H265EncoderType::V4l2M2m => write!(f, "V4L2 M2M"),
|
||||
H265EncoderType::MediaCodec => write!(f, "MediaCodec"),
|
||||
H265EncoderType::Software => write!(f, "Software"),
|
||||
H265EncoderType::None => write!(f, "None"),
|
||||
}
|
||||
@@ -76,6 +79,7 @@ impl From<EncoderBackend> for H265EncoderType {
|
||||
EncoderBackend::Vaapi => H265EncoderType::Vaapi,
|
||||
EncoderBackend::Rkmpp => H265EncoderType::Rkmpp,
|
||||
EncoderBackend::V4l2m2m => H265EncoderType::V4l2M2m,
|
||||
EncoderBackend::MediaCodec => H265EncoderType::MediaCodec,
|
||||
EncoderBackend::Software => H265EncoderType::Software,
|
||||
}
|
||||
}
|
||||
@@ -243,10 +247,10 @@ pub fn is_h265_available() -> bool {
|
||||
registry.is_codec_available(VideoEncoderType::H265)
|
||||
}
|
||||
|
||||
/// Encoded frame from hwcodec (cloned for ownership)
|
||||
/// Encoded frame from hwcodec.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HwEncodeFrame {
|
||||
pub data: Vec<u8>,
|
||||
pub data: Bytes,
|
||||
pub pts: i64,
|
||||
pub key: i32,
|
||||
}
|
||||
@@ -465,13 +469,12 @@ impl H265Encoder {
|
||||
);
|
||||
}
|
||||
|
||||
match self.inner.encode(data, pts_ms) {
|
||||
match self.inner.encode_bytes(data, pts_ms) {
|
||||
Ok(frames) => {
|
||||
// Zero-copy: drain frames from hwcodec buffer instead of cloning
|
||||
let owned_frames: Vec<HwEncodeFrame> = frames
|
||||
.drain(..)
|
||||
.into_iter()
|
||||
.map(|f| HwEncodeFrame {
|
||||
data: f.data, // Move, not clone
|
||||
data: f.data,
|
||||
pts: f.pts,
|
||||
key: f.key,
|
||||
})
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
//! MJPEG decoder using TurboJPEG (software) -> RGB24.
|
||||
|
||||
use turbojpeg::{Decompressor, Image, PixelFormat as TJPixelFormat};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::format::Resolution;
|
||||
|
||||
pub struct MjpegTurboDecoder {
|
||||
decompressor: Decompressor,
|
||||
resolution: Resolution,
|
||||
}
|
||||
|
||||
impl MjpegTurboDecoder {
|
||||
pub fn new(resolution: Resolution) -> Result<Self> {
|
||||
let decompressor = Decompressor::new().map_err(|e| {
|
||||
AppError::VideoError(format!("Failed to create turbojpeg decoder: {}", e))
|
||||
})?;
|
||||
Ok(Self {
|
||||
decompressor,
|
||||
resolution,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decode_to_rgb(&mut self, mjpeg: &[u8]) -> Result<Vec<u8>> {
|
||||
let header = self
|
||||
.decompressor
|
||||
.read_header(mjpeg)
|
||||
.map_err(|e| AppError::VideoError(format!("turbojpeg read_header failed: {}", e)))?;
|
||||
|
||||
if header.width as u32 != self.resolution.width
|
||||
|| header.height as u32 != self.resolution.height
|
||||
{
|
||||
return Err(AppError::VideoError(format!(
|
||||
"turbojpeg size mismatch: {}x{} (expected {}x{})",
|
||||
header.width, header.height, self.resolution.width, self.resolution.height
|
||||
)));
|
||||
}
|
||||
|
||||
let pitch = header.width * 3;
|
||||
let mut image = Image {
|
||||
pixels: vec![0u8; header.height * pitch],
|
||||
width: header.width,
|
||||
pitch,
|
||||
height: header.height,
|
||||
format: TJPixelFormat::RGB,
|
||||
};
|
||||
|
||||
self.decompressor
|
||||
.decompress(mjpeg, image.as_deref_mut())
|
||||
.map_err(|e| AppError::VideoError(format!("turbojpeg decode failed: {}", e)))?;
|
||||
|
||||
Ok(image.pixels)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,10 @@
|
||||
use hwcodec::common::DataFormat;
|
||||
use hwcodec::ffmpeg_ram::CodecInfo;
|
||||
|
||||
#[cfg(feature = "android-mediacodec")]
|
||||
pub mod android_mediacodec;
|
||||
#[cfg(feature = "android-mediacodec")]
|
||||
pub mod android_mjpeg;
|
||||
pub mod convert;
|
||||
|
||||
pub mod h264;
|
||||
@@ -16,16 +20,17 @@ pub mod video_codec;
|
||||
pub mod vp8;
|
||||
pub mod vp9;
|
||||
|
||||
pub mod mjpeg_turbo;
|
||||
|
||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
||||
#[cfg(all(feature = "desktop", any(target_arch = "aarch64", target_arch = "arm")))]
|
||||
pub mod mjpeg_rkmpp;
|
||||
|
||||
pub use convert::{PixelConverter, Yuv420pBuffer};
|
||||
#[cfg(feature = "android-mediacodec")]
|
||||
pub use android_mediacodec::{AndroidH264Packet, AndroidMediaCodecH264Encoder};
|
||||
#[cfg(feature = "android-mediacodec")]
|
||||
pub use android_mjpeg::AndroidMediaCodecMjpegDecoder;
|
||||
pub use convert::{MjpegToNv12Decoder, PixelConverter, Yuv420pBuffer};
|
||||
pub use h264::{H264Config, H264Encoder, H264EncoderType, H264InputFormat};
|
||||
pub use h265::{H265Config, H265Encoder, H265EncoderType, H265InputFormat};
|
||||
pub use jpeg::JpegEncoder;
|
||||
pub use mjpeg_turbo::MjpegTurboDecoder;
|
||||
pub use registry::{AvailableEncoder, EncoderBackend, EncoderRegistry, VideoEncoderType};
|
||||
pub use self_check::{
|
||||
build_hardware_self_check_runtime_error, run_hardware_self_check, VideoEncoderSelfCheckCell,
|
||||
|
||||
@@ -96,6 +96,8 @@ pub enum EncoderBackend {
|
||||
Rkmpp,
|
||||
/// V4L2 Memory-to-Memory (ARM)
|
||||
V4l2m2m,
|
||||
/// Android MediaCodec via FFmpeg
|
||||
MediaCodec,
|
||||
/// Software encoding (libx264, libx265, libvpx)
|
||||
Software,
|
||||
}
|
||||
@@ -115,6 +117,8 @@ impl EncoderBackend {
|
||||
EncoderBackend::Rkmpp
|
||||
} else if name.contains("v4l2m2m") {
|
||||
EncoderBackend::V4l2m2m
|
||||
} else if name.contains("mediacodec") {
|
||||
EncoderBackend::MediaCodec
|
||||
} else {
|
||||
EncoderBackend::Software
|
||||
}
|
||||
@@ -134,6 +138,7 @@ impl EncoderBackend {
|
||||
EncoderBackend::Amf => "AMF",
|
||||
EncoderBackend::Rkmpp => "RKMPP",
|
||||
EncoderBackend::V4l2m2m => "V4L2 M2M",
|
||||
EncoderBackend::MediaCodec => "MediaCodec",
|
||||
EncoderBackend::Software => "Software",
|
||||
}
|
||||
}
|
||||
@@ -148,6 +153,7 @@ impl EncoderBackend {
|
||||
"amf" => Some(EncoderBackend::Amf),
|
||||
"rkmpp" => Some(EncoderBackend::Rkmpp),
|
||||
"v4l2m2m" | "v4l2" => Some(EncoderBackend::V4l2m2m),
|
||||
"mediacodec" | "android-mediacodec" => Some(EncoderBackend::MediaCodec),
|
||||
"software" | "cpu" => Some(EncoderBackend::Software),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ use serde::Serialize;
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[cfg(feature = "android-mediacodec")]
|
||||
use super::AndroidMediaCodecH264Encoder;
|
||||
use super::{
|
||||
EncoderRegistry, H264Config, H264Encoder, H265Config, H265Encoder, VP8Config, VP8Encoder,
|
||||
VP9Config, VP9Encoder, VideoEncoderType,
|
||||
@@ -235,6 +237,32 @@ fn run_smoke_test(
|
||||
}
|
||||
|
||||
fn run_h264_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> {
|
||||
#[cfg(feature = "android-mediacodec")]
|
||||
if codec_name_ffmpeg == "h264_mediacodec" {
|
||||
let mut encoder = AndroidMediaCodecH264Encoder::new(
|
||||
resolution,
|
||||
PixelFormat::Nv12,
|
||||
30,
|
||||
bitrate_kbps_for_resolution(resolution),
|
||||
)?;
|
||||
encoder.request_keyframe();
|
||||
let frame = build_nv12_test_frame(
|
||||
resolution,
|
||||
PixelFormat::Nv12.frame_size(resolution).unwrap_or(0),
|
||||
);
|
||||
|
||||
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
|
||||
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
|
||||
if frames.iter().any(|frame| !frame.data.is_empty()) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
return Err(AppError::VideoError(
|
||||
"Encoder produced no output after multiple frames".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut encoder = H264Encoder::with_codec(
|
||||
H264Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)),
|
||||
codec_name_ffmpeg,
|
||||
|
||||
Reference in New Issue
Block a user