use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::marker::PhantomData;
use std::os::raw::c_int;
use std::path::Path;
use std::ptr::NonNull;
use std::time::Duration;
use crate::io_context::IoContext;
use crate::io_traits::{IoSink, IoSource};
use crate::{
AV_DICT_IGNORE_SUFFIX, AV_INPUT_BUFFER_PADDING_SIZE, AV_TIME_BASE, AVChannelLayout, AVChapter,
AVCodecID, AVCodecID_AV_CODEC_ID_BIN_DATA, AVCodecParameters, AVColorPrimaries, AVColorRange,
AVColorSpace, AVDictionary, AVDictionaryEntry, AVFormatContext, AVMediaType,
AVMediaType_AVMEDIA_TYPE_ATTACHMENT, AVRational, AVStream, AvError, Codec, CodecContext,
Packet, av_dict_get as ffi_av_dict_get, av_dict_set as ffi_av_dict_set,
av_interleaved_write_frame as ffi_av_interleaved_write_frame, av_mallocz as ffi_av_mallocz,
av_opt_set as ffi_av_opt_set, avcodec_parameters_copy as ffi_avcodec_parameters_copy,
avformat_alloc_context as ffi_avformat_alloc_context,
avformat_close_input as ffi_avformat_close_input,
avformat_new_stream as ffi_avformat_new_stream, avformat_open_input as ffi_avformat_open_input,
};
unsafe fn read_dict(dict: *const AVDictionary) -> HashMap<String, String> {
let mut map = HashMap::new();
if dict.is_null() {
return map;
}
let flags = AV_DICT_IGNORE_SUFFIX.cast_signed();
let mut entry: *const AVDictionaryEntry = std::ptr::null();
loop {
entry = unsafe { ffi_av_dict_get(dict, c"".as_ptr(), entry, flags) };
if entry.is_null() {
break;
}
let (key_ptr, value_ptr) = unsafe { ((*entry).key, (*entry).value) };
if key_ptr.is_null() || value_ptr.is_null() {
continue;
}
let key = unsafe { CStr::from_ptr(key_ptr) }
.to_string_lossy()
.into_owned();
let value = unsafe { CStr::from_ptr(value_ptr) }
.to_string_lossy()
.into_owned();
map.insert(key, value);
}
map
}
#[derive(Debug)]
pub struct InputFormatContext {
ptr: NonNull<AVFormatContext>,
#[allow(dead_code)]
io: Option<IoContext>,
}
impl InputFormatContext {
pub fn open(path: &Path) -> Result<Self, AvError> {
let ptr = unsafe { crate::avformat::open_input(path) }.map_err(AvError::new)?;
Self::from_raw(ptr)
}
pub fn open_url(
url: &str,
connect_timeout: Duration,
read_timeout: Duration,
) -> Result<Self, AvError> {
let ptr = unsafe { crate::avformat::open_input_url(url, connect_timeout, read_timeout) }
.map_err(AvError::new)?;
Self::from_raw(ptr)
}
pub fn open_image_sequence(path: &Path, framerate: u32) -> Result<Self, AvError> {
let ptr = unsafe { crate::avformat::open_input_image_sequence(path, framerate) }
.map_err(AvError::new)?;
Self::from_raw(ptr)
}
pub fn open_custom(source: impl IoSource + 'static) -> Result<Self, AvError> {
crate::ensure_initialized();
let io = IoContext::reader(source)?;
let ctx = unsafe { ffi_avformat_alloc_context() };
let ctx = NonNull::new(ctx).ok_or_else(|| AvError::new(crate::error_codes::ENOMEM))?;
unsafe {
(*ctx.as_ptr()).pb = io.as_ptr();
(*ctx.as_ptr()).flags |= crate::constants::AVFMT_FLAG_CUSTOM_IO;
}
let mut raw = ctx.as_ptr();
let ret = unsafe {
ffi_avformat_open_input(
&mut raw,
std::ptr::null(),
std::ptr::null(),
std::ptr::null_mut(),
)
};
if ret < 0 {
return Err(AvError::new(ret));
}
NonNull::new(raw)
.ok_or_else(|| AvError::new(crate::error_codes::ENOMEM))
.map(|ptr| Self { ptr, io: Some(io) })
}
fn from_raw(ptr: *mut AVFormatContext) -> Result<Self, AvError> {
NonNull::new(ptr)
.ok_or_else(|| AvError::new(crate::error_codes::ENOMEM))
.map(|ptr| Self { ptr, io: None })
}
#[must_use]
pub fn nb_streams(&self) -> u32 {
unsafe { (*self.ptr.as_ptr()).nb_streams }
}
#[must_use]
pub fn duration(&self) -> i64 {
unsafe { (*self.ptr.as_ptr()).duration }
}
#[must_use]
pub fn iformat_flags(&self) -> c_int {
unsafe {
let iformat = (*self.ptr.as_ptr()).iformat;
if iformat.is_null() {
0
} else {
(*iformat).flags
}
}
}
#[must_use]
pub fn bit_rate(&self) -> i64 {
unsafe { (*self.ptr.as_ptr()).bit_rate }
}
#[must_use]
pub fn iformat_name(&self) -> Option<String> {
unsafe {
let iformat = (*self.ptr.as_ptr()).iformat;
if iformat.is_null() {
return None;
}
let name = (*iformat).name;
if name.is_null() {
return None;
}
Some(CStr::from_ptr(name).to_string_lossy().into_owned())
}
}
#[must_use]
pub fn iformat_long_name(&self) -> Option<String> {
unsafe {
let iformat = (*self.ptr.as_ptr()).iformat;
if iformat.is_null() {
return None;
}
let long_name = (*iformat).long_name;
if long_name.is_null() {
return None;
}
Some(CStr::from_ptr(long_name).to_string_lossy().into_owned())
}
}
#[must_use]
pub fn metadata(&self) -> HashMap<String, String> {
unsafe { read_dict((*self.ptr.as_ptr()).metadata) }
}
#[must_use]
pub fn nb_chapters(&self) -> u32 {
unsafe { (*self.ptr.as_ptr()).nb_chapters }
}
#[must_use]
pub fn chapter(&self, index: usize) -> Option<ChapterRef<'_>> {
unsafe {
let ctx = self.ptr.as_ptr();
if index >= (*ctx).nb_chapters as usize {
return None;
}
let chapter_ptr = *(*ctx).chapters.add(index);
NonNull::new(chapter_ptr).map(|ptr| ChapterRef {
ptr,
_marker: PhantomData,
})
}
}
pub fn chapters(&self) -> impl Iterator<Item = ChapterRef<'_>> + '_ {
(0..self.nb_chapters() as usize).filter_map(move |i| self.chapter(i))
}
#[must_use]
pub fn stream(&self, index: usize) -> Option<StreamRef<'_>> {
unsafe {
let ctx = self.ptr.as_ptr();
if index >= (*ctx).nb_streams as usize {
return None;
}
let stream_ptr = *(*ctx).streams.add(index);
NonNull::new(stream_ptr).map(|ptr| StreamRef {
ptr,
_marker: PhantomData,
})
}
}
pub fn streams(&self) -> impl Iterator<Item = StreamRef<'_>> + '_ {
(0..self.nb_streams() as usize).filter_map(move |i| self.stream(i))
}
pub fn find_stream_info(&mut self) -> Result<(), AvError> {
unsafe { crate::avformat::find_stream_info(self.ptr.as_ptr()) }.map_err(AvError::new)
}
pub fn seek_frame(
&mut self,
stream_index: c_int,
timestamp: i64,
flags: c_int,
) -> Result<(), AvError> {
unsafe { crate::avformat::seek_frame(self.ptr.as_ptr(), stream_index, timestamp, flags) }
.map_err(AvError::new)
}
pub fn seek_file(
&mut self,
stream_index: c_int,
min_ts: i64,
ts: i64,
max_ts: i64,
flags: c_int,
) -> Result<(), AvError> {
unsafe {
crate::avformat::seek_file(self.ptr.as_ptr(), stream_index, min_ts, ts, max_ts, flags)
}
.map_err(AvError::new)
}
pub fn read_frame(&mut self, pkt: &mut Packet) -> Result<(), AvError> {
unsafe { crate::avformat::read_frame(self.ptr.as_ptr(), pkt.as_mut_ptr()) }
.map_err(AvError::new)
}
}
impl Drop for InputFormatContext {
fn drop(&mut self) {
unsafe {
let mut raw = self.ptr.as_ptr();
ffi_avformat_close_input(&mut raw);
}
}
}
unsafe impl Send for InputFormatContext {}
#[derive(Debug)]
pub struct OutputFormatContext {
ptr: NonNull<AVFormatContext>,
io: Option<IoContext>,
}
impl OutputFormatContext {
pub fn new(format_name: Option<&str>, filename: &Path) -> Result<Self, AvError> {
crate::ensure_initialized();
let c_format = match format_name {
Some(name) => {
Some(CString::new(name).map_err(|_| AvError::new(crate::error_codes::EINVAL))?)
}
None => None,
};
let filename_str = filename
.to_str()
.ok_or_else(|| AvError::new(crate::error_codes::EINVAL))?;
let c_filename =
CString::new(filename_str).map_err(|_| AvError::new(crate::error_codes::EINVAL))?;
let mut ctx: *mut AVFormatContext = std::ptr::null_mut();
let ret = unsafe {
crate::avformat_alloc_output_context2(
&mut ctx,
std::ptr::null_mut(),
c_format.as_ref().map_or(std::ptr::null(), |c| c.as_ptr()),
c_filename.as_ptr(),
)
};
if ret < 0 {
return Err(AvError::new(ret));
}
NonNull::new(ctx)
.ok_or_else(|| AvError::new(crate::error_codes::ENOMEM))
.map(|ptr| Self { ptr, io: None })
}
#[must_use]
pub fn nb_streams(&self) -> u32 {
unsafe { (*self.ptr.as_ptr()).nb_streams }
}
#[must_use]
pub fn oformat_flags(&self) -> c_int {
unsafe {
let oformat = (*self.ptr.as_ptr()).oformat;
if oformat.is_null() {
0
} else {
(*oformat).flags
}
}
}
#[must_use]
pub fn is_nofile(&self) -> bool {
self.oformat_flags() & crate::constants::AVFMT_NOFILE != 0
}
pub fn open_io(&mut self, path: &Path) -> Result<(), AvError> {
let pb = unsafe { crate::avformat::open_output(path, crate::avformat::avio_flags::WRITE) }
.map_err(AvError::new)?;
unsafe { (*self.ptr.as_ptr()).pb = pb };
Ok(())
}
pub fn set_custom_io(&mut self, sink: impl IoSink + 'static) -> Result<(), AvError> {
let io = IoContext::writer(sink)?;
unsafe {
if self.io.is_none() && !(*self.ptr.as_ptr()).pb.is_null() {
crate::avformat::close_output(&mut (*self.ptr.as_ptr()).pb);
}
(*self.ptr.as_ptr()).pb = io.as_ptr();
(*self.ptr.as_ptr()).flags |= crate::constants::AVFMT_FLAG_CUSTOM_IO;
}
self.io = Some(io);
Ok(())
}
pub fn write_header(&mut self) -> Result<(), AvError> {
let ret = unsafe { crate::avformat_write_header(self.ptr.as_ptr(), std::ptr::null_mut()) };
if ret < 0 {
Err(AvError::new(ret))
} else {
Ok(())
}
}
pub fn write_trailer(&mut self) -> Result<(), AvError> {
let ret = unsafe { crate::av_write_trailer(self.ptr.as_ptr()) };
if ret < 0 {
Err(AvError::new(ret))
} else {
Ok(())
}
}
pub fn close_io(&mut self) {
if let Some(io) = self.io.take() {
unsafe { (*self.ptr.as_ptr()).pb = std::ptr::null_mut() };
drop(io);
return;
}
unsafe { crate::avformat::close_output(&mut (*self.ptr.as_ptr()).pb) };
}
fn stream_ptr(&self, idx: usize) -> *mut AVStream {
unsafe {
let ctx = self.ptr.as_ptr();
if idx >= (*ctx).nb_streams as usize {
std::ptr::null_mut()
} else {
*(*ctx).streams.add(idx)
}
}
}
pub fn new_stream(&mut self, codec: Option<&Codec>) -> Result<usize, AvError> {
let codec_ptr = codec.map_or(std::ptr::null(), Codec::as_ptr);
let stream = unsafe { ffi_avformat_new_stream(self.ptr.as_ptr(), codec_ptr) };
if stream.is_null() {
return Err(AvError::new(crate::error_codes::ENOMEM));
}
Ok(self.nb_streams() as usize - 1)
}
#[must_use]
pub fn stream_time_base(&self, idx: usize) -> AVRational {
let stream = self.stream_ptr(idx);
if stream.is_null() {
AVRational { num: 0, den: 0 }
} else {
unsafe { (*stream).time_base }
}
}
pub fn set_stream_time_base(&mut self, idx: usize, time_base: AVRational) {
let stream = self.stream_ptr(idx);
if !stream.is_null() {
unsafe { (*stream).time_base = time_base };
}
}
pub fn apply_stream_params_from_context(
&mut self,
idx: usize,
ctx: &CodecContext,
) -> Result<(), AvError> {
let stream = self.stream_ptr(idx);
if stream.is_null() {
return Err(AvError::new(crate::error_codes::EINVAL));
}
unsafe {
let par = (*stream).codecpar;
ctx.parameters_from_context(par)
}
}
pub fn copy_stream_params(
&mut self,
idx: usize,
src: CodecParameters<'_>,
) -> Result<(), AvError> {
let stream = self.stream_ptr(idx);
if stream.is_null() {
return Err(AvError::new(crate::error_codes::EINVAL));
}
unsafe {
let dst = (*stream).codecpar;
let ret = ffi_avcodec_parameters_copy(dst, src.as_raw());
if ret < 0 {
return Err(AvError::new(ret));
}
(*dst).codec_tag = 0;
}
Ok(())
}
pub fn set_opt(&mut self, key: &CStr, value: &CStr) -> Result<(), AvError> {
let ret = unsafe {
ffi_av_opt_set(
(*self.ptr.as_ptr()).priv_data,
key.as_ptr(),
value.as_ptr(),
0,
)
};
if ret < 0 {
Err(AvError::new(ret))
} else {
Ok(())
}
}
pub fn write_interleaved(&mut self, pkt: &mut Packet) -> Result<(), AvError> {
let ret = unsafe { ffi_av_interleaved_write_frame(self.ptr.as_ptr(), pkt.as_mut_ptr()) };
if ret < 0 {
Err(AvError::new(ret))
} else {
Ok(())
}
}
pub fn set_metadata(&mut self, key: &str, value: &str) -> Result<(), AvError> {
let key_c = CString::new(key).map_err(|_| AvError::new(crate::error_codes::EINVAL))?;
let value_c = CString::new(value).map_err(|_| AvError::new(crate::error_codes::EINVAL))?;
let ret = unsafe {
ffi_av_dict_set(
&raw mut (*self.ptr.as_ptr()).metadata,
key_c.as_ptr(),
value_c.as_ptr(),
0,
)
};
if ret < 0 {
Err(AvError::new(ret))
} else {
Ok(())
}
}
pub fn add_attachment_stream(
&mut self,
data: &[u8],
mime_type: &str,
filename: &str,
) -> Result<usize, AvError> {
let extradata_size =
c_int::try_from(data.len()).map_err(|_| AvError::new(crate::error_codes::EINVAL))?;
let filename_c =
CString::new(filename).map_err(|_| AvError::new(crate::error_codes::EINVAL))?;
let mime_c =
CString::new(mime_type).map_err(|_| AvError::new(crate::error_codes::EINVAL))?;
unsafe {
let stream = ffi_avformat_new_stream(self.ptr.as_ptr(), std::ptr::null());
if stream.is_null() {
return Err(AvError::new(crate::error_codes::ENOMEM));
}
let codecpar = (*stream).codecpar;
(*codecpar).codec_type = AVMediaType_AVMEDIA_TYPE_ATTACHMENT;
(*codecpar).codec_id = AVCodecID_AV_CODEC_ID_BIN_DATA;
let alloc_size = data.len() + AV_INPUT_BUFFER_PADDING_SIZE as usize;
let extradata = ffi_av_mallocz(alloc_size).cast::<u8>();
if extradata.is_null() {
return Err(AvError::new(crate::error_codes::ENOMEM));
}
std::ptr::copy_nonoverlapping(data.as_ptr(), extradata, data.len());
(*codecpar).extradata = extradata;
(*codecpar).extradata_size = extradata_size;
ffi_av_dict_set(
&raw mut (*stream).metadata,
c"filename".as_ptr(),
filename_c.as_ptr(),
0,
);
ffi_av_dict_set(
&raw mut (*stream).metadata,
c"mimetype".as_ptr(),
mime_c.as_ptr(),
0,
);
}
Ok(self.nb_streams() as usize - 1)
}
pub fn set_chapters(&mut self, chapters: &[ChapterSpec<'_>]) -> Result<(), AvError> {
if chapters.is_empty() {
return Ok(());
}
unsafe {
let ctx = self.ptr.as_ptr();
let arr = ffi_av_mallocz(std::mem::size_of::<*mut AVChapter>() * chapters.len())
.cast::<*mut AVChapter>();
if arr.is_null() {
return Err(AvError::new(crate::error_codes::ENOMEM));
}
(*ctx).chapters = arr;
(*ctx).nb_chapters = 0;
for spec in chapters {
let chap = ffi_av_mallocz(std::mem::size_of::<AVChapter>()).cast::<AVChapter>();
if chap.is_null() {
log::warn!(
"av_mallocz failed for AVChapter, skipping chapter id={}",
spec.id
);
continue;
}
(*chap).id = spec.id;
(*chap).time_base = AVRational {
num: 1,
den: AV_TIME_BASE as c_int,
};
(*chap).start = spec.start_us;
(*chap).end = spec.end_us;
(*chap).metadata = std::ptr::null_mut::<AVDictionary>();
if let Some(title) = spec.title {
if let Ok(title_c) = CString::new(title) {
ffi_av_dict_set(
&raw mut (*chap).metadata,
c"title".as_ptr(),
title_c.as_ptr(),
0,
);
} else {
log::warn!(
"chapter title contains a NUL byte, skipping title id={}",
spec.id
);
}
}
*arr.add((*ctx).nb_chapters as usize) = chap;
(*ctx).nb_chapters += 1;
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug)]
pub struct ChapterSpec<'a> {
pub id: i64,
pub start_us: i64,
pub end_us: i64,
pub title: Option<&'a str>,
}
impl Drop for OutputFormatContext {
fn drop(&mut self) {
unsafe {
let ctx = self.ptr.as_ptr();
if self.io.is_some() {
(*ctx).pb = std::ptr::null_mut();
} else if !(*ctx).pb.is_null() {
crate::avformat::close_output(&mut (*ctx).pb);
}
crate::avformat_free_context(ctx);
}
}
}
unsafe impl Send for OutputFormatContext {}
#[derive(Clone, Copy, Debug)]
pub struct StreamRef<'a> {
ptr: NonNull<AVStream>,
_marker: PhantomData<&'a InputFormatContext>,
}
impl<'a> StreamRef<'a> {
#[must_use]
pub fn index(&self) -> c_int {
unsafe { (*self.ptr.as_ptr()).index }
}
#[must_use]
pub fn time_base(&self) -> AVRational {
unsafe { (*self.ptr.as_ptr()).time_base }
}
#[must_use]
pub fn avg_frame_rate(&self) -> AVRational {
unsafe { (*self.ptr.as_ptr()).avg_frame_rate }
}
#[must_use]
pub fn r_frame_rate(&self) -> AVRational {
unsafe { (*self.ptr.as_ptr()).r_frame_rate }
}
#[must_use]
pub fn nb_frames(&self) -> i64 {
unsafe { (*self.ptr.as_ptr()).nb_frames }
}
#[must_use]
pub fn codecpar(&self) -> CodecParameters<'a> {
unsafe {
let par = (*self.ptr.as_ptr()).codecpar;
CodecParameters {
ptr: NonNull::new_unchecked(par),
_marker: PhantomData,
}
}
}
#[must_use]
pub fn duration(&self) -> i64 {
unsafe { (*self.ptr.as_ptr()).duration }
}
#[must_use]
pub fn disposition(&self) -> c_int {
unsafe { (*self.ptr.as_ptr()).disposition }
}
#[must_use]
pub fn metadata(&self) -> HashMap<String, String> {
unsafe { read_dict((*self.ptr.as_ptr()).metadata) }
}
}
#[derive(Clone, Copy, Debug)]
pub struct CodecParameters<'a> {
ptr: NonNull<AVCodecParameters>,
_marker: PhantomData<&'a ()>,
}
impl<'a> CodecParameters<'a> {
pub(crate) const fn from_raw(ptr: NonNull<AVCodecParameters>) -> Self {
Self {
ptr,
_marker: PhantomData,
}
}
}
impl CodecParameters<'_> {
#[must_use]
pub fn codec_type(&self) -> AVMediaType {
unsafe { (*self.ptr.as_ptr()).codec_type }
}
#[must_use]
pub fn codec_id(&self) -> AVCodecID {
unsafe { (*self.ptr.as_ptr()).codec_id }
}
#[must_use]
pub fn width(&self) -> c_int {
unsafe { (*self.ptr.as_ptr()).width }
}
#[must_use]
pub fn height(&self) -> c_int {
unsafe { (*self.ptr.as_ptr()).height }
}
#[must_use]
pub fn sample_rate(&self) -> c_int {
unsafe { (*self.ptr.as_ptr()).sample_rate }
}
#[must_use]
pub fn color_space(&self) -> AVColorSpace {
unsafe { (*self.ptr.as_ptr()).color_space }
}
#[must_use]
pub fn color_range(&self) -> AVColorRange {
unsafe { (*self.ptr.as_ptr()).color_range }
}
#[must_use]
pub fn color_primaries(&self) -> AVColorPrimaries {
unsafe { (*self.ptr.as_ptr()).color_primaries }
}
#[must_use]
pub fn ch_layout(&self) -> AVChannelLayout {
unsafe { (*self.ptr.as_ptr()).ch_layout }
}
#[must_use]
pub fn format(&self) -> c_int {
unsafe { (*self.ptr.as_ptr()).format }
}
#[must_use]
pub fn bit_rate(&self) -> i64 {
unsafe { (*self.ptr.as_ptr()).bit_rate }
}
pub(crate) fn as_raw(&self) -> *const AVCodecParameters {
self.ptr.as_ptr()
}
}
#[derive(Clone, Copy, Debug)]
pub struct ChapterRef<'a> {
ptr: NonNull<AVChapter>,
_marker: PhantomData<&'a InputFormatContext>,
}
impl ChapterRef<'_> {
#[must_use]
pub fn id(&self) -> i64 {
unsafe { (*self.ptr.as_ptr()).id }
}
#[must_use]
pub fn time_base(&self) -> AVRational {
unsafe { (*self.ptr.as_ptr()).time_base }
}
#[must_use]
pub fn start(&self) -> i64 {
unsafe { (*self.ptr.as_ptr()).start }
}
#[must_use]
pub fn end(&self) -> i64 {
unsafe { (*self.ptr.as_ptr()).end }
}
#[must_use]
pub fn metadata(&self) -> HashMap<String, String> {
unsafe { read_dict((*self.ptr.as_ptr()).metadata) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_should_error_on_missing_path() {
let result = InputFormatContext::open(Path::new("/nonexistent/path/to/file.mp4"));
assert!(result.is_err());
}
fn custom_io_fixture() -> Option<Vec<u8>> {
let path = std::path::PathBuf::from(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../assets/audio/konekonoosanpo.mp3"
));
std::fs::read(path).ok()
}
#[test]
fn open_custom_should_mark_the_context_as_custom_io() {
let Some(bytes) = custom_io_fixture() else {
return; };
let Ok(ctx) = InputFormatContext::open_custom(std::io::Cursor::new(bytes)) else {
return; };
let flags = unsafe { (*ctx.ptr.as_ptr()).flags };
assert_ne!(
flags & crate::constants::AVFMT_FLAG_CUSTOM_IO,
0,
"a custom-IO input must carry AVFMT_FLAG_CUSTOM_IO"
);
}
#[test]
fn open_custom_should_reject_bytes_that_are_not_a_container() {
let result = InputFormatContext::open_custom(std::io::Cursor::new(vec![0u8; 512]));
assert!(result.is_err(), "512 zero bytes are not a media container");
}
#[test]
fn set_custom_io_should_mark_the_context_as_custom_io() {
let Ok(mut ctx) = OutputFormatContext::new(None, Path::new("out.mp4")) else {
return; };
if ctx.set_custom_io(std::io::Cursor::new(Vec::new())).is_err() {
return;
}
let flags = unsafe { (*ctx.ptr.as_ptr()).flags };
assert_ne!(
flags & crate::constants::AVFMT_FLAG_CUSTOM_IO,
0,
"a custom-IO output must carry AVFMT_FLAG_CUSTOM_IO"
);
assert!(ctx.io.is_some(), "the sink must be owned by the context");
}
#[test]
fn input_open_valid_file_should_allocate_and_drop_cleanly() {
let path = std::path::PathBuf::from(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../assets/audio/konekonoosanpo.mp3"
));
let Ok(mut ctx) = InputFormatContext::open(&path) else {
return; };
let _ = ctx.find_stream_info();
if ctx.nb_streams() == 0 {
return;
}
assert!(
ctx.nb_streams() >= 1,
"an opened container should expose at least one stream"
);
}
#[test]
fn read_dict_should_collect_all_entries() {
let mut dict: *mut AVDictionary = std::ptr::null_mut();
unsafe {
ffi_av_dict_set(&mut dict, c"title".as_ptr(), c"Example".as_ptr(), 0);
ffi_av_dict_set(&mut dict, c"language".as_ptr(), c"eng".as_ptr(), 0);
}
let map = unsafe { read_dict(dict) };
unsafe { crate::av_dict_free(&mut dict) };
assert_eq!(map.get("title").map(String::as_str), Some("Example"));
assert_eq!(map.get("language").map(String::as_str), Some("eng"));
assert_eq!(map.len(), 2);
}
#[test]
fn read_dict_should_return_empty_for_null() {
let map = unsafe { read_dict(std::ptr::null()) };
assert!(map.is_empty());
}
#[test]
fn output_new_should_error_on_bogus_format() {
let result =
OutputFormatContext::new(Some("definitely_not_a_real_muxer"), Path::new("out.bin"));
assert!(result.is_err());
}
#[test]
fn output_new_should_allocate_and_drop() {
let Ok(ctx) = OutputFormatContext::new(None, Path::new("out.mp4")) else {
return;
};
assert!(!ctx.is_nofile());
}
#[test]
fn output_stream_api_should_round_trip() {
let Ok(mut ctx) = OutputFormatContext::new(None, Path::new("out.mp4")) else {
return;
};
let idx = ctx
.new_stream(None)
.expect("new_stream should allocate a stream");
assert_eq!(idx, 0);
assert_eq!(ctx.nb_streams(), 1);
ctx.set_stream_time_base(idx, AVRational { num: 1, den: 30 });
let tb = ctx.stream_time_base(idx);
assert_eq!((tb.num, tb.den), (1, 30));
let oob = ctx.stream_time_base(99);
assert_eq!((oob.num, oob.den), (0, 0));
let key = std::ffi::CString::new("definitely_not_a_real_option").unwrap();
let value = std::ffi::CString::new("1").unwrap();
assert!(ctx.set_opt(&key, &value).is_err());
let cc = CodecContext::new(None).expect("codec context alloc should succeed");
assert!(ctx.apply_stream_params_from_context(99, &cc).is_err());
}
#[test]
fn metadata_attachment_and_chapters_should_apply() {
let Ok(mut ctx) = OutputFormatContext::new(None, Path::new("out.mp4")) else {
return;
};
ctx.set_metadata("title", "test")
.expect("set_metadata should succeed");
assert!(ctx.set_metadata("k\0", "v").is_err());
let idx = ctx
.add_attachment_stream(b"font-bytes", "application/x-font", "font.ttf")
.expect("add_attachment_stream should allocate a stream");
assert_eq!(ctx.nb_streams(), idx as u32 + 1);
assert!(ctx.add_attachment_stream(b"x", "m", "f\0").is_err());
ctx.set_chapters(&[]).expect("empty chapters is Ok");
ctx.set_chapters(&[
ChapterSpec {
id: 0,
start_us: 0,
end_us: 1_000_000,
title: Some("Intro"),
},
ChapterSpec {
id: 1,
start_us: 1_000_000,
end_us: 2_000_000,
title: None,
},
])
.expect("set_chapters should allocate");
}
}