Skip to main content

ff_format/
media.rs

1//! Media container information.
2//!
3//! This module provides the [`MediaInfo`] struct for representing metadata about
4//! a media file as a whole, including all its video and audio streams.
5//!
6//! # Examples
7//!
8//! ```
9//! use ff_format::media::{MediaInfo, MediaInfoBuilder};
10//! use ff_format::stream::{VideoStreamInfo, AudioStreamInfo};
11//! use ff_format::{PixelFormat, SampleFormat, Rational};
12//! use ff_format::codec::{VideoCodec, AudioCodec};
13//! use std::time::Duration;
14//! use std::path::PathBuf;
15//!
16//! // Create video stream info
17//! let video = VideoStreamInfo::builder()
18//!     .index(0)
19//!     .codec(VideoCodec::H264)
20//!     .width(1920)
21//!     .height(1080)
22//!     .frame_rate(Rational::new(30, 1))
23//!     .pixel_format(PixelFormat::Yuv420p)
24//!     .build();
25//!
26//! // Create audio stream info
27//! let audio = AudioStreamInfo::builder()
28//!     .index(1)
29//!     .codec(AudioCodec::Aac)
30//!     .sample_rate(48000)
31//!     .channels(2)
32//!     .sample_format(SampleFormat::F32)
33//!     .build();
34//!
35//! // Create media info
36//! let media = MediaInfo::builder()
37//!     .path("/path/to/video.mp4")
38//!     .format("mp4")
39//!     .format_long_name("QuickTime / MOV")
40//!     .duration(Duration::from_secs(120))
41//!     .file_size(1_000_000)
42//!     .bitrate(8_000_000)
43//!     .video_stream(video)
44//!     .audio_stream(audio)
45//!     .metadata("title", "Sample Video")
46//!     .metadata("artist", "Test Artist")
47//!     .build();
48//!
49//! assert!(media.has_video());
50//! assert!(media.has_audio());
51//! assert_eq!(media.resolution(), Some((1920, 1080)));
52//! ```
53
54use std::collections::HashMap;
55use std::path::{Path, PathBuf};
56use std::time::Duration;
57
58use crate::chapter::ChapterInfo;
59use crate::stream::{AudioStreamInfo, SubtitleStreamInfo, VideoStreamInfo};
60
61/// Information about a media file.
62///
63/// This struct contains all metadata about a media container, including
64/// format information, duration, file size, and all contained streams.
65///
66/// # Construction
67///
68/// Use [`MediaInfo::builder()`] for fluent construction:
69///
70/// ```
71/// use ff_format::media::MediaInfo;
72/// use std::time::Duration;
73///
74/// let info = MediaInfo::builder()
75///     .path("/path/to/video.mp4")
76///     .format("mp4")
77///     .duration(Duration::from_secs(120))
78///     .file_size(1_000_000)
79///     .build();
80/// ```
81#[derive(Debug, Clone)]
82pub struct MediaInfo {
83    /// File path
84    path: PathBuf,
85    /// Container format name (e.g., "mp4", "mkv", "avi")
86    format: String,
87    /// Long format name from the container format description
88    format_long_name: Option<String>,
89    /// Total duration
90    duration: Duration,
91    /// File size in bytes
92    file_size: u64,
93    /// Overall bitrate in bits per second
94    bitrate: Option<u64>,
95    /// Video streams in the file
96    video_streams: Vec<VideoStreamInfo>,
97    /// Audio streams in the file
98    audio_streams: Vec<AudioStreamInfo>,
99    /// Subtitle streams in the file
100    subtitle_streams: Vec<SubtitleStreamInfo>,
101    /// Chapter markers in the file
102    chapters: Vec<ChapterInfo>,
103    /// File metadata (title, artist, etc.)
104    metadata: HashMap<String, String>,
105}
106
107impl MediaInfo {
108    /// Creates a new builder for constructing `MediaInfo`.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// use ff_format::media::MediaInfo;
114    /// use std::time::Duration;
115    ///
116    /// let info = MediaInfo::builder()
117    ///     .path("/path/to/video.mp4")
118    ///     .format("mp4")
119    ///     .duration(Duration::from_secs(120))
120    ///     .file_size(1_000_000)
121    ///     .build();
122    /// ```
123    #[must_use]
124    pub fn builder() -> MediaInfoBuilder {
125        MediaInfoBuilder::default()
126    }
127
128    /// Returns the file path.
129    #[must_use]
130    #[inline]
131    pub fn path(&self) -> &Path {
132        &self.path
133    }
134
135    /// Returns the container format name.
136    #[must_use]
137    #[inline]
138    pub fn format(&self) -> &str {
139        &self.format
140    }
141
142    /// Returns the long format name, if available.
143    #[must_use]
144    #[inline]
145    pub fn format_long_name(&self) -> Option<&str> {
146        self.format_long_name.as_deref()
147    }
148
149    /// Returns the total duration.
150    #[must_use]
151    #[inline]
152    pub const fn duration(&self) -> Duration {
153        self.duration
154    }
155
156    /// Returns the file size in bytes.
157    #[must_use]
158    #[inline]
159    pub const fn file_size(&self) -> u64 {
160        self.file_size
161    }
162
163    /// Returns the overall bitrate in bits per second, if known.
164    #[must_use]
165    #[inline]
166    pub const fn bitrate(&self) -> Option<u64> {
167        self.bitrate
168    }
169
170    /// Returns all video streams in the file.
171    #[must_use]
172    #[inline]
173    pub fn video_streams(&self) -> &[VideoStreamInfo] {
174        &self.video_streams
175    }
176
177    /// Returns all audio streams in the file.
178    #[must_use]
179    #[inline]
180    pub fn audio_streams(&self) -> &[AudioStreamInfo] {
181        &self.audio_streams
182    }
183
184    /// Returns all subtitle streams in the file.
185    #[must_use]
186    #[inline]
187    pub fn subtitle_streams(&self) -> &[SubtitleStreamInfo] {
188        &self.subtitle_streams
189    }
190
191    /// Returns all chapters in the file.
192    #[must_use]
193    #[inline]
194    pub fn chapters(&self) -> &[ChapterInfo] {
195        &self.chapters
196    }
197
198    /// Returns `true` if the file contains at least one chapter marker.
199    #[must_use]
200    #[inline]
201    pub fn has_chapters(&self) -> bool {
202        !self.chapters.is_empty()
203    }
204
205    /// Returns the number of chapters.
206    #[must_use]
207    #[inline]
208    pub fn chapter_count(&self) -> usize {
209        self.chapters.len()
210    }
211
212    /// Returns the file metadata.
213    #[must_use]
214    #[inline]
215    pub fn metadata(&self) -> &HashMap<String, String> {
216        &self.metadata
217    }
218
219    /// Returns a specific metadata value by key.
220    #[must_use]
221    #[inline]
222    pub fn metadata_value(&self, key: &str) -> Option<&str> {
223        self.metadata.get(key).map(String::as_str)
224    }
225
226    // === Stream Query Methods ===
227
228    /// Returns `true` if the file contains at least one video stream.
229    #[must_use]
230    #[inline]
231    pub fn has_video(&self) -> bool {
232        !self.video_streams.is_empty()
233    }
234
235    /// Returns `true` if the file contains at least one audio stream.
236    #[must_use]
237    #[inline]
238    pub fn has_audio(&self) -> bool {
239        !self.audio_streams.is_empty()
240    }
241
242    /// Returns `true` if the file contains at least one subtitle stream.
243    #[must_use]
244    #[inline]
245    pub fn has_subtitles(&self) -> bool {
246        !self.subtitle_streams.is_empty()
247    }
248
249    /// Returns the number of video streams.
250    #[must_use]
251    #[inline]
252    pub fn video_stream_count(&self) -> usize {
253        self.video_streams.len()
254    }
255
256    /// Returns the number of audio streams.
257    #[must_use]
258    #[inline]
259    pub fn audio_stream_count(&self) -> usize {
260        self.audio_streams.len()
261    }
262
263    /// Returns the number of subtitle streams.
264    #[must_use]
265    #[inline]
266    pub fn subtitle_stream_count(&self) -> usize {
267        self.subtitle_streams.len()
268    }
269
270    /// Returns the total number of streams (video + audio + subtitle).
271    #[must_use]
272    #[inline]
273    pub fn stream_count(&self) -> usize {
274        self.video_streams.len() + self.audio_streams.len() + self.subtitle_streams.len()
275    }
276
277    // === Primary Stream Selection ===
278
279    /// Returns the primary video stream.
280    ///
281    /// The primary video stream is the first video stream in the file.
282    /// Returns `None` if there are no video streams.
283    #[must_use]
284    #[inline]
285    pub fn primary_video(&self) -> Option<&VideoStreamInfo> {
286        self.video_streams.first()
287    }
288
289    /// Returns the primary audio stream.
290    ///
291    /// The primary audio stream is the first audio stream in the file.
292    /// Returns `None` if there are no audio streams.
293    #[must_use]
294    #[inline]
295    pub fn primary_audio(&self) -> Option<&AudioStreamInfo> {
296        self.audio_streams.first()
297    }
298
299    /// Returns a video stream by index within the video streams list.
300    #[must_use]
301    #[inline]
302    pub fn video_stream(&self, index: usize) -> Option<&VideoStreamInfo> {
303        self.video_streams.get(index)
304    }
305
306    /// Returns an audio stream by index within the audio streams list.
307    #[must_use]
308    #[inline]
309    pub fn audio_stream(&self, index: usize) -> Option<&AudioStreamInfo> {
310        self.audio_streams.get(index)
311    }
312
313    /// Returns a subtitle stream by index within the subtitle streams list.
314    #[must_use]
315    #[inline]
316    pub fn subtitle_stream(&self, index: usize) -> Option<&SubtitleStreamInfo> {
317        self.subtitle_streams.get(index)
318    }
319
320    // === Convenience Methods ===
321
322    /// Returns the resolution of the primary video stream.
323    ///
324    /// Returns `None` if there are no video streams.
325    #[must_use]
326    #[inline]
327    pub fn resolution(&self) -> Option<(u32, u32)> {
328        self.primary_video().map(|v| (v.width(), v.height()))
329    }
330
331    /// Returns the frame rate of the primary video stream.
332    ///
333    /// Returns `None` if there are no video streams.
334    #[must_use]
335    #[inline]
336    pub fn frame_rate(&self) -> Option<f64> {
337        self.primary_video().map(VideoStreamInfo::fps)
338    }
339
340    /// Returns the sample rate of the primary audio stream.
341    ///
342    /// Returns `None` if there are no audio streams.
343    #[must_use]
344    #[inline]
345    pub fn sample_rate(&self) -> Option<u32> {
346        self.primary_audio().map(AudioStreamInfo::sample_rate)
347    }
348
349    /// Returns the channel count of the primary audio stream.
350    ///
351    /// Returns `None` if there are no audio streams.
352    ///
353    /// The type is `u32` to match `FFmpeg` and professional audio APIs. For `rodio`
354    /// or `cpal` (which require `u16`), cast with `.map(|c| c as u16)` — channel
355    /// counts never exceed `u16::MAX` in practice.
356    #[must_use]
357    #[inline]
358    pub fn channels(&self) -> Option<u32> {
359        self.primary_audio().map(AudioStreamInfo::channels)
360    }
361
362    /// Returns `true` if this is a video-only file (no audio streams).
363    #[must_use]
364    #[inline]
365    pub fn is_video_only(&self) -> bool {
366        self.has_video() && !self.has_audio()
367    }
368
369    /// Returns `true` if this is an audio-only file (no video streams).
370    #[must_use]
371    #[inline]
372    pub fn is_audio_only(&self) -> bool {
373        self.has_audio() && !self.has_video()
374    }
375
376    /// Returns the file name (without directory path).
377    #[must_use]
378    #[inline]
379    pub fn file_name(&self) -> Option<&str> {
380        self.path.file_name().and_then(|n| n.to_str())
381    }
382
383    /// Returns the file extension.
384    #[must_use]
385    #[inline]
386    pub fn extension(&self) -> Option<&str> {
387        self.path.extension().and_then(|e| e.to_str())
388    }
389
390    // === Common Metadata Accessors ===
391
392    /// Returns the title from metadata.
393    ///
394    /// This is a convenience method for accessing the "title" metadata key,
395    /// which is commonly used by media containers to store the title of the content.
396    #[must_use]
397    #[inline]
398    pub fn title(&self) -> Option<&str> {
399        self.metadata_value("title")
400    }
401
402    /// Returns the artist from metadata.
403    ///
404    /// This is a convenience method for accessing the "artist" metadata key.
405    #[must_use]
406    #[inline]
407    pub fn artist(&self) -> Option<&str> {
408        self.metadata_value("artist")
409    }
410
411    /// Returns the album from metadata.
412    ///
413    /// This is a convenience method for accessing the "album" metadata key.
414    #[must_use]
415    #[inline]
416    pub fn album(&self) -> Option<&str> {
417        self.metadata_value("album")
418    }
419
420    /// Returns the creation time from metadata.
421    ///
422    /// This is a convenience method for accessing the `creation_time` metadata key,
423    /// which is commonly used by media containers to store when the file was created.
424    /// The format is typically ISO 8601 (e.g., `2024-01-15T10:30:00.000000Z`).
425    #[must_use]
426    #[inline]
427    pub fn creation_time(&self) -> Option<&str> {
428        self.metadata_value("creation_time")
429    }
430
431    /// Returns the date from metadata.
432    ///
433    /// This is a convenience method for accessing the "date" metadata key.
434    #[must_use]
435    #[inline]
436    pub fn date(&self) -> Option<&str> {
437        self.metadata_value("date")
438    }
439
440    /// Returns the comment from metadata.
441    ///
442    /// This is a convenience method for accessing the "comment" metadata key.
443    #[must_use]
444    #[inline]
445    pub fn comment(&self) -> Option<&str> {
446        self.metadata_value("comment")
447    }
448
449    /// Returns the encoder from metadata.
450    ///
451    /// This is a convenience method for accessing the "encoder" metadata key,
452    /// which stores information about the software used to create the file.
453    #[must_use]
454    #[inline]
455    pub fn encoder(&self) -> Option<&str> {
456        self.metadata_value("encoder")
457    }
458}
459
460impl Default for MediaInfo {
461    fn default() -> Self {
462        Self {
463            path: PathBuf::new(),
464            format: String::new(),
465            format_long_name: None,
466            duration: Duration::ZERO,
467            file_size: 0,
468            bitrate: None,
469            video_streams: Vec::new(),
470            audio_streams: Vec::new(),
471            subtitle_streams: Vec::new(),
472            chapters: Vec::new(),
473            metadata: HashMap::new(),
474        }
475    }
476}
477
478/// Builder for constructing [`MediaInfo`].
479///
480/// # Examples
481///
482/// ```
483/// use ff_format::media::MediaInfo;
484/// use std::time::Duration;
485///
486/// let info = MediaInfo::builder()
487///     .path("/path/to/video.mp4")
488///     .format("mp4")
489///     .format_long_name("QuickTime / MOV")
490///     .duration(Duration::from_secs(120))
491///     .file_size(1_000_000)
492///     .bitrate(8_000_000)
493///     .metadata("title", "Sample Video")
494///     .build();
495/// ```
496#[derive(Debug, Clone, Default)]
497pub struct MediaInfoBuilder {
498    path: PathBuf,
499    format: String,
500    format_long_name: Option<String>,
501    duration: Duration,
502    file_size: u64,
503    bitrate: Option<u64>,
504    video_streams: Vec<VideoStreamInfo>,
505    audio_streams: Vec<AudioStreamInfo>,
506    subtitle_streams: Vec<SubtitleStreamInfo>,
507    chapters: Vec<ChapterInfo>,
508    metadata: HashMap<String, String>,
509}
510
511impl MediaInfoBuilder {
512    /// Sets the file path.
513    #[must_use]
514    pub fn path(mut self, path: impl Into<PathBuf>) -> Self {
515        self.path = path.into();
516        self
517    }
518
519    /// Sets the container format name.
520    #[must_use]
521    pub fn format(mut self, format: impl Into<String>) -> Self {
522        self.format = format.into();
523        self
524    }
525
526    /// Sets the long format name.
527    #[must_use]
528    pub fn format_long_name(mut self, name: impl Into<String>) -> Self {
529        self.format_long_name = Some(name.into());
530        self
531    }
532
533    /// Sets the total duration.
534    #[must_use]
535    pub fn duration(mut self, duration: Duration) -> Self {
536        self.duration = duration;
537        self
538    }
539
540    /// Sets the file size in bytes.
541    #[must_use]
542    pub fn file_size(mut self, size: u64) -> Self {
543        self.file_size = size;
544        self
545    }
546
547    /// Sets the overall bitrate in bits per second.
548    #[must_use]
549    pub fn bitrate(mut self, bitrate: u64) -> Self {
550        self.bitrate = Some(bitrate);
551        self
552    }
553
554    /// Adds a video stream.
555    #[must_use]
556    pub fn video_stream(mut self, stream: VideoStreamInfo) -> Self {
557        self.video_streams.push(stream);
558        self
559    }
560
561    /// Sets all video streams at once, replacing any existing streams.
562    #[must_use]
563    pub fn video_streams(mut self, streams: Vec<VideoStreamInfo>) -> Self {
564        self.video_streams = streams;
565        self
566    }
567
568    /// Adds an audio stream.
569    #[must_use]
570    pub fn audio_stream(mut self, stream: AudioStreamInfo) -> Self {
571        self.audio_streams.push(stream);
572        self
573    }
574
575    /// Sets all audio streams at once, replacing any existing streams.
576    #[must_use]
577    pub fn audio_streams(mut self, streams: Vec<AudioStreamInfo>) -> Self {
578        self.audio_streams = streams;
579        self
580    }
581
582    /// Adds a subtitle stream.
583    #[must_use]
584    pub fn subtitle_stream(mut self, stream: SubtitleStreamInfo) -> Self {
585        self.subtitle_streams.push(stream);
586        self
587    }
588
589    /// Sets all subtitle streams at once, replacing any existing streams.
590    #[must_use]
591    pub fn subtitle_streams(mut self, streams: Vec<SubtitleStreamInfo>) -> Self {
592        self.subtitle_streams = streams;
593        self
594    }
595
596    /// Adds a chapter.
597    #[must_use]
598    pub fn chapter(mut self, chapter: ChapterInfo) -> Self {
599        self.chapters.push(chapter);
600        self
601    }
602
603    /// Sets all chapters at once, replacing any existing chapters.
604    #[must_use]
605    pub fn chapters(mut self, chapters: Vec<ChapterInfo>) -> Self {
606        self.chapters = chapters;
607        self
608    }
609
610    /// Adds a metadata key-value pair.
611    #[must_use]
612    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
613        self.metadata.insert(key.into(), value.into());
614        self
615    }
616
617    /// Sets all metadata at once, replacing any existing metadata.
618    #[must_use]
619    pub fn metadata_map(mut self, metadata: HashMap<String, String>) -> Self {
620        self.metadata = metadata;
621        self
622    }
623
624    /// Builds the [`MediaInfo`].
625    #[must_use]
626    pub fn build(self) -> MediaInfo {
627        MediaInfo {
628            path: self.path,
629            format: self.format,
630            format_long_name: self.format_long_name,
631            duration: self.duration,
632            file_size: self.file_size,
633            bitrate: self.bitrate,
634            video_streams: self.video_streams,
635            audio_streams: self.audio_streams,
636            subtitle_streams: self.subtitle_streams,
637            chapters: self.chapters,
638            metadata: self.metadata,
639        }
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646    use crate::codec::{AudioCodec, SubtitleCodec, VideoCodec};
647    use crate::time::Rational;
648    use crate::{PixelFormat, SampleFormat};
649
650    fn sample_video_stream() -> VideoStreamInfo {
651        VideoStreamInfo::builder()
652            .index(0)
653            .codec(VideoCodec::H264)
654            .codec_name("h264")
655            .width(1920)
656            .height(1080)
657            .frame_rate(Rational::new(30, 1))
658            .pixel_format(PixelFormat::Yuv420p)
659            .duration(Duration::from_secs(120))
660            .build()
661    }
662
663    fn sample_audio_stream() -> AudioStreamInfo {
664        AudioStreamInfo::builder()
665            .index(1)
666            .codec(AudioCodec::Aac)
667            .codec_name("aac")
668            .sample_rate(48000)
669            .channels(2)
670            .sample_format(SampleFormat::F32)
671            .duration(Duration::from_secs(120))
672            .build()
673    }
674
675    fn sample_subtitle_stream() -> SubtitleStreamInfo {
676        SubtitleStreamInfo::builder()
677            .index(2)
678            .codec(SubtitleCodec::Srt)
679            .codec_name("srt")
680            .language("eng")
681            .build()
682    }
683
684    mod media_info_tests {
685        use super::*;
686
687        #[test]
688        fn test_builder_basic() {
689            let info = MediaInfo::builder()
690                .path("/path/to/video.mp4")
691                .format("mp4")
692                .duration(Duration::from_secs(120))
693                .file_size(1_000_000)
694                .build();
695
696            assert_eq!(info.path(), Path::new("/path/to/video.mp4"));
697            assert_eq!(info.format(), "mp4");
698            assert_eq!(info.duration(), Duration::from_secs(120));
699            assert_eq!(info.file_size(), 1_000_000);
700            assert!(info.format_long_name().is_none());
701            assert!(info.bitrate().is_none());
702        }
703
704        #[test]
705        fn test_builder_full() {
706            let video = sample_video_stream();
707            let audio = sample_audio_stream();
708
709            let info = MediaInfo::builder()
710                .path("/path/to/video.mp4")
711                .format("mp4")
712                .format_long_name("QuickTime / MOV")
713                .duration(Duration::from_secs(120))
714                .file_size(150_000_000)
715                .bitrate(10_000_000)
716                .video_stream(video)
717                .audio_stream(audio)
718                .metadata("title", "Test Video")
719                .metadata("artist", "Test Artist")
720                .build();
721
722            assert_eq!(info.format_long_name(), Some("QuickTime / MOV"));
723            assert_eq!(info.bitrate(), Some(10_000_000));
724            assert_eq!(info.video_stream_count(), 1);
725            assert_eq!(info.audio_stream_count(), 1);
726            assert_eq!(info.metadata_value("title"), Some("Test Video"));
727            assert_eq!(info.metadata_value("artist"), Some("Test Artist"));
728            assert!(info.metadata_value("nonexistent").is_none());
729        }
730
731        #[test]
732        fn test_default() {
733            let info = MediaInfo::default();
734            assert_eq!(info.path(), Path::new(""));
735            assert_eq!(info.format(), "");
736            assert_eq!(info.duration(), Duration::ZERO);
737            assert_eq!(info.file_size(), 0);
738            assert!(!info.has_video());
739            assert!(!info.has_audio());
740        }
741
742        #[test]
743        fn test_has_streams() {
744            // No streams
745            let empty = MediaInfo::default();
746            assert!(!empty.has_video());
747            assert!(!empty.has_audio());
748
749            // Video only
750            let video_only = MediaInfo::builder()
751                .video_stream(sample_video_stream())
752                .build();
753            assert!(video_only.has_video());
754            assert!(!video_only.has_audio());
755            assert!(video_only.is_video_only());
756            assert!(!video_only.is_audio_only());
757
758            // Audio only
759            let audio_only = MediaInfo::builder()
760                .audio_stream(sample_audio_stream())
761                .build();
762            assert!(!audio_only.has_video());
763            assert!(audio_only.has_audio());
764            assert!(!audio_only.is_video_only());
765            assert!(audio_only.is_audio_only());
766
767            // Both
768            let both = MediaInfo::builder()
769                .video_stream(sample_video_stream())
770                .audio_stream(sample_audio_stream())
771                .build();
772            assert!(both.has_video());
773            assert!(both.has_audio());
774            assert!(!both.is_video_only());
775            assert!(!both.is_audio_only());
776        }
777
778        #[test]
779        fn test_primary_streams() {
780            let video1 = VideoStreamInfo::builder()
781                .index(0)
782                .width(1920)
783                .height(1080)
784                .build();
785            let video2 = VideoStreamInfo::builder()
786                .index(2)
787                .width(1280)
788                .height(720)
789                .build();
790            let audio1 = AudioStreamInfo::builder()
791                .index(1)
792                .sample_rate(48000)
793                .build();
794            let audio2 = AudioStreamInfo::builder()
795                .index(3)
796                .sample_rate(44100)
797                .build();
798
799            let info = MediaInfo::builder()
800                .video_stream(video1)
801                .video_stream(video2)
802                .audio_stream(audio1)
803                .audio_stream(audio2)
804                .build();
805
806            // Primary should be first
807            let primary_video = info.primary_video().unwrap();
808            assert_eq!(primary_video.width(), 1920);
809            assert_eq!(primary_video.index(), 0);
810
811            let primary_audio = info.primary_audio().unwrap();
812            assert_eq!(primary_audio.sample_rate(), 48000);
813            assert_eq!(primary_audio.index(), 1);
814        }
815
816        #[test]
817        fn test_stream_access_by_index() {
818            let video1 = VideoStreamInfo::builder().width(1920).build();
819            let video2 = VideoStreamInfo::builder().width(1280).build();
820            let audio1 = AudioStreamInfo::builder().sample_rate(48000).build();
821
822            let info = MediaInfo::builder()
823                .video_stream(video1)
824                .video_stream(video2)
825                .audio_stream(audio1)
826                .build();
827
828            assert_eq!(info.video_stream(0).unwrap().width(), 1920);
829            assert_eq!(info.video_stream(1).unwrap().width(), 1280);
830            assert!(info.video_stream(2).is_none());
831
832            assert_eq!(info.audio_stream(0).unwrap().sample_rate(), 48000);
833            assert!(info.audio_stream(1).is_none());
834        }
835
836        #[test]
837        fn test_resolution_and_frame_rate() {
838            let info = MediaInfo::builder()
839                .video_stream(sample_video_stream())
840                .build();
841
842            assert_eq!(info.resolution(), Some((1920, 1080)));
843            assert!((info.frame_rate().unwrap() - 30.0).abs() < 0.001);
844
845            // No video
846            let no_video = MediaInfo::default();
847            assert!(no_video.resolution().is_none());
848            assert!(no_video.frame_rate().is_none());
849        }
850
851        #[test]
852        fn test_sample_rate_and_channels() {
853            let info = MediaInfo::builder()
854                .audio_stream(sample_audio_stream())
855                .build();
856
857            assert_eq!(info.sample_rate(), Some(48000));
858            assert_eq!(info.channels(), Some(2));
859
860            // No audio
861            let no_audio = MediaInfo::default();
862            assert!(no_audio.sample_rate().is_none());
863            assert!(no_audio.channels().is_none());
864        }
865
866        #[test]
867        fn test_stream_counts() {
868            let info = MediaInfo::builder()
869                .video_stream(sample_video_stream())
870                .video_stream(sample_video_stream())
871                .audio_stream(sample_audio_stream())
872                .audio_stream(sample_audio_stream())
873                .audio_stream(sample_audio_stream())
874                .build();
875
876            assert_eq!(info.video_stream_count(), 2);
877            assert_eq!(info.audio_stream_count(), 3);
878            assert_eq!(info.stream_count(), 5);
879        }
880
881        #[test]
882        fn has_subtitles_should_return_true_when_subtitle_streams_present() {
883            let no_subs = MediaInfo::default();
884            assert!(!no_subs.has_subtitles());
885            assert_eq!(no_subs.subtitle_stream_count(), 0);
886
887            let with_subs = MediaInfo::builder()
888                .subtitle_stream(sample_subtitle_stream())
889                .subtitle_stream(sample_subtitle_stream())
890                .build();
891            assert!(with_subs.has_subtitles());
892            assert_eq!(with_subs.subtitle_stream_count(), 2);
893        }
894
895        #[test]
896        fn subtitle_stream_count_should_be_included_in_stream_count() {
897            let info = MediaInfo::builder()
898                .video_stream(sample_video_stream())
899                .audio_stream(sample_audio_stream())
900                .subtitle_stream(sample_subtitle_stream())
901                .build();
902            assert_eq!(info.stream_count(), 3);
903        }
904
905        #[test]
906        fn subtitle_stream_by_index_should_return_correct_stream() {
907            let sub1 = SubtitleStreamInfo::builder()
908                .index(2)
909                .codec(SubtitleCodec::Srt)
910                .language("eng")
911                .build();
912            let sub2 = SubtitleStreamInfo::builder()
913                .index(3)
914                .codec(SubtitleCodec::Ass)
915                .language("jpn")
916                .build();
917
918            let info = MediaInfo::builder()
919                .subtitle_stream(sub1)
920                .subtitle_stream(sub2)
921                .build();
922
923            assert_eq!(info.subtitle_stream(0).unwrap().language(), Some("eng"));
924            assert_eq!(info.subtitle_stream(1).unwrap().language(), Some("jpn"));
925            assert!(info.subtitle_stream(2).is_none());
926        }
927
928        #[test]
929        fn test_file_name_and_extension() {
930            let info = MediaInfo::builder().path("/path/to/my_video.mp4").build();
931
932            assert_eq!(info.file_name(), Some("my_video.mp4"));
933            assert_eq!(info.extension(), Some("mp4"));
934
935            // Empty path
936            let empty = MediaInfo::default();
937            assert!(empty.file_name().is_none());
938            assert!(empty.extension().is_none());
939        }
940
941        #[test]
942        fn test_metadata_operations() {
943            let mut map = HashMap::new();
944            map.insert("key1".to_string(), "value1".to_string());
945            map.insert("key2".to_string(), "value2".to_string());
946
947            let info = MediaInfo::builder()
948                .metadata_map(map)
949                .metadata("key3", "value3")
950                .build();
951
952            assert_eq!(info.metadata().len(), 3);
953            assert_eq!(info.metadata_value("key1"), Some("value1"));
954            assert_eq!(info.metadata_value("key2"), Some("value2"));
955            assert_eq!(info.metadata_value("key3"), Some("value3"));
956        }
957
958        #[test]
959        fn test_clone() {
960            let info = MediaInfo::builder()
961                .path("/path/to/video.mp4")
962                .format("mp4")
963                .format_long_name("QuickTime / MOV")
964                .duration(Duration::from_secs(120))
965                .file_size(1_000_000)
966                .video_stream(sample_video_stream())
967                .audio_stream(sample_audio_stream())
968                .metadata("title", "Test")
969                .build();
970
971            let cloned = info.clone();
972            assert_eq!(info.path(), cloned.path());
973            assert_eq!(info.format(), cloned.format());
974            assert_eq!(info.format_long_name(), cloned.format_long_name());
975            assert_eq!(info.duration(), cloned.duration());
976            assert_eq!(info.file_size(), cloned.file_size());
977            assert_eq!(info.video_stream_count(), cloned.video_stream_count());
978            assert_eq!(info.audio_stream_count(), cloned.audio_stream_count());
979            assert_eq!(info.metadata_value("title"), cloned.metadata_value("title"));
980        }
981
982        #[test]
983        fn test_debug() {
984            let info = MediaInfo::builder()
985                .path("/path/to/video.mp4")
986                .format("mp4")
987                .duration(Duration::from_secs(120))
988                .file_size(1_000_000)
989                .build();
990
991            let debug = format!("{info:?}");
992            assert!(debug.contains("MediaInfo"));
993            assert!(debug.contains("mp4"));
994        }
995
996        #[test]
997        fn test_video_streams_setter() {
998            let streams = vec![sample_video_stream(), sample_video_stream()];
999
1000            let info = MediaInfo::builder().video_streams(streams).build();
1001
1002            assert_eq!(info.video_stream_count(), 2);
1003        }
1004
1005        #[test]
1006        fn test_audio_streams_setter() {
1007            let streams = vec![
1008                sample_audio_stream(),
1009                sample_audio_stream(),
1010                sample_audio_stream(),
1011            ];
1012
1013            let info = MediaInfo::builder().audio_streams(streams).build();
1014
1015            assert_eq!(info.audio_stream_count(), 3);
1016        }
1017    }
1018
1019    mod media_info_builder_tests {
1020        use super::*;
1021
1022        #[test]
1023        fn test_builder_default() {
1024            let builder = MediaInfoBuilder::default();
1025            let info = builder.build();
1026            assert_eq!(info.path(), Path::new(""));
1027            assert_eq!(info.format(), "");
1028            assert_eq!(info.duration(), Duration::ZERO);
1029        }
1030
1031        #[test]
1032        fn test_builder_clone() {
1033            let builder = MediaInfo::builder()
1034                .path("/path/to/video.mp4")
1035                .format("mp4")
1036                .duration(Duration::from_secs(120));
1037
1038            let cloned = builder.clone();
1039            let info1 = builder.build();
1040            let info2 = cloned.build();
1041
1042            assert_eq!(info1.path(), info2.path());
1043            assert_eq!(info1.format(), info2.format());
1044            assert_eq!(info1.duration(), info2.duration());
1045        }
1046
1047        #[test]
1048        fn test_builder_debug() {
1049            let builder = MediaInfo::builder()
1050                .path("/path/to/video.mp4")
1051                .format("mp4");
1052
1053            let debug = format!("{builder:?}");
1054            assert!(debug.contains("MediaInfoBuilder"));
1055        }
1056    }
1057
1058    mod metadata_convenience_tests {
1059        use super::*;
1060
1061        #[test]
1062        fn test_title() {
1063            let info = MediaInfo::builder()
1064                .metadata("title", "Sample Video Title")
1065                .build();
1066
1067            assert_eq!(info.title(), Some("Sample Video Title"));
1068        }
1069
1070        #[test]
1071        fn test_title_missing() {
1072            let info = MediaInfo::default();
1073            assert!(info.title().is_none());
1074        }
1075
1076        #[test]
1077        fn test_artist() {
1078            let info = MediaInfo::builder()
1079                .metadata("artist", "Test Artist")
1080                .build();
1081
1082            assert_eq!(info.artist(), Some("Test Artist"));
1083        }
1084
1085        #[test]
1086        fn test_album() {
1087            let info = MediaInfo::builder().metadata("album", "Test Album").build();
1088
1089            assert_eq!(info.album(), Some("Test Album"));
1090        }
1091
1092        #[test]
1093        fn test_creation_time() {
1094            let info = MediaInfo::builder()
1095                .metadata("creation_time", "2024-01-15T10:30:00.000000Z")
1096                .build();
1097
1098            assert_eq!(info.creation_time(), Some("2024-01-15T10:30:00.000000Z"));
1099        }
1100
1101        #[test]
1102        fn test_date() {
1103            let info = MediaInfo::builder().metadata("date", "2024-01-15").build();
1104
1105            assert_eq!(info.date(), Some("2024-01-15"));
1106        }
1107
1108        #[test]
1109        fn test_comment() {
1110            let info = MediaInfo::builder()
1111                .metadata("comment", "This is a test comment")
1112                .build();
1113
1114            assert_eq!(info.comment(), Some("This is a test comment"));
1115        }
1116
1117        #[test]
1118        fn test_encoder() {
1119            let info = MediaInfo::builder()
1120                .metadata("encoder", "Lavf58.76.100")
1121                .build();
1122
1123            assert_eq!(info.encoder(), Some("Lavf58.76.100"));
1124        }
1125
1126        #[test]
1127        fn test_multiple_metadata_fields() {
1128            let info = MediaInfo::builder()
1129                .metadata("title", "My Video")
1130                .metadata("artist", "John Doe")
1131                .metadata("album", "My Collection")
1132                .metadata("date", "2024")
1133                .metadata("comment", "A great video")
1134                .metadata("encoder", "FFmpeg")
1135                .metadata("custom_field", "custom_value")
1136                .build();
1137
1138            assert_eq!(info.title(), Some("My Video"));
1139            assert_eq!(info.artist(), Some("John Doe"));
1140            assert_eq!(info.album(), Some("My Collection"));
1141            assert_eq!(info.date(), Some("2024"));
1142            assert_eq!(info.comment(), Some("A great video"));
1143            assert_eq!(info.encoder(), Some("FFmpeg"));
1144            assert_eq!(info.metadata_value("custom_field"), Some("custom_value"));
1145            assert_eq!(info.metadata().len(), 7);
1146        }
1147    }
1148}