Skip to main content

ez_ffmpeg/core/
stream_info.rs

1use std::collections::HashMap;
2use std::ffi::{CStr, CString};
3use std::ptr::{null, null_mut};
4
5#[cfg(not(feature = "docs-rs"))]
6use ffmpeg_sys_next::AVChannelOrder;
7use ffmpeg_sys_next::AVMediaType::{
8    AVMEDIA_TYPE_ATTACHMENT, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_DATA, AVMEDIA_TYPE_SUBTITLE,
9    AVMEDIA_TYPE_UNKNOWN, AVMEDIA_TYPE_VIDEO,
10};
11use ffmpeg_sys_next::{
12    av_dict_free, av_dict_get, av_dict_iterate, av_find_best_stream, avcodec_get_name,
13    avformat_find_stream_info, AVCodecID, AVDictionary, AVDictionaryEntry, AVRational,
14};
15use ffmpeg_sys_next::{avformat_alloc_context, avformat_close_input, avformat_open_input};
16use crate::core::context::AVFormatContextBox;
17use crate::error::{FindStreamError, OpenInputError, Result};
18
19#[derive(Debug, Clone)]
20pub enum StreamInfo {
21    /// Video stream information
22    Video {
23        // from AVStream
24        /// The index of the stream within the media file.
25        index: i32,
26
27        /// The time base for the stream, representing the unit of time for each frame or packet.
28        time_base: AVRational,
29
30        /// The start time of the stream, in `time_base` units.
31        start_time: i64,
32
33        /// The total duration of the stream, in `time_base` units.
34        duration: i64,
35
36        /// The total number of frames in the video stream.
37        nb_frames: i64,
38
39        /// The raw frame rate (frames per second) of the video stream, represented as a rational number.
40        r_frame_rate: AVRational,
41
42        /// The sample aspect ratio of the video frames, which represents the shape of individual pixels.
43        sample_aspect_ratio: AVRational,
44
45        /// Metadata associated with the video stream, such as title, language, etc.
46        metadata: HashMap<String, String>,
47
48        /// The average frame rate of the stream, potentially accounting for variable frame rates.
49        avg_frame_rate: AVRational,
50
51        // from AVCodecParameters
52        /// The codec identifier (e.g., `AV_CODEC_ID_H264`) used to decode the video stream.
53        codec_id: AVCodecID,
54
55        /// A human-readable name of the codec used for the video stream.
56        codec_name: String,
57
58        /// The width of the video frame in pixels.
59        width: i32,
60
61        /// The height of the video frame in pixels.
62        height: i32,
63
64        /// The bitrate of the video stream, measured in bits per second (bps).
65        bit_rate: i64,
66
67        /// The pixel format of the video stream (e.g., `AV_PIX_FMT_YUV420P`).
68        pixel_format: i32,
69
70        /// Delay introduced by the video codec, measured in frames.
71        video_delay: i32,
72
73        /// The frames per second (FPS) of the video stream, represented as a floating point number.
74        /// It is calculated from the `avg_framerate` field (avg_framerate.num / avg_framerate.den).
75        fps: f64,
76
77        /// The rotation of the video stream in degrees. This value is retrieved from the metadata.
78        /// Common values are 0, 90, 180, and 270.
79        rotate: i32,
80    },
81    /// Audio stream information
82    Audio {
83        // from AVStream
84        /// The index of the audio stream within the media file.
85        index: i32,
86
87        /// The time base for the stream, representing the unit of time for each audio packet.
88        time_base: AVRational,
89
90        /// The start time of the audio stream, in `time_base` units.
91        start_time: i64,
92
93        /// The total duration of the audio stream, in `time_base` units.
94        duration: i64,
95
96        /// The total number of frames in the audio stream.
97        nb_frames: i64,
98
99        /// Metadata associated with the audio stream, such as language, title, etc.
100        metadata: HashMap<String, String>,
101
102        /// The average frame rate of the audio stream, which might not always be applicable for audio streams.
103        avg_frame_rate: AVRational,
104
105        // from AVCodecParameters
106        /// The codec identifier used to decode the audio stream (e.g., `AV_CODEC_ID_AAC`).
107        codec_id: AVCodecID,
108
109        /// A human-readable name of the codec used for the audio stream.
110        codec_name: String,
111
112        /// The audio sample rate, measured in samples per second (Hz).
113        sample_rate: i32,
114
115        /// Channel order used in this layout.
116        #[cfg(not(feature = "docs-rs"))]
117        order: AVChannelOrder,
118
119        /// Number of channels in this layout.
120        nb_channels: i32,
121
122        /// The bitrate of the audio stream, measured in bits per second (bps).
123        bit_rate: i64,
124
125        /// The format of the audio samples (e.g., `AV_SAMPLE_FMT_FLTP` for planar float samples).
126        sample_format: i32,
127
128        /// The size of each audio frame, typically representing the number of samples per channel in one frame.
129        frame_size: i32,
130    },
131    /// Subtitle stream information
132    Subtitle {
133        // from AVStream
134        /// The index of the subtitle stream within the media file.
135        index: i32,
136
137        /// The time base for the stream, representing the unit of time for each subtitle event.
138        time_base: AVRational,
139
140        /// The start time of the subtitle stream, in `time_base` units.
141        start_time: i64,
142
143        /// The total duration of the subtitle stream, in `time_base` units.
144        duration: i64,
145
146        /// The total number of subtitle events in the stream.
147        nb_frames: i64,
148
149        /// Metadata associated with the subtitle stream, such as language.
150        metadata: HashMap<String, String>,
151
152        // from AVCodecParameters
153        /// The codec identifier used to decode the subtitle stream (e.g., `AV_CODEC_ID_ASS`).
154        codec_id: AVCodecID,
155
156        /// A human-readable name of the codec used for the subtitle stream.
157        codec_name: String,
158    },
159    /// Data stream information
160    Data {
161        // From AVStream
162        /// The index of the data stream within the media file.
163        index: i32,
164
165        /// The time base for the data stream, representing the unit of time for each data packet.
166        time_base: AVRational,
167
168        /// The start time of the data stream, in `time_base` units.
169        start_time: i64,
170
171        /// The total duration of the data stream, in `time_base` units.
172        duration: i64,
173
174        /// Metadata associated with the data stream, such as additional information about the stream content.
175        metadata: HashMap<String, String>,
176    },
177    /// Attachment stream information
178    Attachment {
179        // From AVStream
180        /// The index of the attachment stream within the media file.
181        index: i32,
182
183        /// Metadata associated with the attachment stream, such as details about the attached file.
184        metadata: HashMap<String, String>,
185
186        // From AVCodecParameters
187        /// The codec identifier used to decode the attachment stream (e.g., `AV_CODEC_ID_PNG` for images).
188        codec_id: AVCodecID,
189
190        /// A human-readable name of the codec used for the attachment stream.
191        codec_name: String,
192    },
193    /// Unknown or unrecognized stream type.
194    ///
195    /// Returned when the codec type does not match any known media type
196    /// (video, audio, subtitle, data, attachment) or when `codecpar` is null.
197    Unknown {
198        /// The index of the unknown stream within the media file.
199        index: i32,
200
201        /// Metadata associated with the unknown stream.
202        metadata: HashMap<String, String>,
203    },
204}
205
206impl StreamInfo {
207    /// Returns a human-readable label for this stream's type
208    /// (e.g. `"Video"`, `"Audio"`, `"Unknown"`).
209    pub fn stream_type(&self) -> &'static str {
210        match self {
211            StreamInfo::Video { .. } => "Video",
212            StreamInfo::Audio { .. } => "Audio",
213            StreamInfo::Subtitle { .. } => "Subtitle",
214            StreamInfo::Data { .. } => "Data",
215            StreamInfo::Attachment { .. } => "Attachment",
216            StreamInfo::Unknown { .. } => "Unknown",
217        }
218    }
219
220    /// Returns `true` if this is a video stream.
221    pub fn is_video(&self) -> bool {
222        matches!(self, StreamInfo::Video { .. })
223    }
224
225    /// Returns `true` if this is an audio stream.
226    pub fn is_audio(&self) -> bool {
227        matches!(self, StreamInfo::Audio { .. })
228    }
229
230    /// Returns the stream index within the media file.
231    pub fn index(&self) -> i32 {
232        match self {
233            StreamInfo::Video { index, .. }
234            | StreamInfo::Audio { index, .. }
235            | StreamInfo::Subtitle { index, .. }
236            | StreamInfo::Data { index, .. }
237            | StreamInfo::Attachment { index, .. }
238            | StreamInfo::Unknown { index, .. } => *index,
239        }
240    }
241}
242
243/// Extracts a `StreamInfo` from a single raw `AVStream` pointer.
244///
245/// # Safety
246/// The caller must ensure `raw_stream` is a valid, non-null pointer to an `AVStream`.
247unsafe fn extract_stream_info_from_stream(raw_stream: *mut ffmpeg_sys_next::AVStream) -> StreamInfo {
248    let stream = &*raw_stream;
249    let metadata = dict_to_hashmap(stream.metadata);
250
251    if stream.codecpar.is_null() {
252        return StreamInfo::Unknown {
253            index: stream.index,
254            metadata,
255        };
256    }
257
258    let codecpar = &*stream.codecpar;
259    let codec_id = codecpar.codec_id;
260    let codec_name = codec_name(codec_id);
261
262    let index = stream.index;
263    let time_base = stream.time_base;
264    let start_time = stream.start_time;
265    let duration = stream.duration;
266    let nb_frames = stream.nb_frames;
267    let avg_frame_rate = stream.avg_frame_rate;
268
269    match codecpar.codec_type {
270        AVMEDIA_TYPE_VIDEO => {
271            let width = codecpar.width;
272            let height = codecpar.height;
273            let bit_rate = codecpar.bit_rate;
274            let pixel_format = codecpar.format;
275            let video_delay = codecpar.video_delay;
276            let r_frame_rate = stream.r_frame_rate;
277            let sample_aspect_ratio = stream.sample_aspect_ratio;
278            let fps = if avg_frame_rate.den == 0 {
279                0.0
280            } else {
281                avg_frame_rate.num as f64 / avg_frame_rate.den as f64
282            };
283            let rotate = metadata
284                .get("rotate")
285                .and_then(|rotate| rotate.parse::<i32>().ok())
286                .unwrap_or(0);
287
288            StreamInfo::Video {
289                index,
290                time_base,
291                start_time,
292                duration,
293                nb_frames,
294                r_frame_rate,
295                sample_aspect_ratio,
296                metadata,
297                avg_frame_rate,
298                codec_id,
299                codec_name,
300                width,
301                height,
302                bit_rate,
303                pixel_format,
304                video_delay,
305                fps,
306                rotate,
307            }
308        }
309        AVMEDIA_TYPE_AUDIO => {
310            let sample_rate = codecpar.sample_rate;
311            #[cfg(not(feature = "docs-rs"))]
312            let ch_layout = codecpar.ch_layout;
313            let sample_format = codecpar.format;
314            let frame_size = codecpar.frame_size;
315            let bit_rate = codecpar.bit_rate;
316
317            StreamInfo::Audio {
318                index,
319                time_base,
320                start_time,
321                duration,
322                nb_frames,
323                metadata,
324                avg_frame_rate,
325                codec_id,
326                codec_name,
327                sample_rate,
328                #[cfg(not(feature = "docs-rs"))]
329                order: ch_layout.order,
330                #[cfg(feature = "docs-rs")]
331                nb_channels: 0,
332                #[cfg(not(feature = "docs-rs"))]
333                nb_channels: ch_layout.nb_channels,
334                bit_rate,
335                sample_format,
336                frame_size,
337            }
338        }
339        AVMEDIA_TYPE_SUBTITLE => StreamInfo::Subtitle {
340            index,
341            time_base,
342            start_time,
343            duration,
344            nb_frames,
345            metadata,
346            codec_id,
347            codec_name,
348        },
349        AVMEDIA_TYPE_DATA => StreamInfo::Data {
350            index,
351            time_base,
352            start_time,
353            duration,
354            metadata,
355        },
356        AVMEDIA_TYPE_ATTACHMENT => StreamInfo::Attachment {
357            index,
358            metadata,
359            codec_id,
360            codec_name,
361        },
362        _ => StreamInfo::Unknown { index, metadata },
363    }
364}
365
366/// Extracts `StreamInfo` for all streams in the given format context.
367///
368/// Returns an error if the streams pointer is null (when `nb_streams > 0`)
369/// or if all streams are of unknown type.
370///
371/// # Safety
372/// The caller must ensure `fmt_ctx_box` holds a valid, fully-initialized
373/// `AVFormatContext` (i.e. `avformat_open_input` + `avformat_find_stream_info`
374/// have succeeded).
375pub(crate) unsafe fn extract_stream_infos(fmt_ctx_box: &AVFormatContextBox) -> Result<Vec<StreamInfo>> {
376    let fmt_ctx = fmt_ctx_box.fmt_ctx;
377    if fmt_ctx.is_null() {
378        return Err(OpenInputError::OutOfMemory.into());
379    }
380    let nb_streams = (*fmt_ctx).nb_streams as usize;
381    let streams_ptr = (*fmt_ctx).streams;
382
383    if nb_streams > 0 && streams_ptr.is_null() {
384        return Err(FindStreamError::NoStreamFound.into());
385    }
386
387    let mut infos = Vec::with_capacity(nb_streams);
388
389    for i in 0..nb_streams {
390        let raw_stream = *streams_ptr.add(i);
391        if raw_stream.is_null() {
392            infos.push(StreamInfo::Unknown {
393                index: i as i32,
394                metadata: HashMap::new(),
395            });
396            continue;
397        }
398        infos.push(extract_stream_info_from_stream(raw_stream));
399    }
400
401    if !infos.is_empty() && infos.iter().all(|i| matches!(i, StreamInfo::Unknown { .. })) {
402        return Err(FindStreamError::NoStreamFound.into());
403    }
404
405    Ok(infos)
406}
407
408/// Finds the best stream of the given media type and extracts its `StreamInfo`.
409///
410/// This is the shared implementation for all `find_*_stream_info` functions.
411/// It opens the file, calls `av_find_best_stream`, validates the returned index,
412/// and delegates extraction to `extract_stream_info_from_stream`.
413fn find_best_stream_info(
414    url: impl Into<String>,
415    media_type: ffmpeg_sys_next::AVMediaType,
416) -> Result<Option<StreamInfo>> {
417    let in_fmt_ctx_box = init_format_context(url)?;
418
419    // SAFETY: in_fmt_ctx_box holds a valid AVFormatContext from init_format_context.
420    // We bounds-check best_index against nb_streams and null-check streams_ptr
421    // before dereferencing.
422    unsafe {
423        let best_index = av_find_best_stream(
424            in_fmt_ctx_box.fmt_ctx,
425            media_type,
426            -1,
427            -1,
428            null_mut(),
429            0,
430        );
431        if best_index < 0 {
432            return Ok(None);
433        }
434
435        let nb_streams = (*in_fmt_ctx_box.fmt_ctx).nb_streams as usize;
436        let index = best_index as usize;
437        if index >= nb_streams {
438            return Err(FindStreamError::NoStreamFound.into());
439        }
440
441        let streams_ptr = (*in_fmt_ctx_box.fmt_ctx).streams;
442        if streams_ptr.is_null() {
443            return Err(FindStreamError::NoStreamFound.into());
444        }
445
446        let raw_stream = *streams_ptr.add(index);
447        if raw_stream.is_null() {
448            return Err(FindStreamError::NoStreamFound.into());
449        }
450
451        let info = extract_stream_info_from_stream(raw_stream);
452        // If codecpar was null, extract returns Unknown instead of the requested type.
453        // Only filter Unknown when the caller asked for a specific (non-Unknown) type.
454        if media_type != AVMEDIA_TYPE_UNKNOWN && matches!(info, StreamInfo::Unknown { .. }) {
455            return Ok(None);
456        }
457        Ok(Some(info))
458    }
459}
460
461/// Retrieves video stream information from a given media URL.
462///
463/// This function opens the media file or stream specified by the URL and
464/// searches for the best video stream. If a video stream is found, it
465/// returns the relevant metadata and codec parameters wrapped in a
466/// `StreamInfo::Video` enum variant.
467///
468/// # Parameters
469/// - `url`: The URL or file path of the media file to analyze.
470///
471/// # Returns
472/// - `Ok(Some(StreamInfo::Video))`: Contains the video stream information if found.
473/// - `Ok(None)`: Returned if no video stream is found.
474/// - `Err`: If an error occurs during the operation (e.g., file cannot be opened or stream information cannot be found).
475pub fn find_video_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
476    find_best_stream_info(url, AVMEDIA_TYPE_VIDEO)
477}
478
479/// Retrieves audio stream information from a given media URL.
480///
481/// This function opens the media file or stream specified by the URL and
482/// searches for the best audio stream. If an audio stream is found, it
483/// returns the relevant metadata and codec parameters wrapped in a
484/// `StreamInfo::Audio` enum variant.
485///
486/// # Parameters
487/// - `url`: The URL or file path of the media file to analyze.
488///
489/// # Returns
490/// - `Ok(Some(StreamInfo::Audio))`: Contains the audio stream information if found.
491/// - `Ok(None)`: Returned if no audio stream is found.
492/// - `Err`: If an error occurs during the operation (e.g., file cannot be opened or stream information cannot be found).
493pub fn find_audio_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
494    find_best_stream_info(url, AVMEDIA_TYPE_AUDIO)
495}
496
497/// Retrieves subtitle stream information from a given media URL.
498///
499/// This function opens the media file or stream specified by the URL and
500/// searches for the best subtitle stream. If a subtitle stream is found, it
501/// returns the relevant metadata and codec parameters wrapped in a
502/// `StreamInfo::Subtitle` enum variant. It also attempts to retrieve any
503/// language information from the stream metadata.
504///
505/// # Parameters
506/// - `url`: The URL or file path of the media file to analyze.
507///
508/// # Returns
509/// - `Ok(Some(StreamInfo::Subtitle))`: Contains the subtitle stream information if found.
510/// - `Ok(None)`: Returned if no subtitle stream is found.
511/// - `Err`: If an error occurs during the operation (e.g., file cannot be opened or stream information cannot be found).
512pub fn find_subtitle_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
513    find_best_stream_info(url, AVMEDIA_TYPE_SUBTITLE)
514}
515
516/// Finds the data stream information from the given media URL.
517///
518/// This function opens the media file or stream specified by the URL and
519/// searches for a data stream (`AVMEDIA_TYPE_DATA`). It returns relevant metadata
520/// wrapped in a `StreamInfo::Data` enum variant.
521///
522/// # Parameters
523/// - `url`: The URL or file path of the media file.
524///
525/// # Returns
526/// - `Ok(Some(StreamInfo::Data))`: Contains the data stream information if found.
527/// - `Ok(None)`: Returned if no data stream is found.
528/// - `Err`: If an error occurs during the operation.
529pub fn find_data_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
530    find_best_stream_info(url, AVMEDIA_TYPE_DATA)
531}
532
533/// Finds the attachment stream information from the given media URL.
534///
535/// This function opens the media file or stream specified by the URL and
536/// searches for an attachment stream (`AVMEDIA_TYPE_ATTACHMENT`). It returns
537/// relevant metadata and codec information wrapped in a `StreamInfo::Attachment`
538/// enum variant.
539///
540/// # Parameters
541/// - `url`: The URL or file path of the media file.
542///
543/// # Returns
544/// - `Ok(Some(StreamInfo::Attachment))`: Contains the attachment stream information if found.
545/// - `Ok(None)`: Returned if no attachment stream is found.
546/// - `Err`: If an error occurs during the operation.
547pub fn find_attachment_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
548    find_best_stream_info(url, AVMEDIA_TYPE_ATTACHMENT)
549}
550
551/// Finds the unknown stream information from the given media URL.
552///
553/// This function opens the media file or stream specified by the URL and
554/// searches for any unknown stream (`AVMEDIA_TYPE_UNKNOWN`). It returns
555/// relevant metadata wrapped in a `StreamInfo::Unknown` enum variant.
556///
557/// # Parameters
558/// - `url`: The URL or file path of the media file.
559///
560/// # Returns
561/// - `Ok(Some(StreamInfo::Unknown))`: Contains the unknown stream information if found.
562/// - `Ok(None)`: Returned if no unknown stream is found.
563/// - `Err`: If an error occurs during the operation.
564pub fn find_unknown_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
565    find_best_stream_info(url, AVMEDIA_TYPE_UNKNOWN)
566}
567
568/// Retrieves information for all streams (video, audio, subtitle, etc.) from a given media URL.
569///
570/// This function opens the media file or stream specified by the URL and
571/// retrieves information for all available streams (e.g., video, audio, subtitles).
572/// The information for each stream is wrapped in a corresponding `StreamInfo` enum
573/// variant and collected into a `Vec<StreamInfo>`.
574///
575/// # Parameters
576/// - `url`: The URL or file path of the media file to analyze.
577///
578/// # Returns
579/// - `Ok(Vec<StreamInfo>)`: A vector containing information for all detected streams.
580/// - `Err`: If an error occurs during the operation (e.g., file cannot be opened or stream information cannot be found).
581pub fn find_all_stream_infos(url: impl Into<String>) -> Result<Vec<StreamInfo>> {
582    let in_fmt_ctx_box = init_format_context(url)?;
583    // SAFETY: in_fmt_ctx_box is fully initialized by init_format_context.
584    unsafe { extract_stream_infos(&in_fmt_ctx_box) }
585}
586
587#[inline]
588fn codec_name(id: AVCodecID) -> String {
589    // SAFETY: avcodec_get_name is a pure lookup that returns a static string
590    // pointer for any AVCodecID value. We null-check before dereferencing.
591    unsafe {
592        let ptr = avcodec_get_name(id);
593        if ptr.is_null() {
594            "Unknown codec".into()
595        } else {
596            CStr::from_ptr(ptr).to_string_lossy().into_owned()
597        }
598    }
599}
600
601pub(crate) fn init_format_context(url: impl Into<String>) -> Result<AVFormatContextBox> {
602    crate::core::initialize_ffmpeg();
603
604    // Convert URL before allocating FFmpeg resources so a NUL-byte error
605    // cannot leak the AVFormatContext.
606    let url_cstr = CString::new(url.into())?;
607
608    // SAFETY: All FFmpeg allocations are paired with their cleanup on every
609    // error path (avformat_close_input). avformat_open_input takes ownership
610    // of in_fmt_ctx on success; on failure it sets in_fmt_ctx to null.
611    unsafe {
612        let mut in_fmt_ctx = avformat_alloc_context();
613        if in_fmt_ctx.is_null() {
614            return Err(OpenInputError::OutOfMemory.into());
615        }
616
617        let mut format_opts = null_mut();
618        let scan_all_pmts_key = CString::new("scan_all_pmts")?;
619        if av_dict_get(
620            format_opts,
621            scan_all_pmts_key.as_ptr(),
622            null(),
623            ffmpeg_sys_next::AV_DICT_MATCH_CASE,
624        )
625        .is_null()
626        {
627            let scan_all_pmts_value = CString::new("1")?;
628            ffmpeg_sys_next::av_dict_set(
629                &mut format_opts,
630                scan_all_pmts_key.as_ptr(),
631                scan_all_pmts_value.as_ptr(),
632                ffmpeg_sys_next::AV_DICT_DONT_OVERWRITE,
633            );
634        };
635
636        #[cfg(not(feature = "docs-rs"))]
637        let mut ret =
638            { avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), &mut format_opts) };
639        #[cfg(feature = "docs-rs")]
640        let mut ret = 0;
641
642        // Free leftover options not consumed by avformat_open_input.
643        av_dict_free(&mut format_opts);
644
645        if ret < 0 {
646            avformat_close_input(&mut in_fmt_ctx);
647            return Err(OpenInputError::from(ret).into());
648        }
649
650        ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
651        if ret < 0 {
652            avformat_close_input(&mut in_fmt_ctx);
653            return Err(FindStreamError::from(ret).into());
654        }
655
656        Ok(AVFormatContextBox::new(in_fmt_ctx, true, false))
657    }
658}
659
660fn dict_to_hashmap(dict: *mut AVDictionary) -> HashMap<String, String> {
661    if dict.is_null() {
662        return HashMap::new();
663    }
664    let mut map = HashMap::new();
665    // SAFETY: dict is non-null (checked above). av_dict_iterate returns
666    // entries with valid key/value C strings until it returns null.
667    unsafe {
668        let mut e: *const AVDictionaryEntry = null_mut();
669        while {
670            e = av_dict_iterate(dict, e);
671            !e.is_null()
672        } {
673            let k = CStr::from_ptr((*e).key).to_string_lossy().into_owned();
674            let v = CStr::from_ptr((*e).value).to_string_lossy().into_owned();
675            map.insert(k, v);
676        }
677    }
678    map
679}
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684
685    #[test]
686    fn test_not_found() {
687        let result = find_all_stream_infos("not_found.mp4");
688        assert!(result.is_err());
689
690        let error = result.err().unwrap();
691        println!("{error}");
692        assert!(matches!(
693            error,
694            crate::error::Error::OpenInputStream(OpenInputError::NotFound)
695        ))
696    }
697
698    #[test]
699    fn test_find_all_stream_infos() {
700        let stream_infos = find_all_stream_infos("test.mp4").unwrap();
701        assert_eq!(2, stream_infos.len());
702        for stream_info in stream_infos {
703            println!("{:?}", stream_info);
704        }
705    }
706
707    #[test]
708    fn test_find_video_stream_info() {
709        let option = find_video_stream_info("test.mp4").unwrap();
710        assert!(option.is_some());
711        let video_stream_info = option.unwrap();
712        println!("video_stream_info:{:?}", video_stream_info);
713    }
714
715    #[test]
716    fn test_find_audio_stream_info() {
717        let option = find_audio_stream_info("test.mp4").unwrap();
718        assert!(option.is_some());
719        let audio_stream_info = option.unwrap();
720        println!("audio_stream_info:{:?}", audio_stream_info);
721    }
722
723    #[test]
724    fn test_find_subtitle_stream_info() {
725        let option = find_subtitle_stream_info("test.mp4").unwrap();
726        assert!(option.is_none())
727    }
728
729    #[test]
730    fn test_find_data_stream_info() {
731        let option = find_data_stream_info("test.mp4").unwrap();
732        assert!(option.is_none());
733    }
734
735    #[test]
736    fn test_find_attachment_stream_info() {
737        let option = find_attachment_stream_info("test.mp4").unwrap();
738        assert!(option.is_none())
739    }
740
741    #[test]
742    fn test_find_unknown_stream_info() {
743        let option = find_unknown_stream_info("test.mp4").unwrap();
744        assert!(option.is_none())
745    }
746
747    #[test]
748    fn test_is_video() {
749        let video = StreamInfo::Video {
750            index: 0, time_base: AVRational { num: 1, den: 30 },
751            start_time: 0, duration: 100, nb_frames: 100,
752            r_frame_rate: AVRational { num: 30, den: 1 },
753            sample_aspect_ratio: AVRational { num: 1, den: 1 },
754            avg_frame_rate: AVRational { num: 30, den: 1 },
755            width: 1920, height: 1080, bit_rate: 0, pixel_format: 0,
756            video_delay: 0, fps: 30.0, rotate: 0,
757            codec_id: AVCodecID::AV_CODEC_ID_H264,
758            codec_name: "h264".to_string(), metadata: HashMap::new(),
759        };
760        let unknown = StreamInfo::Unknown { index: 1, metadata: HashMap::new() };
761        assert!(video.is_video());
762        assert!(!video.is_audio());
763        assert!(!unknown.is_video());
764    }
765
766    #[test]
767    fn test_is_audio() {
768        let audio = StreamInfo::Audio {
769            index: 1, time_base: AVRational { num: 1, den: 44100 },
770            start_time: 0, duration: 100, nb_frames: 0,
771            avg_frame_rate: AVRational { num: 0, den: 1 },
772            sample_rate: 44100,
773            #[cfg(not(feature = "docs-rs"))]
774            order: AVChannelOrder::AV_CHANNEL_ORDER_UNSPEC,
775            nb_channels: 2, bit_rate: 128000, sample_format: 0, frame_size: 1024,
776            codec_id: AVCodecID::AV_CODEC_ID_AAC,
777            codec_name: "aac".to_string(), metadata: HashMap::new(),
778        };
779        assert!(audio.is_audio());
780        assert!(!audio.is_video());
781    }
782
783    #[test]
784    fn test_index() {
785        let video = StreamInfo::Video {
786            index: 5, time_base: AVRational { num: 1, den: 30 },
787            start_time: 0, duration: 100, nb_frames: 100,
788            r_frame_rate: AVRational { num: 30, den: 1 },
789            sample_aspect_ratio: AVRational { num: 1, den: 1 },
790            avg_frame_rate: AVRational { num: 30, den: 1 },
791            width: 1920, height: 1080, bit_rate: 0, pixel_format: 0,
792            video_delay: 0, fps: 30.0, rotate: 0,
793            codec_id: AVCodecID::AV_CODEC_ID_H264,
794            codec_name: "h264".to_string(), metadata: HashMap::new(),
795        };
796        let unknown = StreamInfo::Unknown { index: 42, metadata: HashMap::new() };
797        assert_eq!(video.index(), 5);
798        assert_eq!(unknown.index(), 42);
799    }
800}