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