use std::{
collections::VecDeque,
ffi::{CStr, c_int},
io::{Read, Seek},
mem,
num::NonZeroI32,
path::Path,
ptr::{addr_of, read_unaligned},
sync::Arc,
};
use derive_more::{IsVariant, TryUnwrap, Unwrap};
use ffmpeg_next::{
Packet, Rational,
ffi::{
AV_DISPOSITION_ATTACHED_PIC, AV_DISPOSITION_TIMED_THUMBNAILS, AV_NOPTS_VALUE, AVDictionary,
av_dict_get,
},
format::{self, context::Input},
media,
};
use mediadecode::{
Timebase, Timestamp,
demuxer::{
AttachmentPacket, AttachmentTrackPacket, AttachmentTrackParams, AudioTrackPacket,
AudioTrackParams, DataTrackPacket, DataTrackParams, DemuxedPacket, Demuxer,
SubtitleTrackPacket, SubtitleTrackParams, TrackIndex, TrackInfo, TrackKind, TrackParams,
UnknownTrackParams, VideoTrackPacket, VideoTrackParams,
},
};
use smol_str::SmolStr;
use crate::{
Ffmpeg, FfmpegBuffer, boundary,
buffer::PacketBufferError,
codec_id::CodecId,
extras::{AttachmentPacketExtra, TrackExtra},
reader_guard::{GuardedReader, PanicLatch},
sample_format::SampleFormat,
};
fn av_time_base_q() -> Timebase {
Timebase::new(1, NonZeroI32::new(1_000_000).expect("1e6 is non-zero"))
}
pub struct FfmpegDemuxer {
input: Input,
tracks: Vec<TrackInfo<Ffmpeg>>,
pending: VecDeque<(
TrackIndex,
AttachmentPacket<AttachmentPacketExtra, FfmpegBuffer>,
)>,
eof: bool,
reader_panic: Option<Arc<PanicLatch>>,
}
impl FfmpegDemuxer {
pub fn open<P: AsRef<Path> + ?Sized>(path: &P) -> Result<Self, DemuxError> {
Self::from_input(format::input(path)?)
}
pub fn open_reader<R: Read + Seek + Send + 'static>(
reader: R,
filename: Option<&str>,
) -> Result<Self, DemuxError> {
let (guarded, latch) = GuardedReader::new(reader);
let io = format::context::StreamIo::from_read_seek(guarded)?;
let input = format::input_from_stream(io, filename, None)
.map_err(|e| reader_panic(&latch).unwrap_or(DemuxError::Ffmpeg(e)))?;
if let Some(panicked) = reader_panic(&latch) {
return Err(panicked);
}
let mut demuxer = Self::from_input(input)?;
demuxer.reader_panic = Some(latch);
Ok(demuxer)
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn input(&self) -> &Input {
&self.input
}
fn from_input(input: Input) -> Result<Self, DemuxError> {
let (tracks, pending) = build_tracks(&input)?;
Ok(Self {
input,
tracks,
pending,
eof: false,
reader_panic: None,
})
}
fn panicked(&self) -> Option<DemuxError> {
self.reader_panic.as_deref().and_then(reader_panic)
}
}
fn on_stream<T>(
stream_index: usize,
result: Result<Option<T>, PacketBufferError>,
) -> Result<Option<T>, DemuxError> {
result.map_err(|source| DemuxError::PacketBuffer(PacketBuffer::new(stream_index, source)))
}
fn reader_panic(latch: &PanicLatch) -> Option<DemuxError> {
latch
.message()
.map(|message| DemuxError::ReaderPanic(ReaderPanic::new(message)))
}
impl Demuxer for FfmpegDemuxer {
type Adapter = Ffmpeg;
type Buffer = FfmpegBuffer;
type Error = DemuxError;
fn tracks(&self) -> &[TrackInfo<Ffmpeg>] {
&self.tracks
}
fn take_tracks(&mut self) -> Vec<TrackInfo<Ffmpeg>> {
mem::take(&mut self.tracks)
}
fn next_packet(&mut self) -> Result<Option<DemuxedPacket<Ffmpeg, FfmpegBuffer>>, DemuxError> {
if let Some(panicked) = self.panicked() {
return Err(panicked);
}
if let Some((track, packet)) = self.pending.pop_front() {
return Ok(Some(DemuxedPacket::Attachment(AttachmentTrackPacket::new(
track, packet,
))));
}
loop {
let mut packet = Packet::empty();
let read = packet.read(&mut self.input);
if let Some(panicked) = self.panicked() {
return Err(panicked);
}
match read {
Ok(()) => {}
Err(ffmpeg_next::Error::Eof) => {
self.eof = true;
return Ok(None);
}
Err(ffmpeg_next::Error::InvalidData) => continue,
Err(e) => return Err(DemuxError::Ffmpeg(e)),
}
let index = packet.stream();
let Some(info) = self.tracks.get(index) else {
continue;
};
let track = TrackIndex::new(index);
let time_base = info.timebase();
let built = match info.kind() {
TrackKind::Video => on_stream(
index,
boundary::video_packet_from_ffmpeg_in(&packet, time_base),
)?
.map(|packet| DemuxedPacket::Video(VideoTrackPacket::new(track, packet))),
TrackKind::Audio => on_stream(
index,
boundary::audio_packet_from_ffmpeg_in(&packet, time_base),
)?
.map(|packet| DemuxedPacket::Audio(AudioTrackPacket::new(track, packet))),
TrackKind::Subtitle => on_stream(
index,
boundary::subtitle_packet_from_ffmpeg_in(&packet, time_base),
)?
.map(|packet| DemuxedPacket::Subtitle(SubtitleTrackPacket::new(track, packet))),
TrackKind::Data => on_stream(
index,
boundary::data_packet_from_ffmpeg_in(&packet, time_base),
)?
.map(|packet| DemuxedPacket::Data(DataTrackPacket::new(track, packet))),
TrackKind::Attachment => continue,
TrackKind::Unknown => continue,
};
if let Some(out) = built {
return Ok(Some(out));
}
}
}
fn seek(&mut self, target: Timestamp) -> Result<(), DemuxError> {
let ts = target.rescale_to(av_time_base_q()).pts();
if self.eof {
self.input.clear_eof();
self.eof = false;
}
let sought = self.input.seek(ts, ..ts);
if let Some(panicked) = self.panicked() {
return Err(panicked);
}
sought?;
Ok(())
}
}
#[derive(thiserror::Error, Debug, Clone)]
#[error("out of memory capturing the attachment payload for stream {stream_index}")]
pub struct AttachmentAlloc {
stream_index: usize,
}
impl AttachmentAlloc {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(stream_index: usize) -> Self {
Self { stream_index }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn stream_index(&self) -> usize {
self.stream_index
}
}
#[derive(thiserror::Error, Debug, Clone)]
#[error("the codec parameters for stream {stream_index} were never allocated")]
pub struct ParametersMissing {
stream_index: usize,
}
impl ParametersMissing {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(stream_index: usize) -> Self {
Self { stream_index }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn stream_index(&self) -> usize {
self.stream_index
}
}
#[derive(thiserror::Error, Debug, Clone)]
#[error("out of memory allocating the codec parameters for stream {stream_index}")]
pub struct ParametersAlloc {
stream_index: usize,
}
impl ParametersAlloc {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(stream_index: usize) -> Self {
Self { stream_index }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn stream_index(&self) -> usize {
self.stream_index
}
}
#[derive(thiserror::Error, Debug, Clone)]
#[error("the codec parameters for stream {stream_index} could not be copied: {source}")]
pub struct ParametersCopy {
stream_index: usize,
#[source]
source: ffmpeg_next::Error,
}
impl ParametersCopy {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(stream_index: usize, source: ffmpeg_next::Error) -> Self {
Self {
stream_index,
source,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn stream_index(&self) -> usize {
self.stream_index
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn source(&self) -> &ffmpeg_next::Error {
&self.source
}
}
#[derive(thiserror::Error, Debug, Clone)]
#[error("stream {stream_index}: {source}")]
pub struct PacketBuffer {
stream_index: usize,
#[source]
source: PacketBufferError,
}
impl PacketBuffer {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(stream_index: usize, source: PacketBufferError) -> Self {
Self {
stream_index,
source,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn stream_index(&self) -> usize {
self.stream_index
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn source(&self) -> &PacketBufferError {
&self.source
}
}
#[derive(thiserror::Error, Debug, Clone)]
#[error("the reader panicked: {message}")]
pub struct ReaderPanic {
message: SmolStr,
}
impl ReaderPanic {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(message: SmolStr) -> Self {
Self { message }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn message(&self) -> &str {
self.message.as_str()
}
}
#[derive(thiserror::Error, Debug, Clone, IsVariant, Unwrap, TryUnwrap)]
#[unwrap(ref, ref_mut)]
#[try_unwrap(ref, ref_mut)]
pub enum DemuxError {
#[error(transparent)]
Ffmpeg(#[from] ffmpeg_next::Error),
#[error(transparent)]
AttachmentAlloc(#[from] AttachmentAlloc),
#[error(transparent)]
ParametersMissing(#[from] ParametersMissing),
#[error(transparent)]
ParametersAlloc(#[from] ParametersAlloc),
#[error(transparent)]
ParametersCopy(#[from] ParametersCopy),
#[error(transparent)]
PacketBuffer(#[from] PacketBuffer),
#[error(transparent)]
ReaderPanic(#[from] ReaderPanic),
}
type BuiltTracks = (
Vec<TrackInfo<Ffmpeg>>,
VecDeque<(
TrackIndex,
AttachmentPacket<AttachmentPacketExtra, FfmpegBuffer>,
)>,
);
fn build_tracks(input: &Input) -> Result<BuiltTracks, DemuxError> {
let count = input.streams().len();
let mut tracks = Vec::with_capacity(count);
let mut pending = VecDeque::new();
for stream in input.streams() {
let index = stream.index();
debug_assert_eq!(
index,
tracks.len(),
"AVStream indices are dense and ordered"
);
let parameters = stream.parameters();
let par = unsafe { parameters.as_ptr() };
let medium = parameters.medium();
let codec =
CodecId::from_raw(unsafe { read_unaligned(addr_of!((*par).codec_id).cast::<i32>()) });
let disposition = unsafe { (*stream.as_ptr()).disposition };
let attached_pic = is_attachment_disposition(disposition);
let time_base = rational_to_timebase(stream.time_base());
let raw_duration = stream.duration();
let duration = (raw_duration != AV_NOPTS_VALUE && raw_duration > 0)
.then(|| Timestamp::new(raw_duration, time_base));
let raw_start = stream.start_time();
let frames = stream.frames();
let params = if attached_pic {
TrackParams::Attachment(AttachmentTrackParams::new(codec))
} else {
match medium {
media::Type::Video => TrackParams::Video(VideoTrackParams::new(
codec,
unsafe { (*par).width }.max(0) as u32,
unsafe { (*par).height }.max(0) as u32,
boundary::from_av_pixel_format(unsafe { (*par).format }),
rate_to_timebase(stream.avg_frame_rate()),
)),
media::Type::Audio => {
let ch_layout = unsafe { std::ptr::addr_of!((*par).ch_layout) };
let channel_layout =
unsafe { crate::channel_layout::channel_layout_description_from_raw_ptr(ch_layout) };
TrackParams::Audio(AudioTrackParams::new(
codec,
unsafe { (*par).sample_rate }.max(0) as u32,
channel_layout.channels().min(255) as u8,
SampleFormat::from_raw(unsafe { (*par).format }),
channel_layout,
))
}
media::Type::Subtitle => TrackParams::Subtitle(SubtitleTrackParams::new(codec)),
media::Type::Data => TrackParams::Data(DataTrackParams::new(codec)),
media::Type::Attachment => TrackParams::Attachment(AttachmentTrackParams::new(codec)),
media::Type::Unknown => TrackParams::Unknown(UnknownTrackParams::new(codec)),
}
};
let extra = TrackExtra::new(
index as i32,
crate::extras::clone_parameters(¶meters, index)?,
)?
.with_disposition(disposition)
.with_start_time((raw_start != AV_NOPTS_VALUE).then_some(raw_start))
.with_frame_count((frames > 0).then_some(frames));
let metadata = unsafe { (*stream.as_ptr()).metadata };
let info = TrackInfo::new(time_base, params, extra)
.with_duration(duration)
.with_filename(unsafe { metadata_text(metadata, c"filename") })
.with_mime_type(unsafe { metadata_text(metadata, c"mimetype") });
if info.kind() == TrackKind::Attachment {
let packet = if attached_pic {
let pkt = unsafe { std::ptr::addr_of!((*stream.as_ptr()).attached_pic) };
unsafe { attached_pic_payload(pkt, index) }?
} else {
extradata_payload(&stream)?
};
pending.push_back((TrackIndex::new(index), packet));
}
tracks.push(info);
}
Ok((tracks, pending))
}
const fn is_attachment_disposition(disposition: c_int) -> bool {
disposition & AV_DISPOSITION_ATTACHED_PIC != 0
&& disposition & AV_DISPOSITION_TIMED_THUMBNAILS == 0
}
const METADATA_VALUE_MAX_BYTES: usize = 64 * 1024;
unsafe fn metadata_text(dict: *const AVDictionary, key: &CStr) -> Option<SmolStr> {
if dict.is_null() {
return None;
}
let entry = unsafe { av_dict_get(dict, key.as_ptr(), std::ptr::null(), 0) };
if entry.is_null() {
return None;
}
let value = unsafe { (*entry).value };
if value.is_null() {
return None;
}
for len in 0..METADATA_VALUE_MAX_BYTES {
if unsafe { *value.add(len).cast::<u8>() } == 0 {
let bytes = unsafe { std::slice::from_raw_parts(value.cast::<u8>(), len) };
return Some(SmolStr::new(std::string::String::from_utf8_lossy(bytes)));
}
}
None
}
unsafe fn attached_pic_payload(
pkt: *const ffmpeg_next::ffi::AVPacket,
index: usize,
) -> Result<AttachmentPacket<AttachmentPacketExtra, FfmpegBuffer>, DemuxError> {
let captured = unsafe { crate::buffer::payload_of(pkt) }
.map_err(|source| DemuxError::PacketBuffer(PacketBuffer::new(index, source)))?;
let extra = AttachmentPacketExtra::new(index as i32);
Ok(match captured {
Some(payload) => {
let flags = unsafe { boundary::md_flags_from_av_packet(pkt) }
.map_err(|source| DemuxError::PacketBuffer(PacketBuffer::new(index, source)))?;
AttachmentPacket::new(payload, extra).with_flags(flags)
}
None => AttachmentPacket::new(
FfmpegBuffer::copy_from_slice(&[])
.ok_or(DemuxError::AttachmentAlloc(AttachmentAlloc::new(index)))?,
extra.with_synthesized(true),
),
})
}
fn extradata_payload(
stream: &ffmpeg_next::format::stream::Stream<'_>,
) -> Result<AttachmentPacket<AttachmentPacketExtra, FfmpegBuffer>, DemuxError> {
let index = stream.index();
let parameters = stream.parameters();
let par = unsafe { parameters.as_ptr() };
let ptr = unsafe { (*par).extradata };
let len = unsafe { (*par).extradata_size }.max(0) as usize;
let bytes: &[u8] = if ptr.is_null() || len == 0 {
&[]
} else {
unsafe { std::slice::from_raw_parts(ptr, len) }
};
let payload = FfmpegBuffer::copy_from_slice(bytes)
.ok_or(DemuxError::AttachmentAlloc(AttachmentAlloc::new(index)))?;
Ok(AttachmentPacket::new(
payload,
AttachmentPacketExtra::new(index as i32).with_synthesized(true),
))
}
fn rational_to_timebase(value: Rational) -> Timebase {
Timebase::new(
value.numerator(),
NonZeroI32::new(value.denominator().max(1)).expect("clamped to at least 1"),
)
}
fn rate_to_timebase(value: Rational) -> Option<Timebase> {
let (num, den) = (value.numerator(), value.denominator());
(num > 0 && den > 0).then(|| Timebase::new(num, NonZeroI32::new(den).expect("checked above")))
}
#[cfg(test)]
mod tests {
use ffmpeg_next::ffi::{av_dict_free, av_dict_set};
use ffmpeg_next::codec::Parameters;
use super::*;
use crate::extras::TrackExtra;
fn dict_with(key: &CStr, value: &[u8]) -> *mut AVDictionary {
let mut dict: *mut AVDictionary = std::ptr::null_mut();
let mut terminated = value.to_vec();
terminated.push(0);
let rc = unsafe {
av_dict_set(
&mut dict,
key.as_ptr(),
terminated.as_ptr().cast::<std::ffi::c_char>(),
0,
)
};
assert!(rc >= 0, "av_dict_set failed: {rc}");
dict
}
#[test]
fn metadata_that_is_not_utf8_is_read_lossily_not_unsoundly() {
let raw = b"caf\xE9.ttf".to_vec();
assert!(
std::str::from_utf8(&raw).is_err(),
"the source bytes really are not UTF-8",
);
let dict = dict_with(c"filename", &raw);
let text = unsafe { metadata_text(dict, c"filename") }.expect("the entry exists");
assert_eq!(text.as_str(), "caf\u{FFFD}.ttf");
assert_eq!(unsafe { metadata_text(dict, c"mimetype") }, None);
assert_eq!(
unsafe { metadata_text(std::ptr::null(), c"filename") },
None
);
unsafe { av_dict_free(&mut { dict }) };
}
#[test]
fn valid_metadata_survives_unchanged() {
let dict = dict_with(c"mimetype", b"application/x-truetype-font");
assert_eq!(
unsafe { metadata_text(dict, c"mimetype") }.as_deref(),
Some("application/x-truetype-font"),
);
unsafe { av_dict_free(&mut { dict }) };
}
#[test]
fn an_unterminated_length_is_refused_rather_than_truncated() {
let long = vec![b'a'; METADATA_VALUE_MAX_BYTES + 1];
let dict = dict_with(c"filename", &long);
assert_eq!(unsafe { metadata_text(dict, c"filename") }, None);
unsafe { av_dict_free(&mut { dict }) };
}
struct PanicsWithAHostilePayload;
struct PanicOnDrop;
impl Drop for PanicOnDrop {
fn drop(&mut self) {
panic!("and the payload went too");
}
}
impl std::io::Read for PanicsWithAHostilePayload {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
std::panic::panic_any(PanicOnDrop);
}
}
impl std::io::Seek for PanicsWithAHostilePayload {
fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result<u64> {
std::panic::panic_any(PanicOnDrop);
}
}
#[test]
fn a_reader_panic_with_a_hostile_payload_does_not_abort_the_process() {
crate::fault_subprocess::in_subprocess(
"demuxer::tests::a_reader_panic_with_a_hostile_payload_does_not_abort_the_process",
|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let opened = FfmpegDemuxer::open_reader(PanicsWithAHostilePayload, Some("x.mkv"));
std::panic::set_hook(previous);
match opened {
Err(DemuxError::ReaderPanic(_)) => {}
Err(other) => panic!("expected ReaderPanic, got {other:?}"),
Ok(_) => panic!("a reader that only panics cannot open a container"),
}
},
);
}
#[test]
fn codec_parameters_that_cannot_be_allocated_are_named() {
crate::fault_subprocess::in_subprocess(
"demuxer::tests::codec_parameters_that_cannot_be_allocated_are_named",
|| {
let source = Parameters::new();
assert!(
!unsafe { source.as_ptr() }.is_null(),
"the source allocates before the cap goes on",
);
crate::fault_subprocess::cap_ffmpeg_allocations(1);
let refused = crate::extras::clone_parameters(&source, 4);
crate::fault_subprocess::uncap_ffmpeg_allocations();
assert!(
matches!(
refused,
Err(DemuxError::ParametersAlloc(ref p)) if p.stream_index() == 4
),
"expected ParametersAlloc, got {:?}",
refused.map(|_| ()),
);
crate::extras::clone_parameters(&source, 4).expect("an uncapped copy");
},
);
}
#[test]
fn the_public_track_extra_copies_are_checked_too() {
crate::fault_subprocess::in_subprocess(
"demuxer::tests::the_public_track_extra_copies_are_checked_too",
|| {
let source = Parameters::new();
assert!(!unsafe { source.as_ptr() }.is_null(), "allocated uncapped");
let extra = TrackExtra::new(
6,
crate::extras::clone_parameters(&source, 6).expect("uncapped"),
)
.expect("real parameters");
crate::fault_subprocess::cap_ffmpeg_allocations(1);
let cloned = extra.try_clone().map(|_| ());
let handed = extra.clone_parameters().map(|_| ());
crate::fault_subprocess::uncap_ffmpeg_allocations();
assert!(
matches!(cloned, Err(DemuxError::ParametersAlloc(ref p)) if p.stream_index() == 6),
"TrackExtra::try_clone: {cloned:?}",
);
assert!(
matches!(handed, Err(DemuxError::ParametersAlloc(ref p)) if p.stream_index() == 6),
"TrackExtra::clone_parameters: {handed:?}",
);
extra.try_clone().expect("an uncapped row copy");
extra.clone_parameters().expect("an uncapped handoff");
},
);
}
#[test]
fn parameters_that_never_allocated_are_refused_at_the_door() {
crate::fault_subprocess::in_subprocess(
"demuxer::tests::parameters_that_never_allocated_are_refused_at_the_door",
|| {
crate::fault_subprocess::cap_ffmpeg_allocations(1);
let never_allocated = Parameters::new();
crate::fault_subprocess::uncap_ffmpeg_allocations();
assert!(
unsafe { never_allocated.as_ptr() }.is_null(),
"the safe constructor really does hand back a null-backed value",
);
let refused = TrackExtra::new(9, never_allocated);
let Err(DemuxError::ParametersMissing(p)) = refused.map(|_| ()) else {
panic!("a null-backed source must not become a track row");
};
assert_eq!(p.stream_index(), 9);
let never_allocated = {
crate::fault_subprocess::cap_ffmpeg_allocations(1);
let p = Parameters::new();
crate::fault_subprocess::uncap_ffmpeg_allocations();
p
};
assert!(matches!(
crate::extras::clone_parameters(&never_allocated, 9).map(|_| ()),
Err(DemuxError::ParametersMissing(p)) if p.stream_index() == 9,
));
let real = Parameters::new();
let extra = TrackExtra::new(9, real).expect("real parameters");
extra.try_clone().expect("row copy");
extra.clone_parameters().expect("handoff");
},
);
}
#[cfg(feature = "resample")]
#[test]
fn a_spec_read_from_parameters_that_never_allocated_is_absent() {
crate::fault_subprocess::in_subprocess(
"demuxer::tests::a_spec_read_from_parameters_that_never_allocated_is_absent",
|| {
crate::fault_subprocess::cap_ffmpeg_allocations(1);
let never_allocated = Parameters::new();
crate::fault_subprocess::uncap_ffmpeg_allocations();
assert!(unsafe { never_allocated.as_ptr() }.is_null());
assert_eq!(
crate::ResampleSpec::from_parameters(&never_allocated),
None,
"parameters that do not exist describe no audio",
);
},
);
}
#[test]
fn codec_parameters_whose_copy_fails_are_named() {
crate::fault_subprocess::in_subprocess(
"demuxer::tests::codec_parameters_whose_copy_fails_are_named",
|| {
const EXTRADATA: usize = 8 * 1024 * 1024;
let mut source = Parameters::new();
unsafe {
let par = source.as_mut_ptr();
let extradata = ffmpeg_next::ffi::av_mallocz(EXTRADATA) as *mut u8;
assert!(!extradata.is_null(), "av_mallocz");
(*par).extradata = extradata;
(*par).extradata_size = EXTRADATA as i32;
}
crate::fault_subprocess::cap_ffmpeg_allocations(64 * 1024);
let refused = crate::extras::clone_parameters(&source, 2);
crate::fault_subprocess::uncap_ffmpeg_allocations();
match refused {
Err(DemuxError::ParametersCopy(p)) => assert_eq!(p.stream_index(), 2),
Err(other) => panic!("expected ParametersCopy, got {other:?}"),
Ok(_) => panic!("a copy that could not copy the extradata must not succeed"),
}
crate::extras::clone_parameters(&source, 2).expect("an uncapped copy");
},
);
}
#[test]
fn a_timed_thumbnail_stream_is_not_an_attachment() {
assert!(
is_attachment_disposition(AV_DISPOSITION_ATTACHED_PIC),
"a plain attached picture is still an attachment",
);
assert!(
!is_attachment_disposition(AV_DISPOSITION_ATTACHED_PIC | AV_DISPOSITION_TIMED_THUMBNAILS),
"a timed-thumbnail stream is a timed track, whatever else it is flagged",
);
assert!(!is_attachment_disposition(0));
assert!(!is_attachment_disposition(AV_DISPOSITION_TIMED_THUMBNAILS));
assert!(is_attachment_disposition(
AV_DISPOSITION_ATTACHED_PIC | ffmpeg_next::ffi::AV_DISPOSITION_DEFAULT
));
assert!(
ffmpeg_next::format::stream::Disposition::from_bits(AV_DISPOSITION_TIMED_THUMBNAILS)
.is_none(),
"ffmpeg_next mints no TIMED_THUMBNAILS bit — from_bits_truncate would drop it silently",
);
}
#[test]
fn an_uncapturable_cover_still_gets_its_one_packet() {
let empty: ffmpeg_next::ffi::AVPacket = unsafe { std::mem::zeroed() };
let packet = unsafe { attached_pic_payload(&empty, 7) }
.expect("an unparked cover is a degenerate track, not an unreadable file");
assert!(packet.data().as_ref().is_empty());
assert!(
packet.extra().synthesized(),
"nothing in the container handed this payload over",
);
assert_eq!(packet.extra().stream_index(), 7);
}
#[test]
fn a_zero_denominator_timebase_is_clamped_not_refused() {
let tb = rational_to_timebase(Rational::new(1, 0));
assert_eq!(tb.den().get(), 1);
assert_eq!(tb.num(), 1);
}
#[test]
fn a_declared_frame_rate_becomes_a_rate_shaped_timebase() {
let ntsc = rate_to_timebase(Rational::new(30_000, 1001)).expect("declared");
assert_eq!((ntsc.num(), ntsc.den().get()), (30_000, 1001));
assert_eq!(
rate_to_timebase(Rational::new(0, 1)),
None,
"0 fps is absent"
);
assert_eq!(
rate_to_timebase(Rational::new(30, 0)),
None,
"no denominator"
);
}
#[test]
fn the_seek_timebase_is_microseconds() {
let tb = av_time_base_q();
assert_eq!((tb.num(), tb.den().get()), (1, 1_000_000));
let target = Timestamp::new(1_500, Timebase::new(1, NonZeroI32::new(1000).expect("ms")));
assert_eq!(target.rescale_to(tb).pts(), 1_500_000);
}
}