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    #[must_use]
353    #[inline]
354    pub fn channels(&self) -> Option<u32> {
355        self.primary_audio().map(AudioStreamInfo::channels)
356    }
357
358    /// Returns `true` if this is a video-only file (no audio streams).
359    #[must_use]
360    #[inline]
361    pub fn is_video_only(&self) -> bool {
362        self.has_video() && !self.has_audio()
363    }
364
365    /// Returns `true` if this is an audio-only file (no video streams).
366    #[must_use]
367    #[inline]
368    pub fn is_audio_only(&self) -> bool {
369        self.has_audio() && !self.has_video()
370    }
371
372    /// Returns the file name (without directory path).
373    #[must_use]
374    #[inline]
375    pub fn file_name(&self) -> Option<&str> {
376        self.path.file_name().and_then(|n| n.to_str())
377    }
378
379    /// Returns the file extension.
380    #[must_use]
381    #[inline]
382    pub fn extension(&self) -> Option<&str> {
383        self.path.extension().and_then(|e| e.to_str())
384    }
385
386    // === Common Metadata Accessors ===
387
388    /// Returns the title from metadata.
389    ///
390    /// This is a convenience method for accessing the "title" metadata key,
391    /// which is commonly used by media containers to store the title of the content.
392    #[must_use]
393    #[inline]
394    pub fn title(&self) -> Option<&str> {
395        self.metadata_value("title")
396    }
397
398    /// Returns the artist from metadata.
399    ///
400    /// This is a convenience method for accessing the "artist" metadata key.
401    #[must_use]
402    #[inline]
403    pub fn artist(&self) -> Option<&str> {
404        self.metadata_value("artist")
405    }
406
407    /// Returns the album from metadata.
408    ///
409    /// This is a convenience method for accessing the "album" metadata key.
410    #[must_use]
411    #[inline]
412    pub fn album(&self) -> Option<&str> {
413        self.metadata_value("album")
414    }
415
416    /// Returns the creation time from metadata.
417    ///
418    /// This is a convenience method for accessing the `creation_time` metadata key,
419    /// which is commonly used by media containers to store when the file was created.
420    /// The format is typically ISO 8601 (e.g., `2024-01-15T10:30:00.000000Z`).
421    #[must_use]
422    #[inline]
423    pub fn creation_time(&self) -> Option<&str> {
424        self.metadata_value("creation_time")
425    }
426
427    /// Returns the date from metadata.
428    ///
429    /// This is a convenience method for accessing the "date" metadata key.
430    #[must_use]
431    #[inline]
432    pub fn date(&self) -> Option<&str> {
433        self.metadata_value("date")
434    }
435
436    /// Returns the comment from metadata.
437    ///
438    /// This is a convenience method for accessing the "comment" metadata key.
439    #[must_use]
440    #[inline]
441    pub fn comment(&self) -> Option<&str> {
442        self.metadata_value("comment")
443    }
444
445    /// Returns the encoder from metadata.
446    ///
447    /// This is a convenience method for accessing the "encoder" metadata key,
448    /// which stores information about the software used to create the file.
449    #[must_use]
450    #[inline]
451    pub fn encoder(&self) -> Option<&str> {
452        self.metadata_value("encoder")
453    }
454}
455
456impl Default for MediaInfo {
457    fn default() -> Self {
458        Self {
459            path: PathBuf::new(),
460            format: String::new(),
461            format_long_name: None,
462            duration: Duration::ZERO,
463            file_size: 0,
464            bitrate: None,
465            video_streams: Vec::new(),
466            audio_streams: Vec::new(),
467            subtitle_streams: Vec::new(),
468            chapters: Vec::new(),
469            metadata: HashMap::new(),
470        }
471    }
472}
473
474/// Builder for constructing [`MediaInfo`].
475///
476/// # Examples
477///
478/// ```
479/// use ff_format::media::MediaInfo;
480/// use std::time::Duration;
481///
482/// let info = MediaInfo::builder()
483///     .path("/path/to/video.mp4")
484///     .format("mp4")
485///     .format_long_name("QuickTime / MOV")
486///     .duration(Duration::from_secs(120))
487///     .file_size(1_000_000)
488///     .bitrate(8_000_000)
489///     .metadata("title", "Sample Video")
490///     .build();
491/// ```
492#[derive(Debug, Clone, Default)]
493pub struct MediaInfoBuilder {
494    path: PathBuf,
495    format: String,
496    format_long_name: Option<String>,
497    duration: Duration,
498    file_size: u64,
499    bitrate: Option<u64>,
500    video_streams: Vec<VideoStreamInfo>,
501    audio_streams: Vec<AudioStreamInfo>,
502    subtitle_streams: Vec<SubtitleStreamInfo>,
503    chapters: Vec<ChapterInfo>,
504    metadata: HashMap<String, String>,
505}
506
507impl MediaInfoBuilder {
508    /// Sets the file path.
509    #[must_use]
510    pub fn path(mut self, path: impl Into<PathBuf>) -> Self {
511        self.path = path.into();
512        self
513    }
514
515    /// Sets the container format name.
516    #[must_use]
517    pub fn format(mut self, format: impl Into<String>) -> Self {
518        self.format = format.into();
519        self
520    }
521
522    /// Sets the long format name.
523    #[must_use]
524    pub fn format_long_name(mut self, name: impl Into<String>) -> Self {
525        self.format_long_name = Some(name.into());
526        self
527    }
528
529    /// Sets the total duration.
530    #[must_use]
531    pub fn duration(mut self, duration: Duration) -> Self {
532        self.duration = duration;
533        self
534    }
535
536    /// Sets the file size in bytes.
537    #[must_use]
538    pub fn file_size(mut self, size: u64) -> Self {
539        self.file_size = size;
540        self
541    }
542
543    /// Sets the overall bitrate in bits per second.
544    #[must_use]
545    pub fn bitrate(mut self, bitrate: u64) -> Self {
546        self.bitrate = Some(bitrate);
547        self
548    }
549
550    /// Adds a video stream.
551    #[must_use]
552    pub fn video_stream(mut self, stream: VideoStreamInfo) -> Self {
553        self.video_streams.push(stream);
554        self
555    }
556
557    /// Sets all video streams at once, replacing any existing streams.
558    #[must_use]
559    pub fn video_streams(mut self, streams: Vec<VideoStreamInfo>) -> Self {
560        self.video_streams = streams;
561        self
562    }
563
564    /// Adds an audio stream.
565    #[must_use]
566    pub fn audio_stream(mut self, stream: AudioStreamInfo) -> Self {
567        self.audio_streams.push(stream);
568        self
569    }
570
571    /// Sets all audio streams at once, replacing any existing streams.
572    #[must_use]
573    pub fn audio_streams(mut self, streams: Vec<AudioStreamInfo>) -> Self {
574        self.audio_streams = streams;
575        self
576    }
577
578    /// Adds a subtitle stream.
579    #[must_use]
580    pub fn subtitle_stream(mut self, stream: SubtitleStreamInfo) -> Self {
581        self.subtitle_streams.push(stream);
582        self
583    }
584
585    /// Sets all subtitle streams at once, replacing any existing streams.
586    #[must_use]
587    pub fn subtitle_streams(mut self, streams: Vec<SubtitleStreamInfo>) -> Self {
588        self.subtitle_streams = streams;
589        self
590    }
591
592    /// Adds a chapter.
593    #[must_use]
594    pub fn chapter(mut self, chapter: ChapterInfo) -> Self {
595        self.chapters.push(chapter);
596        self
597    }
598
599    /// Sets all chapters at once, replacing any existing chapters.
600    #[must_use]
601    pub fn chapters(mut self, chapters: Vec<ChapterInfo>) -> Self {
602        self.chapters = chapters;
603        self
604    }
605
606    /// Adds a metadata key-value pair.
607    #[must_use]
608    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
609        self.metadata.insert(key.into(), value.into());
610        self
611    }
612
613    /// Sets all metadata at once, replacing any existing metadata.
614    #[must_use]
615    pub fn metadata_map(mut self, metadata: HashMap<String, String>) -> Self {
616        self.metadata = metadata;
617        self
618    }
619
620    /// Builds the [`MediaInfo`].
621    #[must_use]
622    pub fn build(self) -> MediaInfo {
623        MediaInfo {
624            path: self.path,
625            format: self.format,
626            format_long_name: self.format_long_name,
627            duration: self.duration,
628            file_size: self.file_size,
629            bitrate: self.bitrate,
630            video_streams: self.video_streams,
631            audio_streams: self.audio_streams,
632            subtitle_streams: self.subtitle_streams,
633            chapters: self.chapters,
634            metadata: self.metadata,
635        }
636    }
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642    use crate::codec::{AudioCodec, SubtitleCodec, VideoCodec};
643    use crate::time::Rational;
644    use crate::{PixelFormat, SampleFormat};
645
646    fn sample_video_stream() -> VideoStreamInfo {
647        VideoStreamInfo::builder()
648            .index(0)
649            .codec(VideoCodec::H264)
650            .codec_name("h264")
651            .width(1920)
652            .height(1080)
653            .frame_rate(Rational::new(30, 1))
654            .pixel_format(PixelFormat::Yuv420p)
655            .duration(Duration::from_secs(120))
656            .build()
657    }
658
659    fn sample_audio_stream() -> AudioStreamInfo {
660        AudioStreamInfo::builder()
661            .index(1)
662            .codec(AudioCodec::Aac)
663            .codec_name("aac")
664            .sample_rate(48000)
665            .channels(2)
666            .sample_format(SampleFormat::F32)
667            .duration(Duration::from_secs(120))
668            .build()
669    }
670
671    fn sample_subtitle_stream() -> SubtitleStreamInfo {
672        SubtitleStreamInfo::builder()
673            .index(2)
674            .codec(SubtitleCodec::Srt)
675            .codec_name("srt")
676            .language("eng")
677            .build()
678    }
679
680    mod media_info_tests {
681        use super::*;
682
683        #[test]
684        fn test_builder_basic() {
685            let info = MediaInfo::builder()
686                .path("/path/to/video.mp4")
687                .format("mp4")
688                .duration(Duration::from_secs(120))
689                .file_size(1_000_000)
690                .build();
691
692            assert_eq!(info.path(), Path::new("/path/to/video.mp4"));
693            assert_eq!(info.format(), "mp4");
694            assert_eq!(info.duration(), Duration::from_secs(120));
695            assert_eq!(info.file_size(), 1_000_000);
696            assert!(info.format_long_name().is_none());
697            assert!(info.bitrate().is_none());
698        }
699
700        #[test]
701        fn test_builder_full() {
702            let video = sample_video_stream();
703            let audio = sample_audio_stream();
704
705            let info = MediaInfo::builder()
706                .path("/path/to/video.mp4")
707                .format("mp4")
708                .format_long_name("QuickTime / MOV")
709                .duration(Duration::from_secs(120))
710                .file_size(150_000_000)
711                .bitrate(10_000_000)
712                .video_stream(video)
713                .audio_stream(audio)
714                .metadata("title", "Test Video")
715                .metadata("artist", "Test Artist")
716                .build();
717
718            assert_eq!(info.format_long_name(), Some("QuickTime / MOV"));
719            assert_eq!(info.bitrate(), Some(10_000_000));
720            assert_eq!(info.video_stream_count(), 1);
721            assert_eq!(info.audio_stream_count(), 1);
722            assert_eq!(info.metadata_value("title"), Some("Test Video"));
723            assert_eq!(info.metadata_value("artist"), Some("Test Artist"));
724            assert!(info.metadata_value("nonexistent").is_none());
725        }
726
727        #[test]
728        fn test_default() {
729            let info = MediaInfo::default();
730            assert_eq!(info.path(), Path::new(""));
731            assert_eq!(info.format(), "");
732            assert_eq!(info.duration(), Duration::ZERO);
733            assert_eq!(info.file_size(), 0);
734            assert!(!info.has_video());
735            assert!(!info.has_audio());
736        }
737
738        #[test]
739        fn test_has_streams() {
740            // No streams
741            let empty = MediaInfo::default();
742            assert!(!empty.has_video());
743            assert!(!empty.has_audio());
744
745            // Video only
746            let video_only = MediaInfo::builder()
747                .video_stream(sample_video_stream())
748                .build();
749            assert!(video_only.has_video());
750            assert!(!video_only.has_audio());
751            assert!(video_only.is_video_only());
752            assert!(!video_only.is_audio_only());
753
754            // Audio only
755            let audio_only = MediaInfo::builder()
756                .audio_stream(sample_audio_stream())
757                .build();
758            assert!(!audio_only.has_video());
759            assert!(audio_only.has_audio());
760            assert!(!audio_only.is_video_only());
761            assert!(audio_only.is_audio_only());
762
763            // Both
764            let both = MediaInfo::builder()
765                .video_stream(sample_video_stream())
766                .audio_stream(sample_audio_stream())
767                .build();
768            assert!(both.has_video());
769            assert!(both.has_audio());
770            assert!(!both.is_video_only());
771            assert!(!both.is_audio_only());
772        }
773
774        #[test]
775        fn test_primary_streams() {
776            let video1 = VideoStreamInfo::builder()
777                .index(0)
778                .width(1920)
779                .height(1080)
780                .build();
781            let video2 = VideoStreamInfo::builder()
782                .index(2)
783                .width(1280)
784                .height(720)
785                .build();
786            let audio1 = AudioStreamInfo::builder()
787                .index(1)
788                .sample_rate(48000)
789                .build();
790            let audio2 = AudioStreamInfo::builder()
791                .index(3)
792                .sample_rate(44100)
793                .build();
794
795            let info = MediaInfo::builder()
796                .video_stream(video1)
797                .video_stream(video2)
798                .audio_stream(audio1)
799                .audio_stream(audio2)
800                .build();
801
802            // Primary should be first
803            let primary_video = info.primary_video().unwrap();
804            assert_eq!(primary_video.width(), 1920);
805            assert_eq!(primary_video.index(), 0);
806
807            let primary_audio = info.primary_audio().unwrap();
808            assert_eq!(primary_audio.sample_rate(), 48000);
809            assert_eq!(primary_audio.index(), 1);
810        }
811
812        #[test]
813        fn test_stream_access_by_index() {
814            let video1 = VideoStreamInfo::builder().width(1920).build();
815            let video2 = VideoStreamInfo::builder().width(1280).build();
816            let audio1 = AudioStreamInfo::builder().sample_rate(48000).build();
817
818            let info = MediaInfo::builder()
819                .video_stream(video1)
820                .video_stream(video2)
821                .audio_stream(audio1)
822                .build();
823
824            assert_eq!(info.video_stream(0).unwrap().width(), 1920);
825            assert_eq!(info.video_stream(1).unwrap().width(), 1280);
826            assert!(info.video_stream(2).is_none());
827
828            assert_eq!(info.audio_stream(0).unwrap().sample_rate(), 48000);
829            assert!(info.audio_stream(1).is_none());
830        }
831
832        #[test]
833        fn test_resolution_and_frame_rate() {
834            let info = MediaInfo::builder()
835                .video_stream(sample_video_stream())
836                .build();
837
838            assert_eq!(info.resolution(), Some((1920, 1080)));
839            assert!((info.frame_rate().unwrap() - 30.0).abs() < 0.001);
840
841            // No video
842            let no_video = MediaInfo::default();
843            assert!(no_video.resolution().is_none());
844            assert!(no_video.frame_rate().is_none());
845        }
846
847        #[test]
848        fn test_sample_rate_and_channels() {
849            let info = MediaInfo::builder()
850                .audio_stream(sample_audio_stream())
851                .build();
852
853            assert_eq!(info.sample_rate(), Some(48000));
854            assert_eq!(info.channels(), Some(2));
855
856            // No audio
857            let no_audio = MediaInfo::default();
858            assert!(no_audio.sample_rate().is_none());
859            assert!(no_audio.channels().is_none());
860        }
861
862        #[test]
863        fn test_stream_counts() {
864            let info = MediaInfo::builder()
865                .video_stream(sample_video_stream())
866                .video_stream(sample_video_stream())
867                .audio_stream(sample_audio_stream())
868                .audio_stream(sample_audio_stream())
869                .audio_stream(sample_audio_stream())
870                .build();
871
872            assert_eq!(info.video_stream_count(), 2);
873            assert_eq!(info.audio_stream_count(), 3);
874            assert_eq!(info.stream_count(), 5);
875        }
876
877        #[test]
878        fn has_subtitles_should_return_true_when_subtitle_streams_present() {
879            let no_subs = MediaInfo::default();
880            assert!(!no_subs.has_subtitles());
881            assert_eq!(no_subs.subtitle_stream_count(), 0);
882
883            let with_subs = MediaInfo::builder()
884                .subtitle_stream(sample_subtitle_stream())
885                .subtitle_stream(sample_subtitle_stream())
886                .build();
887            assert!(with_subs.has_subtitles());
888            assert_eq!(with_subs.subtitle_stream_count(), 2);
889        }
890
891        #[test]
892        fn subtitle_stream_count_should_be_included_in_stream_count() {
893            let info = MediaInfo::builder()
894                .video_stream(sample_video_stream())
895                .audio_stream(sample_audio_stream())
896                .subtitle_stream(sample_subtitle_stream())
897                .build();
898            assert_eq!(info.stream_count(), 3);
899        }
900
901        #[test]
902        fn subtitle_stream_by_index_should_return_correct_stream() {
903            let sub1 = SubtitleStreamInfo::builder()
904                .index(2)
905                .codec(SubtitleCodec::Srt)
906                .language("eng")
907                .build();
908            let sub2 = SubtitleStreamInfo::builder()
909                .index(3)
910                .codec(SubtitleCodec::Ass)
911                .language("jpn")
912                .build();
913
914            let info = MediaInfo::builder()
915                .subtitle_stream(sub1)
916                .subtitle_stream(sub2)
917                .build();
918
919            assert_eq!(info.subtitle_stream(0).unwrap().language(), Some("eng"));
920            assert_eq!(info.subtitle_stream(1).unwrap().language(), Some("jpn"));
921            assert!(info.subtitle_stream(2).is_none());
922        }
923
924        #[test]
925        fn test_file_name_and_extension() {
926            let info = MediaInfo::builder().path("/path/to/my_video.mp4").build();
927
928            assert_eq!(info.file_name(), Some("my_video.mp4"));
929            assert_eq!(info.extension(), Some("mp4"));
930
931            // Empty path
932            let empty = MediaInfo::default();
933            assert!(empty.file_name().is_none());
934            assert!(empty.extension().is_none());
935        }
936
937        #[test]
938        fn test_metadata_operations() {
939            let mut map = HashMap::new();
940            map.insert("key1".to_string(), "value1".to_string());
941            map.insert("key2".to_string(), "value2".to_string());
942
943            let info = MediaInfo::builder()
944                .metadata_map(map)
945                .metadata("key3", "value3")
946                .build();
947
948            assert_eq!(info.metadata().len(), 3);
949            assert_eq!(info.metadata_value("key1"), Some("value1"));
950            assert_eq!(info.metadata_value("key2"), Some("value2"));
951            assert_eq!(info.metadata_value("key3"), Some("value3"));
952        }
953
954        #[test]
955        fn test_clone() {
956            let info = MediaInfo::builder()
957                .path("/path/to/video.mp4")
958                .format("mp4")
959                .format_long_name("QuickTime / MOV")
960                .duration(Duration::from_secs(120))
961                .file_size(1_000_000)
962                .video_stream(sample_video_stream())
963                .audio_stream(sample_audio_stream())
964                .metadata("title", "Test")
965                .build();
966
967            let cloned = info.clone();
968            assert_eq!(info.path(), cloned.path());
969            assert_eq!(info.format(), cloned.format());
970            assert_eq!(info.format_long_name(), cloned.format_long_name());
971            assert_eq!(info.duration(), cloned.duration());
972            assert_eq!(info.file_size(), cloned.file_size());
973            assert_eq!(info.video_stream_count(), cloned.video_stream_count());
974            assert_eq!(info.audio_stream_count(), cloned.audio_stream_count());
975            assert_eq!(info.metadata_value("title"), cloned.metadata_value("title"));
976        }
977
978        #[test]
979        fn test_debug() {
980            let info = MediaInfo::builder()
981                .path("/path/to/video.mp4")
982                .format("mp4")
983                .duration(Duration::from_secs(120))
984                .file_size(1_000_000)
985                .build();
986
987            let debug = format!("{info:?}");
988            assert!(debug.contains("MediaInfo"));
989            assert!(debug.contains("mp4"));
990        }
991
992        #[test]
993        fn test_video_streams_setter() {
994            let streams = vec![sample_video_stream(), sample_video_stream()];
995
996            let info = MediaInfo::builder().video_streams(streams).build();
997
998            assert_eq!(info.video_stream_count(), 2);
999        }
1000
1001        #[test]
1002        fn test_audio_streams_setter() {
1003            let streams = vec![
1004                sample_audio_stream(),
1005                sample_audio_stream(),
1006                sample_audio_stream(),
1007            ];
1008
1009            let info = MediaInfo::builder().audio_streams(streams).build();
1010
1011            assert_eq!(info.audio_stream_count(), 3);
1012        }
1013    }
1014
1015    mod media_info_builder_tests {
1016        use super::*;
1017
1018        #[test]
1019        fn test_builder_default() {
1020            let builder = MediaInfoBuilder::default();
1021            let info = builder.build();
1022            assert_eq!(info.path(), Path::new(""));
1023            assert_eq!(info.format(), "");
1024            assert_eq!(info.duration(), Duration::ZERO);
1025        }
1026
1027        #[test]
1028        fn test_builder_clone() {
1029            let builder = MediaInfo::builder()
1030                .path("/path/to/video.mp4")
1031                .format("mp4")
1032                .duration(Duration::from_secs(120));
1033
1034            let cloned = builder.clone();
1035            let info1 = builder.build();
1036            let info2 = cloned.build();
1037
1038            assert_eq!(info1.path(), info2.path());
1039            assert_eq!(info1.format(), info2.format());
1040            assert_eq!(info1.duration(), info2.duration());
1041        }
1042
1043        #[test]
1044        fn test_builder_debug() {
1045            let builder = MediaInfo::builder()
1046                .path("/path/to/video.mp4")
1047                .format("mp4");
1048
1049            let debug = format!("{builder:?}");
1050            assert!(debug.contains("MediaInfoBuilder"));
1051        }
1052    }
1053
1054    mod metadata_convenience_tests {
1055        use super::*;
1056
1057        #[test]
1058        fn test_title() {
1059            let info = MediaInfo::builder()
1060                .metadata("title", "Sample Video Title")
1061                .build();
1062
1063            assert_eq!(info.title(), Some("Sample Video Title"));
1064        }
1065
1066        #[test]
1067        fn test_title_missing() {
1068            let info = MediaInfo::default();
1069            assert!(info.title().is_none());
1070        }
1071
1072        #[test]
1073        fn test_artist() {
1074            let info = MediaInfo::builder()
1075                .metadata("artist", "Test Artist")
1076                .build();
1077
1078            assert_eq!(info.artist(), Some("Test Artist"));
1079        }
1080
1081        #[test]
1082        fn test_album() {
1083            let info = MediaInfo::builder().metadata("album", "Test Album").build();
1084
1085            assert_eq!(info.album(), Some("Test Album"));
1086        }
1087
1088        #[test]
1089        fn test_creation_time() {
1090            let info = MediaInfo::builder()
1091                .metadata("creation_time", "2024-01-15T10:30:00.000000Z")
1092                .build();
1093
1094            assert_eq!(info.creation_time(), Some("2024-01-15T10:30:00.000000Z"));
1095        }
1096
1097        #[test]
1098        fn test_date() {
1099            let info = MediaInfo::builder().metadata("date", "2024-01-15").build();
1100
1101            assert_eq!(info.date(), Some("2024-01-15"));
1102        }
1103
1104        #[test]
1105        fn test_comment() {
1106            let info = MediaInfo::builder()
1107                .metadata("comment", "This is a test comment")
1108                .build();
1109
1110            assert_eq!(info.comment(), Some("This is a test comment"));
1111        }
1112
1113        #[test]
1114        fn test_encoder() {
1115            let info = MediaInfo::builder()
1116                .metadata("encoder", "Lavf58.76.100")
1117                .build();
1118
1119            assert_eq!(info.encoder(), Some("Lavf58.76.100"));
1120        }
1121
1122        #[test]
1123        fn test_multiple_metadata_fields() {
1124            let info = MediaInfo::builder()
1125                .metadata("title", "My Video")
1126                .metadata("artist", "John Doe")
1127                .metadata("album", "My Collection")
1128                .metadata("date", "2024")
1129                .metadata("comment", "A great video")
1130                .metadata("encoder", "FFmpeg")
1131                .metadata("custom_field", "custom_value")
1132                .build();
1133
1134            assert_eq!(info.title(), Some("My Video"));
1135            assert_eq!(info.artist(), Some("John Doe"));
1136            assert_eq!(info.album(), Some("My Collection"));
1137            assert_eq!(info.date(), Some("2024"));
1138            assert_eq!(info.comment(), Some("A great video"));
1139            assert_eq!(info.encoder(), Some("FFmpeg"));
1140            assert_eq!(info.metadata_value("custom_field"), Some("custom_value"));
1141            assert_eq!(info.metadata().len(), 7);
1142        }
1143    }
1144}