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