use ffmpeg_sys_next::{
av_packet_alloc, av_packet_free, av_packet_unref, av_read_frame, avformat_seek_file, AVPacket,
AVERROR, AV_PKT_FLAG_CORRUPT, AV_PKT_FLAG_KEY, EAGAIN,
};
use std::iter::FusedIterator;
use crate::core::stream_info::StreamInfo;
use crate::error::{DemuxingError, OpenInputError, PacketScannerError, Result};
use crate::raw::FormatContext;
#[derive(Debug, Clone)]
pub struct PacketInfo {
stream_index: usize,
pts: Option<i64>,
dts: Option<i64>,
duration: i64,
size: usize,
pos: i64,
is_keyframe: bool,
is_corrupt: bool,
is_video: bool,
is_audio: bool,
data: Option<std::sync::Arc<[u8]>>,
}
impl PacketInfo {
pub fn stream_index(&self) -> usize {
self.stream_index
}
pub fn pts(&self) -> Option<i64> {
self.pts
}
pub fn dts(&self) -> Option<i64> {
self.dts
}
pub fn duration(&self) -> i64 {
self.duration
}
pub fn size(&self) -> usize {
self.size
}
pub fn pos(&self) -> i64 {
self.pos
}
pub fn is_keyframe(&self) -> bool {
self.is_keyframe
}
pub fn is_corrupt(&self) -> bool {
self.is_corrupt
}
pub fn is_video(&self) -> bool {
self.is_video
}
pub fn is_audio(&self) -> bool {
self.is_audio
}
pub fn data(&self) -> Option<&[u8]> {
self.data.as_deref()
}
}
pub struct PacketScanner {
fmt_ctx_box: FormatContext,
pkt: *mut AVPacket,
streams: Vec<StreamInfo>,
capture_data: bool,
}
unsafe impl Send for PacketScanner {}
impl PacketScanner {
pub fn open(url: impl Into<String>) -> Result<Self> {
let fmt_ctx_box = crate::core::stream_info::init_format_context(url)?;
let streams = unsafe { crate::core::stream_info::extract_stream_infos(&fmt_ctx_box)? };
unsafe {
let pkt = av_packet_alloc();
if pkt.is_null() {
return Err(OpenInputError::OutOfMemory.into());
}
Ok(Self {
fmt_ctx_box,
pkt,
streams,
capture_data: false,
})
}
}
pub fn set_capture_data(&mut self, capture: bool) {
self.capture_data = capture;
}
pub fn seek(&mut self, timestamp_us: i64) -> Result<()> {
unsafe {
let ret = avformat_seek_file(
self.fmt_ctx_box.as_ptr(),
-1,
i64::MIN,
timestamp_us,
timestamp_us,
0,
);
if ret < 0 {
return Err(PacketScannerError::SeekError(DemuxingError::from(ret)).into());
}
}
Ok(())
}
pub fn next_packet(&mut self) -> Result<Option<PacketInfo>> {
const MAX_EAGAIN_RETRIES: u32 = 500;
unsafe {
av_packet_unref(self.pkt);
let mut eagain_retries: u32 = 0;
loop {
let ret = av_read_frame(self.fmt_ctx_box.as_ptr(), self.pkt);
if ret == AVERROR(EAGAIN) {
eagain_retries += 1;
if eagain_retries > MAX_EAGAIN_RETRIES {
return Err(PacketScannerError::ReadError(DemuxingError::from(ret)).into());
}
std::thread::sleep(std::time::Duration::from_millis(10));
continue;
}
if ret < 0 {
if ret == ffmpeg_sys_next::AVERROR_EOF {
return Ok(None);
}
return Err(PacketScannerError::ReadError(DemuxingError::from(ret)).into());
}
break;
}
let pkt = &*self.pkt;
let pts = if pkt.pts == ffmpeg_sys_next::AV_NOPTS_VALUE {
None
} else {
Some(pkt.pts)
};
let dts = if pkt.dts == ffmpeg_sys_next::AV_NOPTS_VALUE {
None
} else {
Some(pkt.dts)
};
let stream_index = pkt.stream_index.max(0) as usize;
let (is_video, is_audio) = self
.streams
.get(stream_index)
.map(|s| (s.is_video(), s.is_audio()))
.unwrap_or((false, false));
Ok(Some(PacketInfo {
stream_index,
pts,
dts,
duration: pkt.duration,
size: {
debug_assert!(pkt.size >= 0, "negative pkt.size: {}", pkt.size);
pkt.size.max(0) as usize
},
pos: pkt.pos,
is_keyframe: (pkt.flags & AV_PKT_FLAG_KEY) != 0,
is_corrupt: (pkt.flags & AV_PKT_FLAG_CORRUPT) != 0,
is_video,
is_audio,
data: (self.capture_data && !pkt.data.is_null() && pkt.size > 0).then(|| {
std::sync::Arc::from(std::slice::from_raw_parts(
pkt.data,
pkt.size as usize,
))
}),
}))
}
}
pub fn streams(&self) -> &[StreamInfo] {
&self.streams
}
pub fn video_stream(&self) -> Option<&StreamInfo> {
self.streams.iter().find(|s| s.is_video())
}
pub fn audio_stream(&self) -> Option<&StreamInfo> {
self.streams.iter().find(|s| s.is_audio())
}
pub fn stream_for_packet(&self, packet: &PacketInfo) -> Option<&StreamInfo> {
self.streams.get(packet.stream_index())
}
pub fn packets(&mut self) -> PacketIter<'_> {
PacketIter {
scanner: self,
done: false,
}
}
}
impl Drop for PacketScanner {
fn drop(&mut self) {
unsafe {
if !self.pkt.is_null() {
av_packet_free(&mut self.pkt);
}
}
}
}
pub struct PacketIter<'a> {
scanner: &'a mut PacketScanner,
done: bool,
}
impl<'a> Iterator for PacketIter<'a> {
type Item = Result<PacketInfo>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
match self.scanner.next_packet() {
Ok(Some(info)) => Some(Ok(info)),
Ok(None) => {
self.done = true;
None
}
Err(e) => {
self.done = true;
Some(Err(e))
}
}
}
}
impl<'a> FusedIterator for PacketIter<'a> {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_open_not_found() {
let result = PacketScanner::open("not_found.mp4");
assert!(result.is_err());
}
#[test]
fn test_scan_packets() {
let mut scanner = PacketScanner::open("test.mp4").unwrap();
let mut count = 0;
let mut keyframes = 0;
for packet in scanner.packets() {
let info = packet.unwrap();
count += 1;
if info.is_keyframe() {
keyframes += 1;
}
}
assert!(count > 0, "expected at least one packet");
assert!(keyframes > 0, "expected at least one keyframe");
println!("total packets: {}, keyframes: {}", count, keyframes);
}
#[test]
fn capture_data_defaults_off_and_copies_payloads_when_enabled() {
let mut scanner = PacketScanner::open("test.mp4").unwrap();
let mut sizes = Vec::new();
for packet in scanner.packets() {
let info = packet.unwrap();
assert!(
info.data().is_none(),
"a capture-off scan must not copy payloads"
);
sizes.push(info.size());
}
assert!(!sizes.is_empty(), "expected packets in the fixture");
fn capture_scan() -> Vec<PacketInfo> {
let mut scanner = PacketScanner::open("test.mp4").unwrap();
scanner.set_capture_data(true);
scanner.packets().map(|p| p.unwrap()).collect()
}
let first = capture_scan();
let second = capture_scan();
assert_eq!(first.len(), sizes.len(), "both scans see the same packets");
assert_eq!(
second.len(),
first.len(),
"repeated capture scans see the same packets"
);
for (i, (a, b)) in first.iter().zip(&second).enumerate() {
let data_a = a
.data()
.expect("a capture-on scan must carry the payload");
assert!(!data_a.is_empty(), "packet {i}: captured payload is empty");
assert_eq!(
data_a.len(),
a.size(),
"packet {i}: captured length must equal the packet size"
);
assert_eq!(
a.size(),
sizes[i],
"packet {i}: size differs from the capture-off scan"
);
let data_b = b
.data()
.expect("a capture-on rescan must carry the payload");
assert_eq!(
data_b.len(),
b.size(),
"packet {i}: rescan captured length must equal the packet size"
);
assert!(
data_a == data_b,
"packet {i}: payload bytes differ across two scans"
);
}
}
#[test]
fn test_seek_and_read() {
let mut scanner = PacketScanner::open("test.mp4").unwrap();
scanner.seek(1_000_000).unwrap();
let packet = scanner.next_packet().unwrap();
assert!(packet.is_some(), "expected a packet after seeking");
}
#[test]
fn test_streams() {
let scanner = PacketScanner::open("test.mp4").unwrap();
let streams = scanner.streams();
assert!(!streams.is_empty(), "expected at least one stream");
assert_eq!(
streams.len(),
2,
"test.mp4 should have 2 streams (video + audio)"
);
}
#[test]
fn test_video_stream() {
let scanner = PacketScanner::open("test.mp4").unwrap();
let video = scanner.video_stream();
assert!(video.is_some(), "expected a video stream");
assert!(video.unwrap().is_video());
}
#[test]
fn test_audio_stream() {
let scanner = PacketScanner::open("test.mp4").unwrap();
let audio = scanner.audio_stream();
assert!(audio.is_some(), "expected an audio stream");
assert!(audio.unwrap().is_audio());
}
#[test]
fn test_stream_for_packet() {
let mut scanner = PacketScanner::open("test.mp4").unwrap();
let packet = scanner.next_packet().unwrap();
assert!(packet.is_some(), "expected at least one packet");
let info = packet.unwrap();
let stream = scanner.stream_for_packet(&info);
assert!(
stream.is_some(),
"stream_for_packet should return Some for valid packet"
);
}
}