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