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/// Rotation in whole degrees from the stream-level display matrix, or None
244/// when the stream carries none.
245///
246/// # Safety
247/// The caller must ensure `codecpar` points into a live `AVStream`.
248unsafe fn display_matrix_rotation(
249    codecpar: &ffmpeg_sys_next::AVCodecParameters,
250) -> Option<i32> {
251    if codecpar.coded_side_data.is_null() || codecpar.nb_coded_side_data <= 0 {
252        return None;
253    }
254    let side_data = std::slice::from_raw_parts(
255        codecpar.coded_side_data,
256        codecpar.nb_coded_side_data as usize,
257    );
258    crate::core::display::rotation_from_side_data(side_data)
259}
260
261/// Extracts a `StreamInfo` from a single raw `AVStream` pointer.
262///
263/// # Safety
264/// The caller must ensure `raw_stream` is a valid, non-null pointer to an `AVStream`.
265unsafe fn extract_stream_info_from_stream(raw_stream: *mut ffmpeg_sys_next::AVStream) -> StreamInfo {
266    let stream = &*raw_stream;
267    let metadata = dict_to_hashmap(stream.metadata);
268
269    if stream.codecpar.is_null() {
270        return StreamInfo::Unknown {
271            index: stream.index,
272            metadata,
273        };
274    }
275
276    let codecpar = &*stream.codecpar;
277    let codec_id = codecpar.codec_id;
278    let codec_name = codec_name(codec_id);
279
280    let index = stream.index;
281    let time_base = stream.time_base;
282    let start_time = stream.start_time;
283    let duration = stream.duration;
284    let nb_frames = stream.nb_frames;
285    let avg_frame_rate = stream.avg_frame_rate;
286
287    match codecpar.codec_type {
288        AVMEDIA_TYPE_VIDEO => {
289            let width = codecpar.width;
290            let height = codecpar.height;
291            let bit_rate = codecpar.bit_rate;
292            let pixel_format = codecpar.format;
293            let video_delay = codecpar.video_delay;
294            let r_frame_rate = stream.r_frame_rate;
295            let sample_aspect_ratio = stream.sample_aspect_ratio;
296            let fps = if avg_frame_rate.den == 0 {
297                0.0
298            } else {
299                avg_frame_rate.num as f64 / avg_frame_rate.den as f64
300            };
301            // Modern demuxers (mov.c, matroskadec.c) export rotation only as
302            // an AV_PKT_DATA_DISPLAYMATRIX entry in coded_side_data; the
303            // metadata "rotate" tag survives as a fallback for nonstandard
304            // containers (matches fftools get_rotation, cmdutils.c:1475).
305            let rotate = display_matrix_rotation(codecpar)
306                .or_else(|| {
307                    metadata
308                        .get("rotate")
309                        .and_then(|rotate| rotate.parse::<i32>().ok())
310                })
311                .unwrap_or(0);
312
313            StreamInfo::Video {
314                index,
315                time_base,
316                start_time,
317                duration,
318                nb_frames,
319                r_frame_rate,
320                sample_aspect_ratio,
321                metadata,
322                avg_frame_rate,
323                codec_id,
324                codec_name,
325                width,
326                height,
327                bit_rate,
328                pixel_format,
329                video_delay,
330                fps,
331                rotate,
332            }
333        }
334        AVMEDIA_TYPE_AUDIO => {
335            let sample_rate = codecpar.sample_rate;
336            #[cfg(not(feature = "docs-rs"))]
337            let ch_layout = codecpar.ch_layout;
338            let sample_format = codecpar.format;
339            let frame_size = codecpar.frame_size;
340            let bit_rate = codecpar.bit_rate;
341
342            StreamInfo::Audio {
343                index,
344                time_base,
345                start_time,
346                duration,
347                nb_frames,
348                metadata,
349                avg_frame_rate,
350                codec_id,
351                codec_name,
352                sample_rate,
353                #[cfg(not(feature = "docs-rs"))]
354                order: ch_layout.order,
355                #[cfg(feature = "docs-rs")]
356                nb_channels: 0,
357                #[cfg(not(feature = "docs-rs"))]
358                nb_channels: ch_layout.nb_channels,
359                bit_rate,
360                sample_format,
361                frame_size,
362            }
363        }
364        AVMEDIA_TYPE_SUBTITLE => StreamInfo::Subtitle {
365            index,
366            time_base,
367            start_time,
368            duration,
369            nb_frames,
370            metadata,
371            codec_id,
372            codec_name,
373        },
374        AVMEDIA_TYPE_DATA => StreamInfo::Data {
375            index,
376            time_base,
377            start_time,
378            duration,
379            metadata,
380        },
381        AVMEDIA_TYPE_ATTACHMENT => StreamInfo::Attachment {
382            index,
383            metadata,
384            codec_id,
385            codec_name,
386        },
387        _ => StreamInfo::Unknown { index, metadata },
388    }
389}
390
391/// Extracts `StreamInfo` for all streams in the given format context.
392///
393/// Returns an error if the streams pointer is null (when `nb_streams > 0`)
394/// or if all streams are of unknown type.
395///
396/// # Safety
397/// The caller must ensure `fmt_ctx_box` holds a valid, fully-initialized
398/// `AVFormatContext` (i.e. `avformat_open_input` + `avformat_find_stream_info`
399/// have succeeded).
400pub(crate) unsafe fn extract_stream_infos(fmt_ctx_box: &AVFormatContextBox) -> Result<Vec<StreamInfo>> {
401    let fmt_ctx = fmt_ctx_box.fmt_ctx;
402    if fmt_ctx.is_null() {
403        return Err(OpenInputError::OutOfMemory.into());
404    }
405    let nb_streams = (*fmt_ctx).nb_streams as usize;
406    let streams_ptr = (*fmt_ctx).streams;
407
408    if nb_streams > 0 && streams_ptr.is_null() {
409        return Err(FindStreamError::NoStreamFound.into());
410    }
411
412    let mut infos = Vec::with_capacity(nb_streams);
413
414    for i in 0..nb_streams {
415        let raw_stream = *streams_ptr.add(i);
416        if raw_stream.is_null() {
417            infos.push(StreamInfo::Unknown {
418                index: i as i32,
419                metadata: HashMap::new(),
420            });
421            continue;
422        }
423        infos.push(extract_stream_info_from_stream(raw_stream));
424    }
425
426    if !infos.is_empty() && infos.iter().all(|i| matches!(i, StreamInfo::Unknown { .. })) {
427        return Err(FindStreamError::NoStreamFound.into());
428    }
429
430    Ok(infos)
431}
432
433/// Finds the best stream of the given media type and extracts its `StreamInfo`.
434///
435/// This is the shared implementation for all `find_*_stream_info` functions.
436/// It opens the file, calls `av_find_best_stream`, validates the returned index,
437/// and delegates extraction to `extract_stream_info_from_stream`.
438fn find_best_stream_info(
439    url: impl Into<String>,
440    media_type: ffmpeg_sys_next::AVMediaType,
441) -> Result<Option<StreamInfo>> {
442    let in_fmt_ctx_box = init_format_context(url)?;
443
444    // SAFETY: in_fmt_ctx_box holds a valid AVFormatContext from init_format_context.
445    // We bounds-check best_index against nb_streams and null-check streams_ptr
446    // before dereferencing.
447    unsafe {
448        let best_index = av_find_best_stream(
449            in_fmt_ctx_box.fmt_ctx,
450            media_type,
451            -1,
452            -1,
453            null_mut(),
454            0,
455        );
456        if best_index < 0 {
457            return Ok(None);
458        }
459
460        let nb_streams = (*in_fmt_ctx_box.fmt_ctx).nb_streams as usize;
461        let index = best_index as usize;
462        if index >= nb_streams {
463            return Err(FindStreamError::NoStreamFound.into());
464        }
465
466        let streams_ptr = (*in_fmt_ctx_box.fmt_ctx).streams;
467        if streams_ptr.is_null() {
468            return Err(FindStreamError::NoStreamFound.into());
469        }
470
471        let raw_stream = *streams_ptr.add(index);
472        if raw_stream.is_null() {
473            return Err(FindStreamError::NoStreamFound.into());
474        }
475
476        let info = extract_stream_info_from_stream(raw_stream);
477        // If codecpar was null, extract returns Unknown instead of the requested type.
478        // Only filter Unknown when the caller asked for a specific (non-Unknown) type.
479        if media_type != AVMEDIA_TYPE_UNKNOWN && matches!(info, StreamInfo::Unknown { .. }) {
480            return Ok(None);
481        }
482        Ok(Some(info))
483    }
484}
485
486/// Retrieves video stream information from a given media URL.
487///
488/// This function opens the media file or stream specified by the URL and
489/// searches for the best video stream. If a video stream is found, it
490/// returns the relevant metadata and codec parameters wrapped in a
491/// `StreamInfo::Video` enum variant.
492///
493/// # Parameters
494/// - `url`: The URL or file path of the media file to analyze.
495///
496/// # Returns
497/// - `Ok(Some(StreamInfo::Video))`: Contains the video stream information if found.
498/// - `Ok(None)`: Returned if no video stream is found.
499/// - `Err`: If an error occurs during the operation (e.g., file cannot be opened or stream information cannot be found).
500pub fn find_video_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
501    find_best_stream_info(url, AVMEDIA_TYPE_VIDEO)
502}
503
504/// Retrieves audio stream information from a given media URL.
505///
506/// This function opens the media file or stream specified by the URL and
507/// searches for the best audio stream. If an audio stream is found, it
508/// returns the relevant metadata and codec parameters wrapped in a
509/// `StreamInfo::Audio` enum variant.
510///
511/// # Parameters
512/// - `url`: The URL or file path of the media file to analyze.
513///
514/// # Returns
515/// - `Ok(Some(StreamInfo::Audio))`: Contains the audio stream information if found.
516/// - `Ok(None)`: Returned if no audio stream is found.
517/// - `Err`: If an error occurs during the operation (e.g., file cannot be opened or stream information cannot be found).
518pub fn find_audio_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
519    find_best_stream_info(url, AVMEDIA_TYPE_AUDIO)
520}
521
522/// Retrieves subtitle stream information from a given media URL.
523///
524/// This function opens the media file or stream specified by the URL and
525/// searches for the best subtitle stream. If a subtitle stream is found, it
526/// returns the relevant metadata and codec parameters wrapped in a
527/// `StreamInfo::Subtitle` enum variant. It also attempts to retrieve any
528/// language information from the stream metadata.
529///
530/// # Parameters
531/// - `url`: The URL or file path of the media file to analyze.
532///
533/// # Returns
534/// - `Ok(Some(StreamInfo::Subtitle))`: Contains the subtitle stream information if found.
535/// - `Ok(None)`: Returned if no subtitle stream is found.
536/// - `Err`: If an error occurs during the operation (e.g., file cannot be opened or stream information cannot be found).
537pub fn find_subtitle_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
538    find_best_stream_info(url, AVMEDIA_TYPE_SUBTITLE)
539}
540
541/// Finds the data stream information from the given media URL.
542///
543/// This function opens the media file or stream specified by the URL and
544/// searches for a data stream (`AVMEDIA_TYPE_DATA`). It returns relevant metadata
545/// wrapped in a `StreamInfo::Data` enum variant.
546///
547/// # Parameters
548/// - `url`: The URL or file path of the media file.
549///
550/// # Returns
551/// - `Ok(Some(StreamInfo::Data))`: Contains the data stream information if found.
552/// - `Ok(None)`: Returned if no data stream is found.
553/// - `Err`: If an error occurs during the operation.
554pub fn find_data_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
555    find_best_stream_info(url, AVMEDIA_TYPE_DATA)
556}
557
558/// Finds the attachment stream information from the given media URL.
559///
560/// This function opens the media file or stream specified by the URL and
561/// searches for an attachment stream (`AVMEDIA_TYPE_ATTACHMENT`). It returns
562/// relevant metadata and codec information wrapped in a `StreamInfo::Attachment`
563/// enum variant.
564///
565/// # Parameters
566/// - `url`: The URL or file path of the media file.
567///
568/// # Returns
569/// - `Ok(Some(StreamInfo::Attachment))`: Contains the attachment stream information if found.
570/// - `Ok(None)`: Returned if no attachment stream is found.
571/// - `Err`: If an error occurs during the operation.
572pub fn find_attachment_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
573    find_best_stream_info(url, AVMEDIA_TYPE_ATTACHMENT)
574}
575
576/// Finds the unknown stream information from the given media URL.
577///
578/// This function opens the media file or stream specified by the URL and
579/// searches for any unknown stream (`AVMEDIA_TYPE_UNKNOWN`). It returns
580/// relevant metadata wrapped in a `StreamInfo::Unknown` enum variant.
581///
582/// # Parameters
583/// - `url`: The URL or file path of the media file.
584///
585/// # Returns
586/// - `Ok(Some(StreamInfo::Unknown))`: Contains the unknown stream information if found.
587/// - `Ok(None)`: Returned if no unknown stream is found.
588/// - `Err`: If an error occurs during the operation.
589pub fn find_unknown_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
590    find_best_stream_info(url, AVMEDIA_TYPE_UNKNOWN)
591}
592
593/// Retrieves information for all streams (video, audio, subtitle, etc.) from a given media URL.
594///
595/// This function opens the media file or stream specified by the URL and
596/// retrieves information for all available streams (e.g., video, audio, subtitles).
597/// The information for each stream is wrapped in a corresponding `StreamInfo` enum
598/// variant and collected into a `Vec<StreamInfo>`.
599///
600/// # Parameters
601/// - `url`: The URL or file path of the media file to analyze.
602///
603/// # Returns
604/// - `Ok(Vec<StreamInfo>)`: A vector containing information for all detected streams.
605/// - `Err`: If an error occurs during the operation (e.g., file cannot be opened or stream information cannot be found).
606pub fn find_all_stream_infos(url: impl Into<String>) -> Result<Vec<StreamInfo>> {
607    let in_fmt_ctx_box = init_format_context(url)?;
608    // SAFETY: in_fmt_ctx_box is fully initialized by init_format_context.
609    unsafe { extract_stream_infos(&in_fmt_ctx_box) }
610}
611
612#[inline]
613fn codec_name(id: AVCodecID) -> String {
614    // SAFETY: avcodec_get_name is a pure lookup that returns a static string
615    // pointer for any AVCodecID value. We null-check before dereferencing.
616    unsafe {
617        let ptr = avcodec_get_name(id);
618        if ptr.is_null() {
619            "Unknown codec".into()
620        } else {
621            CStr::from_ptr(ptr).to_string_lossy().into_owned()
622        }
623    }
624}
625
626pub(crate) fn init_format_context(url: impl Into<String>) -> Result<AVFormatContextBox> {
627    crate::core::initialize_ffmpeg();
628
629    // Convert URL before allocating FFmpeg resources so a NUL-byte error
630    // cannot leak the AVFormatContext.
631    let url_cstr = CString::new(url.into())?;
632
633    // SAFETY: All FFmpeg allocations are paired with their cleanup on every
634    // error path (avformat_close_input). avformat_open_input takes ownership
635    // of in_fmt_ctx on success; on failure it sets in_fmt_ctx to null.
636    unsafe {
637        let mut in_fmt_ctx = avformat_alloc_context();
638        if in_fmt_ctx.is_null() {
639            return Err(OpenInputError::OutOfMemory.into());
640        }
641
642        let mut format_opts = null_mut();
643        let scan_all_pmts_key = CString::new("scan_all_pmts")?;
644        if av_dict_get(
645            format_opts,
646            scan_all_pmts_key.as_ptr(),
647            null(),
648            ffmpeg_sys_next::AV_DICT_MATCH_CASE,
649        )
650        .is_null()
651        {
652            let scan_all_pmts_value = CString::new("1")?;
653            ffmpeg_sys_next::av_dict_set(
654                &mut format_opts,
655                scan_all_pmts_key.as_ptr(),
656                scan_all_pmts_value.as_ptr(),
657                ffmpeg_sys_next::AV_DICT_DONT_OVERWRITE,
658            );
659        };
660
661        #[cfg(not(feature = "docs-rs"))]
662        let mut ret =
663            { avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), &mut format_opts) };
664        #[cfg(feature = "docs-rs")]
665        let mut ret = 0;
666
667        // Free leftover options not consumed by avformat_open_input.
668        av_dict_free(&mut format_opts);
669
670        if ret < 0 {
671            avformat_close_input(&mut in_fmt_ctx);
672            return Err(OpenInputError::from(ret).into());
673        }
674
675        ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
676        if ret < 0 {
677            avformat_close_input(&mut in_fmt_ctx);
678            return Err(FindStreamError::from(ret).into());
679        }
680
681        Ok(AVFormatContextBox::new(in_fmt_ctx, true, false))
682    }
683}
684
685fn dict_to_hashmap(dict: *mut AVDictionary) -> HashMap<String, String> {
686    if dict.is_null() {
687        return HashMap::new();
688    }
689    let mut map = HashMap::new();
690    // SAFETY: dict is non-null (checked above). av_dict_iterate returns
691    // entries with valid key/value C strings until it returns null.
692    unsafe {
693        let mut e: *const AVDictionaryEntry = null_mut();
694        while {
695            e = av_dict_iterate(dict, e);
696            !e.is_null()
697        } {
698            let k = CStr::from_ptr((*e).key).to_string_lossy().into_owned();
699            let v = CStr::from_ptr((*e).value).to_string_lossy().into_owned();
700            map.insert(k, v);
701        }
702    }
703    map
704}
705
706#[cfg(test)]
707mod tests {
708    use super::*;
709
710    #[test]
711    fn test_not_found() {
712        let result = find_all_stream_infos("not_found.mp4");
713        assert!(result.is_err());
714
715        let error = result.err().unwrap();
716        println!("{error}");
717        assert!(matches!(
718            error,
719            crate::error::Error::OpenInputStream(OpenInputError::NotFound)
720        ))
721    }
722
723    #[test]
724    fn test_find_all_stream_infos() {
725        let stream_infos = find_all_stream_infos("test.mp4").unwrap();
726        assert_eq!(2, stream_infos.len());
727        for stream_info in stream_infos {
728            println!("{:?}", stream_info);
729        }
730    }
731
732    #[test]
733    fn test_find_video_stream_info() {
734        let option = find_video_stream_info("test.mp4").unwrap();
735        assert!(option.is_some());
736        let video_stream_info = option.unwrap();
737        println!("video_stream_info:{:?}", video_stream_info);
738    }
739
740    #[test]
741    fn test_find_audio_stream_info() {
742        let option = find_audio_stream_info("test.mp4").unwrap();
743        assert!(option.is_some());
744        let audio_stream_info = option.unwrap();
745        println!("audio_stream_info:{:?}", audio_stream_info);
746    }
747
748    #[test]
749    fn test_find_subtitle_stream_info() {
750        let option = find_subtitle_stream_info("test.mp4").unwrap();
751        assert!(option.is_none())
752    }
753
754    #[test]
755    fn test_find_data_stream_info() {
756        let option = find_data_stream_info("test.mp4").unwrap();
757        assert!(option.is_none());
758    }
759
760    #[test]
761    fn test_find_attachment_stream_info() {
762        let option = find_attachment_stream_info("test.mp4").unwrap();
763        assert!(option.is_none())
764    }
765
766    #[test]
767    fn test_find_unknown_stream_info() {
768        let option = find_unknown_stream_info("test.mp4").unwrap();
769        assert!(option.is_none())
770    }
771
772    #[test]
773    fn rotate_reads_display_matrix_side_data() {
774        use ffmpeg_sys_next::AVPacketSideDataType::AV_PKT_DATA_DISPLAYMATRIX;
775        use ffmpeg_sys_next::{
776            av_display_rotation_set, av_malloc, av_packet_side_data_add,
777            avformat_alloc_context, avformat_free_context, avformat_new_stream,
778        };
779
780        unsafe {
781            let mut fmt_ctx = avformat_alloc_context();
782            assert!(!fmt_ctx.is_null());
783            let stream = avformat_new_stream(fmt_ctx, null());
784            assert!(!stream.is_null());
785            let codecpar = (*stream).codecpar;
786            (*codecpar).codec_type = AVMEDIA_TYPE_VIDEO;
787
788            // iPhone portrait: the display matrix says "rotate 90 degrees
789            // clockwise to show upright" — the legacy metadata tag for the
790            // same file was rotate=90. No metadata is set here: modern
791            // demuxers only export the matrix.
792            let matrix = av_malloc(36);
793            assert!(!matrix.is_null());
794            av_display_rotation_set(matrix as *mut i32, 90.0);
795            let added = av_packet_side_data_add(
796                &mut (*codecpar).coded_side_data,
797                &mut (*codecpar).nb_coded_side_data,
798                AV_PKT_DATA_DISPLAYMATRIX,
799                matrix,
800                36,
801                0,
802            );
803            assert!(!added.is_null());
804
805            let info = extract_stream_info_from_stream(stream);
806            match info {
807                StreamInfo::Video { rotate, .. } => assert_eq!(
808                    rotate, 90,
809                    "rotate must come from the display matrix side data"
810                ),
811                other => panic!("expected video stream info, got {other:?}"),
812            }
813
814            avformat_free_context(fmt_ctx);
815            fmt_ctx = null_mut();
816            let _ = fmt_ctx;
817        }
818    }
819
820    #[test]
821    fn test_is_video() {
822        let video = StreamInfo::Video {
823            index: 0, time_base: AVRational { num: 1, den: 30 },
824            start_time: 0, duration: 100, nb_frames: 100,
825            r_frame_rate: AVRational { num: 30, den: 1 },
826            sample_aspect_ratio: AVRational { num: 1, den: 1 },
827            avg_frame_rate: AVRational { num: 30, den: 1 },
828            width: 1920, height: 1080, bit_rate: 0, pixel_format: 0,
829            video_delay: 0, fps: 30.0, rotate: 0,
830            codec_id: AVCodecID::AV_CODEC_ID_H264,
831            codec_name: "h264".to_string(), metadata: HashMap::new(),
832        };
833        let unknown = StreamInfo::Unknown { index: 1, metadata: HashMap::new() };
834        assert!(video.is_video());
835        assert!(!video.is_audio());
836        assert!(!unknown.is_video());
837    }
838
839    #[test]
840    fn test_is_audio() {
841        let audio = StreamInfo::Audio {
842            index: 1, time_base: AVRational { num: 1, den: 44100 },
843            start_time: 0, duration: 100, nb_frames: 0,
844            avg_frame_rate: AVRational { num: 0, den: 1 },
845            sample_rate: 44100,
846            #[cfg(not(feature = "docs-rs"))]
847            order: AVChannelOrder::AV_CHANNEL_ORDER_UNSPEC,
848            nb_channels: 2, bit_rate: 128000, sample_format: 0, frame_size: 1024,
849            codec_id: AVCodecID::AV_CODEC_ID_AAC,
850            codec_name: "aac".to_string(), metadata: HashMap::new(),
851        };
852        assert!(audio.is_audio());
853        assert!(!audio.is_video());
854    }
855
856    #[test]
857    fn test_index() {
858        let video = StreamInfo::Video {
859            index: 5, time_base: AVRational { num: 1, den: 30 },
860            start_time: 0, duration: 100, nb_frames: 100,
861            r_frame_rate: AVRational { num: 30, den: 1 },
862            sample_aspect_ratio: AVRational { num: 1, den: 1 },
863            avg_frame_rate: AVRational { num: 30, den: 1 },
864            width: 1920, height: 1080, bit_rate: 0, pixel_format: 0,
865            video_delay: 0, fps: 30.0, rotate: 0,
866            codec_id: AVCodecID::AV_CODEC_ID_H264,
867            codec_name: "h264".to_string(), metadata: HashMap::new(),
868        };
869        let unknown = StreamInfo::Unknown { index: 42, metadata: HashMap::new() };
870        assert_eq!(video.index(), 5);
871        assert_eq!(unknown.index(), 42);
872    }
873}