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