#![allow(unsafe_code)]
#![allow(clippy::similar_names)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::match_same_arms)]
#![allow(clippy::ptr_as_ptr)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::unnecessary_cast)]
#![allow(clippy::if_not_else)]
#![allow(clippy::unnecessary_wraps)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::if_same_then_else)]
#![allow(clippy::cast_lossless)]
use std::ffi::CStr;
use std::path::Path;
use std::ptr;
use std::sync::Arc;
use std::time::Duration;
use ff_format::NetworkOptions;
use ff_format::PooledBuffer;
use ff_format::codec::VideoCodec;
use ff_format::color::{ColorPrimaries, ColorRange, ColorSpace};
use ff_format::container::ContainerInfo;
use ff_format::time::{Rational, Timestamp};
use ff_format::{PixelFormat, VideoFrame, VideoStreamInfo};
use ff_sys::{
AVBufferRef, AVCodecContext, AVCodecID, AVColorPrimaries, AVColorRange, AVColorSpace,
AVFormatContext, AVFrame, AVHWDeviceType, AVMediaType_AVMEDIA_TYPE_VIDEO, AVPacket,
AVPixelFormat, SwsContext,
};
use crate::HardwareAccel;
use crate::error::DecodeError;
use crate::video::builder::OutputScale;
use ff_common::FramePool;
const KEYFRAME_SEEK_TOLERANCE_SECS: u64 = 1;
mod context;
mod decoding;
mod format_convert;
mod hardware;
mod seeking;
struct AvFormatContextGuard(*mut AVFormatContext);
impl AvFormatContextGuard {
unsafe fn new(path: &Path) -> Result<Self, DecodeError> {
let format_ctx = unsafe {
ff_sys::avformat::open_input(path).map_err(|e| DecodeError::Ffmpeg {
code: e,
message: format!("Failed to open file: {}", ff_sys::av_error_string(e)),
})?
};
Ok(Self(format_ctx))
}
const fn as_ptr(&self) -> *mut AVFormatContext {
self.0
}
fn into_raw(self) -> *mut AVFormatContext {
let ptr = self.0;
std::mem::forget(self);
ptr
}
unsafe fn new_image_sequence(path: &Path, framerate: u32) -> Result<Self, DecodeError> {
let format_ctx = unsafe {
ff_sys::avformat::open_input_image_sequence(path, framerate).map_err(|e| {
DecodeError::Ffmpeg {
code: e,
message: format!(
"Failed to open image sequence: {}",
ff_sys::av_error_string(e)
),
}
})?
};
Ok(Self(format_ctx))
}
unsafe fn new_url(url: &str, network: &NetworkOptions) -> Result<Self, DecodeError> {
let format_ctx = unsafe {
ff_sys::avformat::open_input_url(url, network.connect_timeout, network.read_timeout)
.map_err(|e| {
crate::network::map_network_error(e, crate::network::sanitize_url(url))
})?
};
Ok(Self(format_ctx))
}
}
impl Drop for AvFormatContextGuard {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
ff_sys::avformat::close_input(&mut (self.0 as *mut _));
}
}
}
}
struct AvCodecContextGuard(*mut AVCodecContext);
impl AvCodecContextGuard {
unsafe fn new(codec: *const ff_sys::AVCodec) -> Result<Self, DecodeError> {
let codec_ctx = unsafe {
ff_sys::avcodec::alloc_context3(codec).map_err(|e| DecodeError::Ffmpeg {
code: e,
message: format!("Failed to allocate codec context: {e}"),
})?
};
Ok(Self(codec_ctx))
}
const fn as_ptr(&self) -> *mut AVCodecContext {
self.0
}
fn into_raw(self) -> *mut AVCodecContext {
let ptr = self.0;
std::mem::forget(self);
ptr
}
}
impl Drop for AvCodecContextGuard {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
ff_sys::avcodec::free_context(&mut (self.0 as *mut _));
}
}
}
}
struct AvPacketGuard(*mut AVPacket);
impl AvPacketGuard {
unsafe fn new() -> Result<Self, DecodeError> {
let packet = unsafe { ff_sys::av_packet_alloc() };
if packet.is_null() {
return Err(DecodeError::Ffmpeg {
code: 0,
message: "Failed to allocate packet".to_string(),
});
}
Ok(Self(packet))
}
fn into_raw(self) -> *mut AVPacket {
let ptr = self.0;
std::mem::forget(self);
ptr
}
}
impl Drop for AvPacketGuard {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
ff_sys::av_packet_free(&mut (self.0 as *mut _));
}
}
}
}
struct AvFrameGuard(*mut AVFrame);
impl AvFrameGuard {
unsafe fn new() -> Result<Self, DecodeError> {
let frame = unsafe { ff_sys::av_frame_alloc() };
if frame.is_null() {
return Err(DecodeError::Ffmpeg {
code: 0,
message: "Failed to allocate frame".to_string(),
});
}
Ok(Self(frame))
}
const fn as_ptr(&self) -> *mut AVFrame {
self.0
}
fn into_raw(self) -> *mut AVFrame {
let ptr = self.0;
std::mem::forget(self);
ptr
}
}
impl Drop for AvFrameGuard {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
ff_sys::av_frame_free(&mut (self.0 as *mut _));
}
}
}
}
pub(crate) struct VideoDecoderInner {
pub(super) format_ctx: *mut AVFormatContext,
pub(super) codec_ctx: *mut AVCodecContext,
pub(super) stream_index: i32,
pub(super) sws_ctx: Option<*mut SwsContext>,
pub(super) sws_cache_key: Option<(u32, u32, i32, u32, u32, i32)>,
pub(super) output_format: Option<PixelFormat>,
pub(super) output_scale: Option<OutputScale>,
pub(super) is_live: bool,
pub(super) eof: bool,
pub(super) position: Duration,
pub(super) packet: *mut AVPacket,
pub(super) frame: *mut AVFrame,
pub(super) thumbnail_sws_ctx: Option<*mut SwsContext>,
pub(super) thumbnail_cache_key: Option<(u32, u32, u32, u32, AVPixelFormat)>,
pub(super) hw_device_ctx: Option<*mut AVBufferRef>,
pub(super) active_hw_accel: HardwareAccel,
pub(super) frame_pool: Option<Arc<dyn FramePool>>,
pub(super) url: Option<String>,
pub(super) network_opts: NetworkOptions,
pub(super) reconnect_count: u32,
pub(super) consecutive_invalid: u32,
}
impl VideoDecoderInner {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
path: &Path,
output_format: Option<PixelFormat>,
output_scale: Option<OutputScale>,
hardware_accel: HardwareAccel,
thread_count: usize,
frame_rate: Option<u32>,
frame_pool: Option<Arc<dyn FramePool>>,
network_opts: Option<NetworkOptions>,
) -> Result<(Self, VideoStreamInfo, ContainerInfo), DecodeError> {
ff_sys::ensure_initialized();
let path_str = path.to_str().unwrap_or("");
let is_image_sequence = path_str.contains('%');
let is_network_url = crate::network::is_url(path_str);
let url = if is_network_url {
Some(path_str.to_owned())
} else {
None
};
let stored_network_opts = network_opts.clone().unwrap_or_default();
if is_network_url {
crate::network::check_srt_url(path_str)?;
}
let format_ctx_guard = unsafe {
if is_network_url {
let network = network_opts.unwrap_or_default();
log::info!(
"opening network source url={} connect_timeout_ms={} read_timeout_ms={}",
crate::network::sanitize_url(path_str),
network.connect_timeout.as_millis(),
network.read_timeout.as_millis(),
);
AvFormatContextGuard::new_url(path_str, &network)?
} else if is_image_sequence {
let fps = frame_rate.unwrap_or(25);
AvFormatContextGuard::new_image_sequence(path, fps)?
} else {
AvFormatContextGuard::new(path)?
}
};
let format_ctx = format_ctx_guard.as_ptr();
unsafe {
ff_sys::avformat::find_stream_info(format_ctx).map_err(|e| DecodeError::Ffmpeg {
code: e,
message: format!("Failed to find stream info: {}", ff_sys::av_error_string(e)),
})?;
}
let is_live = unsafe {
let iformat = (*format_ctx).iformat;
!iformat.is_null() && ((*iformat).flags & ff_sys::AVFMT_TS_DISCONT) != 0
};
let (stream_index, codec_id) =
unsafe { Self::find_video_stream(format_ctx) }.ok_or_else(|| {
DecodeError::NoVideoStream {
path: path.to_path_buf(),
}
})?;
let codec_name = unsafe { Self::extract_codec_name(codec_id) };
let codec = unsafe {
ff_sys::avcodec::find_decoder(codec_id).ok_or_else(|| {
if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_EXR {
DecodeError::DecoderUnavailable {
codec: "exr".to_string(),
hint: "Requires FFmpeg built with EXR support \
(--enable-decoder=exr)"
.to_string(),
}
} else {
DecodeError::UnsupportedCodec {
codec: format!("{codec_name} (codec_id={codec_id:?})"),
}
}
})?
};
let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
let codec_ctx = codec_ctx_guard.as_ptr();
unsafe {
let stream = (*format_ctx).streams.add(stream_index as usize);
let codecpar = (*(*stream)).codecpar;
ff_sys::avcodec::parameters_to_context(codec_ctx, codecpar).map_err(|e| {
DecodeError::Ffmpeg {
code: e,
message: format!(
"Failed to copy codec parameters: {}",
ff_sys::av_error_string(e)
),
}
})?;
if thread_count > 0 {
(*codec_ctx).thread_count = thread_count as i32;
}
}
let (hw_device_ctx, active_hw_accel) =
unsafe { Self::init_hardware_accel(codec_ctx, hardware_accel)? };
unsafe {
ff_sys::avcodec::open2(codec_ctx, codec, ptr::null_mut()).map_err(|e| {
if let Some(hw_ctx) = hw_device_ctx {
ff_sys::av_buffer_unref(&mut (hw_ctx as *mut _));
}
DecodeError::Ffmpeg {
code: e,
message: format!("Failed to open codec: {}", ff_sys::av_error_string(e)),
}
})?;
}
let stream_info =
unsafe { Self::extract_stream_info(format_ctx, stream_index as i32, codec_ctx)? };
let container_info = unsafe { Self::extract_container_info(format_ctx) };
let packet_guard = unsafe { AvPacketGuard::new()? };
let frame_guard = unsafe { AvFrameGuard::new()? };
Ok((
Self {
format_ctx: format_ctx_guard.into_raw(),
codec_ctx: codec_ctx_guard.into_raw(),
stream_index: stream_index as i32,
sws_ctx: None,
sws_cache_key: None,
output_format,
output_scale,
is_live,
eof: false,
position: Duration::ZERO,
packet: packet_guard.into_raw(),
frame: frame_guard.into_raw(),
thumbnail_sws_ctx: None,
thumbnail_cache_key: None,
hw_device_ctx,
active_hw_accel,
frame_pool,
url,
network_opts: stored_network_opts,
reconnect_count: 0,
consecutive_invalid: 0,
},
stream_info,
container_info,
))
}
}
impl Drop for VideoDecoderInner {
fn drop(&mut self) {
if let Some(sws_ctx) = self.sws_ctx {
unsafe {
ff_sys::swscale::free_context(sws_ctx);
}
}
if let Some(thumbnail_ctx) = self.thumbnail_sws_ctx {
unsafe {
ff_sys::swscale::free_context(thumbnail_ctx);
}
}
if let Some(hw_ctx) = self.hw_device_ctx {
unsafe {
ff_sys::av_buffer_unref(&mut (hw_ctx as *mut _));
}
}
if !self.frame.is_null() {
unsafe {
ff_sys::av_frame_free(&mut (self.frame as *mut _));
}
}
if !self.packet.is_null() {
unsafe {
ff_sys::av_packet_free(&mut (self.packet as *mut _));
}
}
if !self.codec_ctx.is_null() {
unsafe {
ff_sys::avcodec::free_context(&mut (self.codec_ctx as *mut _));
}
}
if !self.format_ctx.is_null() {
unsafe {
ff_sys::avformat::close_input(&mut (self.format_ctx as *mut _));
}
}
}
}
unsafe impl Send for VideoDecoderInner {}
#[cfg(test)]
mod tests {
use ff_format::PixelFormat;
use ff_format::codec::VideoCodec;
use ff_format::color::{ColorPrimaries, ColorRange, ColorSpace};
use crate::HardwareAccel;
use super::VideoDecoderInner;
#[test]
fn pixel_format_yuv420p() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P),
PixelFormat::Yuv420p
);
}
#[test]
fn pixel_format_yuv422p() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P),
PixelFormat::Yuv422p
);
}
#[test]
fn pixel_format_yuv444p() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P),
PixelFormat::Yuv444p
);
}
#[test]
fn pixel_format_rgb24() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24),
PixelFormat::Rgb24
);
}
#[test]
fn pixel_format_bgr24() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_BGR24),
PixelFormat::Bgr24
);
}
#[test]
fn pixel_format_rgba() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA),
PixelFormat::Rgba
);
}
#[test]
fn pixel_format_bgra() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_BGRA),
PixelFormat::Bgra
);
}
#[test]
fn pixel_format_gray8() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8),
PixelFormat::Gray8
);
}
#[test]
fn pixel_format_nv12() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NV12),
PixelFormat::Nv12
);
}
#[test]
fn pixel_format_nv21() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NV21),
PixelFormat::Nv21
);
}
#[test]
fn pixel_format_yuv420p10le_should_return_yuv420p10le() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P10LE),
PixelFormat::Yuv420p10le
);
}
#[test]
fn pixel_format_yuv422p10le_should_return_yuv422p10le() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P10LE),
PixelFormat::Yuv422p10le
);
}
#[test]
fn pixel_format_yuv444p10le_should_return_yuv444p10le() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P10LE),
PixelFormat::Yuv444p10le
);
}
#[test]
fn pixel_format_p010le_should_return_p010le() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_P010LE),
PixelFormat::P010le
);
}
#[test]
fn pixel_format_unknown_falls_back_to_yuv420p() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NONE),
PixelFormat::Yuv420p
);
}
#[test]
fn color_space_bt709() {
assert_eq!(
VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT709),
ColorSpace::Bt709
);
}
#[test]
fn color_space_bt470bg_yields_bt601() {
assert_eq!(
VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT470BG),
ColorSpace::Bt601
);
}
#[test]
fn color_space_smpte170m_yields_bt601() {
assert_eq!(
VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_SMPTE170M),
ColorSpace::Bt601
);
}
#[test]
fn color_space_bt2020_ncl() {
assert_eq!(
VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT2020_NCL),
ColorSpace::Bt2020
);
}
#[test]
fn color_space_unknown_falls_back_to_bt709() {
assert_eq!(
VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_UNSPECIFIED),
ColorSpace::Bt709
);
}
#[test]
fn color_range_jpeg_yields_full() {
assert_eq!(
VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_JPEG),
ColorRange::Full
);
}
#[test]
fn color_range_mpeg_yields_limited() {
assert_eq!(
VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_MPEG),
ColorRange::Limited
);
}
#[test]
fn color_range_unknown_falls_back_to_limited() {
assert_eq!(
VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_UNSPECIFIED),
ColorRange::Limited
);
}
#[test]
fn color_primaries_bt709() {
assert_eq!(
VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT709),
ColorPrimaries::Bt709
);
}
#[test]
fn color_primaries_bt470bg_yields_bt601() {
assert_eq!(
VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT470BG),
ColorPrimaries::Bt601
);
}
#[test]
fn color_primaries_smpte170m_yields_bt601() {
assert_eq!(
VideoDecoderInner::convert_color_primaries(
ff_sys::AVColorPrimaries_AVCOL_PRI_SMPTE170M
),
ColorPrimaries::Bt601
);
}
#[test]
fn color_primaries_bt2020() {
assert_eq!(
VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT2020),
ColorPrimaries::Bt2020
);
}
#[test]
fn color_primaries_unknown_falls_back_to_bt709() {
assert_eq!(
VideoDecoderInner::convert_color_primaries(
ff_sys::AVColorPrimaries_AVCOL_PRI_UNSPECIFIED
),
ColorPrimaries::Bt709
);
}
#[test]
fn codec_h264() {
assert_eq!(
VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_H264),
VideoCodec::H264
);
}
#[test]
fn codec_hevc_yields_h265() {
assert_eq!(
VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_HEVC),
VideoCodec::H265
);
}
#[test]
fn codec_vp8() {
assert_eq!(
VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_VP8),
VideoCodec::Vp8
);
}
#[test]
fn codec_vp9() {
assert_eq!(
VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_VP9),
VideoCodec::Vp9
);
}
#[test]
fn codec_av1() {
assert_eq!(
VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_AV1),
VideoCodec::Av1
);
}
#[test]
fn codec_mpeg4() {
assert_eq!(
VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_MPEG4),
VideoCodec::Mpeg4
);
}
#[test]
fn codec_prores() {
assert_eq!(
VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_PRORES),
VideoCodec::ProRes
);
}
#[test]
fn codec_unknown_falls_back_to_h264() {
assert_eq!(
VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_NONE),
VideoCodec::H264
);
}
#[test]
fn hw_accel_auto_yields_none() {
assert_eq!(
VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Auto),
None
);
}
#[test]
fn hw_accel_none_yields_none() {
assert_eq!(
VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::None),
None
);
}
#[test]
fn hw_accel_nvdec_yields_cuda() {
assert_eq!(
VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Nvdec),
Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_CUDA)
);
}
#[test]
fn hw_accel_qsv_yields_qsv() {
assert_eq!(
VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Qsv),
Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_QSV)
);
}
#[test]
fn hw_accel_amf_yields_d3d11va() {
assert_eq!(
VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Amf),
Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_D3D11VA)
);
}
#[test]
fn hw_accel_videotoolbox() {
assert_eq!(
VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::VideoToolbox),
Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_VIDEOTOOLBOX)
);
}
#[test]
fn hw_accel_vaapi() {
assert_eq!(
VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Vaapi),
Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_VAAPI)
);
}
#[test]
fn pixel_format_to_av_round_trip_yuv420p() {
let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv420p);
assert_eq!(
VideoDecoderInner::convert_pixel_format(av),
PixelFormat::Yuv420p
);
}
#[test]
fn pixel_format_to_av_round_trip_yuv422p() {
let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv422p);
assert_eq!(
VideoDecoderInner::convert_pixel_format(av),
PixelFormat::Yuv422p
);
}
#[test]
fn pixel_format_to_av_round_trip_yuv444p() {
let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv444p);
assert_eq!(
VideoDecoderInner::convert_pixel_format(av),
PixelFormat::Yuv444p
);
}
#[test]
fn pixel_format_to_av_round_trip_rgb24() {
let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Rgb24);
assert_eq!(
VideoDecoderInner::convert_pixel_format(av),
PixelFormat::Rgb24
);
}
#[test]
fn pixel_format_to_av_round_trip_bgr24() {
let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Bgr24);
assert_eq!(
VideoDecoderInner::convert_pixel_format(av),
PixelFormat::Bgr24
);
}
#[test]
fn pixel_format_to_av_round_trip_rgba() {
let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Rgba);
assert_eq!(
VideoDecoderInner::convert_pixel_format(av),
PixelFormat::Rgba
);
}
#[test]
fn pixel_format_to_av_round_trip_bgra() {
let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Bgra);
assert_eq!(
VideoDecoderInner::convert_pixel_format(av),
PixelFormat::Bgra
);
}
#[test]
fn pixel_format_to_av_round_trip_gray8() {
let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Gray8);
assert_eq!(
VideoDecoderInner::convert_pixel_format(av),
PixelFormat::Gray8
);
}
#[test]
fn pixel_format_to_av_round_trip_nv12() {
let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Nv12);
assert_eq!(
VideoDecoderInner::convert_pixel_format(av),
PixelFormat::Nv12
);
}
#[test]
fn pixel_format_to_av_round_trip_nv21() {
let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Nv21);
assert_eq!(
VideoDecoderInner::convert_pixel_format(av),
PixelFormat::Nv21
);
}
#[test]
fn pixel_format_to_av_unknown_falls_back_to_yuv420p_av() {
assert_eq!(
VideoDecoderInner::pixel_format_to_av(PixelFormat::Other(999)),
ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P
);
}
#[test]
fn codec_name_should_return_h264_for_h264_codec_id() {
let name =
unsafe { VideoDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_H264) };
assert_eq!(name, "h264");
}
#[test]
fn codec_name_should_return_none_for_none_codec_id() {
let name =
unsafe { VideoDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_NONE) };
assert_eq!(name, "none");
}
#[test]
fn convert_pixel_format_should_map_gbrpf32le() {
assert_eq!(
VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GBRPF32LE),
PixelFormat::Gbrpf32le
);
}
#[test]
fn unsupported_codec_error_should_include_codec_name() {
let codec_id = ff_sys::AVCodecID_AV_CODEC_ID_H264;
let codec_name = unsafe { VideoDecoderInner::extract_codec_name(codec_id) };
let error = crate::error::DecodeError::UnsupportedCodec {
codec: format!("{codec_name} (codec_id={codec_id:?})"),
};
let msg = error.to_string();
assert!(msg.contains("h264"), "expected codec name in error: {msg}");
assert!(
msg.contains("codec_id="),
"expected codec_id in error: {msg}"
);
}
}