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