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