ez_ffmpeg/core/
stream_info.rs

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