Skip to main content

ff_format/
stream.rs

1//! Video and audio stream information.
2//!
3//! This module provides structs for representing metadata about video and
4//! audio streams within media files.
5//!
6//! # Examples
7//!
8//! ```
9//! use ff_format::stream::{VideoStreamInfo, AudioStreamInfo};
10//! use ff_format::{PixelFormat, SampleFormat, Rational};
11//! use ff_format::codec::{VideoCodec, AudioCodec};
12//! use ff_format::color::{ColorSpace, ColorRange, ColorPrimaries};
13//! use ff_format::channel::ChannelLayout;
14//! use std::time::Duration;
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//! assert_eq!(video.width(), 1920);
27//! assert_eq!(video.height(), 1080);
28//!
29//! // Create audio stream info
30//! let audio = AudioStreamInfo::builder()
31//!     .index(1)
32//!     .codec(AudioCodec::Aac)
33//!     .sample_rate(48000)
34//!     .channels(2)
35//!     .sample_format(SampleFormat::F32)
36//!     .build();
37//!
38//! assert_eq!(audio.sample_rate(), 48000);
39//! assert_eq!(audio.channels(), 2);
40//! ```
41
42use std::time::Duration;
43
44use crate::channel::ChannelLayout;
45use crate::codec::{AudioCodec, VideoCodec};
46use crate::color::{ColorPrimaries, ColorRange, ColorSpace};
47use crate::pixel::PixelFormat;
48use crate::sample::SampleFormat;
49use crate::time::Rational;
50
51/// Information about a video stream within a media file.
52///
53/// This struct contains all metadata needed to understand and process
54/// a video stream, including resolution, codec, frame rate, and color
55/// characteristics.
56///
57/// # Construction
58///
59/// Use [`VideoStreamInfo::builder()`] for fluent construction:
60///
61/// ```
62/// use ff_format::stream::VideoStreamInfo;
63/// use ff_format::{PixelFormat, Rational};
64/// use ff_format::codec::VideoCodec;
65///
66/// let info = VideoStreamInfo::builder()
67///     .index(0)
68///     .codec(VideoCodec::H264)
69///     .width(1920)
70///     .height(1080)
71///     .frame_rate(Rational::new(30, 1))
72///     .build();
73/// ```
74#[derive(Debug, Clone)]
75pub struct VideoStreamInfo {
76    /// Stream index within the container
77    index: u32,
78    /// Video codec
79    codec: VideoCodec,
80    /// Codec name as reported by the demuxer
81    codec_name: String,
82    /// Frame width in pixels
83    width: u32,
84    /// Frame height in pixels
85    height: u32,
86    /// Pixel format
87    pixel_format: PixelFormat,
88    /// Frame rate (frames per second)
89    frame_rate: Rational,
90    /// Stream duration (if known)
91    duration: Option<Duration>,
92    /// Bitrate in bits per second (if known)
93    bitrate: Option<u64>,
94    /// Total number of frames (if known)
95    frame_count: Option<u64>,
96    /// Color space (matrix coefficients)
97    color_space: ColorSpace,
98    /// Color range (limited/full)
99    color_range: ColorRange,
100    /// Color primaries
101    color_primaries: ColorPrimaries,
102}
103
104impl VideoStreamInfo {
105    /// Creates a new builder for constructing `VideoStreamInfo`.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use ff_format::stream::VideoStreamInfo;
111    /// use ff_format::codec::VideoCodec;
112    /// use ff_format::{PixelFormat, Rational};
113    ///
114    /// let info = VideoStreamInfo::builder()
115    ///     .index(0)
116    ///     .codec(VideoCodec::H264)
117    ///     .width(1920)
118    ///     .height(1080)
119    ///     .frame_rate(Rational::new(30, 1))
120    ///     .build();
121    /// ```
122    #[must_use]
123    pub fn builder() -> VideoStreamInfoBuilder {
124        VideoStreamInfoBuilder::default()
125    }
126
127    /// Returns the stream index within the container.
128    #[must_use]
129    #[inline]
130    pub const fn index(&self) -> u32 {
131        self.index
132    }
133
134    /// Returns the video codec.
135    #[must_use]
136    #[inline]
137    pub const fn codec(&self) -> VideoCodec {
138        self.codec
139    }
140
141    /// Returns the codec name as reported by the demuxer.
142    #[must_use]
143    #[inline]
144    pub fn codec_name(&self) -> &str {
145        &self.codec_name
146    }
147
148    /// Returns the frame width in pixels.
149    #[must_use]
150    #[inline]
151    pub const fn width(&self) -> u32 {
152        self.width
153    }
154
155    /// Returns the frame height in pixels.
156    #[must_use]
157    #[inline]
158    pub const fn height(&self) -> u32 {
159        self.height
160    }
161
162    /// Returns the pixel format.
163    #[must_use]
164    #[inline]
165    pub const fn pixel_format(&self) -> PixelFormat {
166        self.pixel_format
167    }
168
169    /// Returns the frame rate as a rational number.
170    #[must_use]
171    #[inline]
172    pub const fn frame_rate(&self) -> Rational {
173        self.frame_rate
174    }
175
176    /// Returns the frame rate as frames per second (f64).
177    #[must_use]
178    #[inline]
179    pub fn fps(&self) -> f64 {
180        self.frame_rate.as_f64()
181    }
182
183    /// Returns the stream duration, if known.
184    #[must_use]
185    #[inline]
186    pub const fn duration(&self) -> Option<Duration> {
187        self.duration
188    }
189
190    /// Returns the bitrate in bits per second, if known.
191    #[must_use]
192    #[inline]
193    pub const fn bitrate(&self) -> Option<u64> {
194        self.bitrate
195    }
196
197    /// Returns the total number of frames, if known.
198    #[must_use]
199    #[inline]
200    pub const fn frame_count(&self) -> Option<u64> {
201        self.frame_count
202    }
203
204    /// Returns the color space (matrix coefficients).
205    #[must_use]
206    #[inline]
207    pub const fn color_space(&self) -> ColorSpace {
208        self.color_space
209    }
210
211    /// Returns the color range (limited/full).
212    #[must_use]
213    #[inline]
214    pub const fn color_range(&self) -> ColorRange {
215        self.color_range
216    }
217
218    /// Returns the color primaries.
219    #[must_use]
220    #[inline]
221    pub const fn color_primaries(&self) -> ColorPrimaries {
222        self.color_primaries
223    }
224
225    /// Returns the aspect ratio as width/height.
226    #[must_use]
227    #[inline]
228    pub fn aspect_ratio(&self) -> f64 {
229        if self.height == 0 {
230            0.0
231        } else {
232            f64::from(self.width) / f64::from(self.height)
233        }
234    }
235
236    /// Returns `true` if the video is HD (720p or higher).
237    #[must_use]
238    #[inline]
239    pub const fn is_hd(&self) -> bool {
240        self.height >= 720
241    }
242
243    /// Returns `true` if the video is Full HD (1080p or higher).
244    #[must_use]
245    #[inline]
246    pub const fn is_full_hd(&self) -> bool {
247        self.height >= 1080
248    }
249
250    /// Returns `true` if the video is 4K UHD (2160p or higher).
251    #[must_use]
252    #[inline]
253    pub const fn is_4k(&self) -> bool {
254        self.height >= 2160
255    }
256
257    /// Returns `true` if this video stream appears to be HDR (High Dynamic Range).
258    ///
259    /// HDR detection is based on two primary indicators:
260    /// 1. **Wide color gamut**: BT.2020 color primaries
261    /// 2. **High bit depth**: 10-bit or higher pixel format
262    ///
263    /// Both conditions must be met for a stream to be considered HDR.
264    /// This is a heuristic detection - for definitive HDR identification,
265    /// additional metadata like transfer characteristics (PQ/HLG) should be checked.
266    ///
267    /// # Examples
268    ///
269    /// ```
270    /// use ff_format::stream::VideoStreamInfo;
271    /// use ff_format::color::ColorPrimaries;
272    /// use ff_format::PixelFormat;
273    ///
274    /// let hdr_video = VideoStreamInfo::builder()
275    ///     .width(3840)
276    ///     .height(2160)
277    ///     .color_primaries(ColorPrimaries::Bt2020)
278    ///     .pixel_format(PixelFormat::Yuv420p10le)
279    ///     .build();
280    ///
281    /// assert!(hdr_video.is_hdr());
282    ///
283    /// // Standard HD video with BT.709 is not HDR
284    /// let sdr_video = VideoStreamInfo::builder()
285    ///     .width(1920)
286    ///     .height(1080)
287    ///     .color_primaries(ColorPrimaries::Bt709)
288    ///     .pixel_format(PixelFormat::Yuv420p)
289    ///     .build();
290    ///
291    /// assert!(!sdr_video.is_hdr());
292    /// ```
293    #[must_use]
294    #[inline]
295    pub fn is_hdr(&self) -> bool {
296        // HDR requires wide color gamut (BT.2020) and high bit depth (10-bit or higher)
297        self.color_primaries.is_wide_gamut() && self.pixel_format.is_high_bit_depth()
298    }
299}
300
301impl Default for VideoStreamInfo {
302    fn default() -> Self {
303        Self {
304            index: 0,
305            codec: VideoCodec::default(),
306            codec_name: String::new(),
307            width: 0,
308            height: 0,
309            pixel_format: PixelFormat::default(),
310            frame_rate: Rational::new(30, 1),
311            duration: None,
312            bitrate: None,
313            frame_count: None,
314            color_space: ColorSpace::default(),
315            color_range: ColorRange::default(),
316            color_primaries: ColorPrimaries::default(),
317        }
318    }
319}
320
321/// Builder for constructing `VideoStreamInfo`.
322#[derive(Debug, Clone, Default)]
323pub struct VideoStreamInfoBuilder {
324    index: u32,
325    codec: VideoCodec,
326    codec_name: String,
327    width: u32,
328    height: u32,
329    pixel_format: PixelFormat,
330    frame_rate: Rational,
331    duration: Option<Duration>,
332    bitrate: Option<u64>,
333    frame_count: Option<u64>,
334    color_space: ColorSpace,
335    color_range: ColorRange,
336    color_primaries: ColorPrimaries,
337}
338
339impl VideoStreamInfoBuilder {
340    /// Sets the stream index.
341    #[must_use]
342    pub fn index(mut self, index: u32) -> Self {
343        self.index = index;
344        self
345    }
346
347    /// Sets the video codec.
348    #[must_use]
349    pub fn codec(mut self, codec: VideoCodec) -> Self {
350        self.codec = codec;
351        self
352    }
353
354    /// Sets the codec name string.
355    #[must_use]
356    pub fn codec_name(mut self, name: impl Into<String>) -> Self {
357        self.codec_name = name.into();
358        self
359    }
360
361    /// Sets the frame width in pixels.
362    #[must_use]
363    pub fn width(mut self, width: u32) -> Self {
364        self.width = width;
365        self
366    }
367
368    /// Sets the frame height in pixels.
369    #[must_use]
370    pub fn height(mut self, height: u32) -> Self {
371        self.height = height;
372        self
373    }
374
375    /// Sets the pixel format.
376    #[must_use]
377    pub fn pixel_format(mut self, format: PixelFormat) -> Self {
378        self.pixel_format = format;
379        self
380    }
381
382    /// Sets the frame rate.
383    #[must_use]
384    pub fn frame_rate(mut self, rate: Rational) -> Self {
385        self.frame_rate = rate;
386        self
387    }
388
389    /// Sets the stream duration.
390    #[must_use]
391    pub fn duration(mut self, duration: Duration) -> Self {
392        self.duration = Some(duration);
393        self
394    }
395
396    /// Sets the bitrate in bits per second.
397    #[must_use]
398    pub fn bitrate(mut self, bitrate: u64) -> Self {
399        self.bitrate = Some(bitrate);
400        self
401    }
402
403    /// Sets the total frame count.
404    #[must_use]
405    pub fn frame_count(mut self, count: u64) -> Self {
406        self.frame_count = Some(count);
407        self
408    }
409
410    /// Sets the color space.
411    #[must_use]
412    pub fn color_space(mut self, space: ColorSpace) -> Self {
413        self.color_space = space;
414        self
415    }
416
417    /// Sets the color range.
418    #[must_use]
419    pub fn color_range(mut self, range: ColorRange) -> Self {
420        self.color_range = range;
421        self
422    }
423
424    /// Sets the color primaries.
425    #[must_use]
426    pub fn color_primaries(mut self, primaries: ColorPrimaries) -> Self {
427        self.color_primaries = primaries;
428        self
429    }
430
431    /// Builds the `VideoStreamInfo`.
432    #[must_use]
433    pub fn build(self) -> VideoStreamInfo {
434        VideoStreamInfo {
435            index: self.index,
436            codec: self.codec,
437            codec_name: self.codec_name,
438            width: self.width,
439            height: self.height,
440            pixel_format: self.pixel_format,
441            frame_rate: self.frame_rate,
442            duration: self.duration,
443            bitrate: self.bitrate,
444            frame_count: self.frame_count,
445            color_space: self.color_space,
446            color_range: self.color_range,
447            color_primaries: self.color_primaries,
448        }
449    }
450}
451
452/// Information about an audio stream within a media file.
453///
454/// This struct contains all metadata needed to understand and process
455/// an audio stream, including sample rate, channel layout, and codec
456/// information.
457///
458/// # Construction
459///
460/// Use [`AudioStreamInfo::builder()`] for fluent construction:
461///
462/// ```
463/// use ff_format::stream::AudioStreamInfo;
464/// use ff_format::SampleFormat;
465/// use ff_format::codec::AudioCodec;
466///
467/// let info = AudioStreamInfo::builder()
468///     .index(1)
469///     .codec(AudioCodec::Aac)
470///     .sample_rate(48000)
471///     .channels(2)
472///     .build();
473/// ```
474#[derive(Debug, Clone)]
475pub struct AudioStreamInfo {
476    /// Stream index within the container
477    index: u32,
478    /// Audio codec
479    codec: AudioCodec,
480    /// Codec name as reported by the demuxer
481    codec_name: String,
482    /// Sample rate in Hz
483    sample_rate: u32,
484    /// Number of channels
485    channels: u32,
486    /// Channel layout
487    channel_layout: ChannelLayout,
488    /// Sample format
489    sample_format: SampleFormat,
490    /// Stream duration (if known)
491    duration: Option<Duration>,
492    /// Bitrate in bits per second (if known)
493    bitrate: Option<u64>,
494    /// Language code (e.g., "eng", "jpn")
495    language: Option<String>,
496}
497
498impl AudioStreamInfo {
499    /// Creates a new builder for constructing `AudioStreamInfo`.
500    ///
501    /// # Examples
502    ///
503    /// ```
504    /// use ff_format::stream::AudioStreamInfo;
505    /// use ff_format::codec::AudioCodec;
506    /// use ff_format::SampleFormat;
507    ///
508    /// let info = AudioStreamInfo::builder()
509    ///     .index(1)
510    ///     .codec(AudioCodec::Aac)
511    ///     .sample_rate(48000)
512    ///     .channels(2)
513    ///     .build();
514    /// ```
515    #[must_use]
516    pub fn builder() -> AudioStreamInfoBuilder {
517        AudioStreamInfoBuilder::default()
518    }
519
520    /// Returns the stream index within the container.
521    #[must_use]
522    #[inline]
523    pub const fn index(&self) -> u32 {
524        self.index
525    }
526
527    /// Returns the audio codec.
528    #[must_use]
529    #[inline]
530    pub const fn codec(&self) -> AudioCodec {
531        self.codec
532    }
533
534    /// Returns the codec name as reported by the demuxer.
535    #[must_use]
536    #[inline]
537    pub fn codec_name(&self) -> &str {
538        &self.codec_name
539    }
540
541    /// Returns the sample rate in Hz.
542    #[must_use]
543    #[inline]
544    pub const fn sample_rate(&self) -> u32 {
545        self.sample_rate
546    }
547
548    /// Returns the number of audio channels.
549    #[must_use]
550    #[inline]
551    pub const fn channels(&self) -> u32 {
552        self.channels
553    }
554
555    /// Returns the channel layout.
556    #[must_use]
557    #[inline]
558    pub const fn channel_layout(&self) -> ChannelLayout {
559        self.channel_layout
560    }
561
562    /// Returns the sample format.
563    #[must_use]
564    #[inline]
565    pub const fn sample_format(&self) -> SampleFormat {
566        self.sample_format
567    }
568
569    /// Returns the stream duration, if known.
570    #[must_use]
571    #[inline]
572    pub const fn duration(&self) -> Option<Duration> {
573        self.duration
574    }
575
576    /// Returns the bitrate in bits per second, if known.
577    #[must_use]
578    #[inline]
579    pub const fn bitrate(&self) -> Option<u64> {
580        self.bitrate
581    }
582
583    /// Returns the language code, if specified.
584    #[must_use]
585    #[inline]
586    pub fn language(&self) -> Option<&str> {
587        self.language.as_deref()
588    }
589
590    /// Returns `true` if this is a mono stream.
591    #[must_use]
592    #[inline]
593    pub const fn is_mono(&self) -> bool {
594        self.channels == 1
595    }
596
597    /// Returns `true` if this is a stereo stream.
598    #[must_use]
599    #[inline]
600    pub const fn is_stereo(&self) -> bool {
601        self.channels == 2
602    }
603
604    /// Returns `true` if this is a surround sound stream (more than 2 channels).
605    #[must_use]
606    #[inline]
607    pub const fn is_surround(&self) -> bool {
608        self.channels > 2
609    }
610}
611
612impl Default for AudioStreamInfo {
613    fn default() -> Self {
614        Self {
615            index: 0,
616            codec: AudioCodec::default(),
617            codec_name: String::new(),
618            sample_rate: 48000,
619            channels: 2,
620            channel_layout: ChannelLayout::default(),
621            sample_format: SampleFormat::default(),
622            duration: None,
623            bitrate: None,
624            language: None,
625        }
626    }
627}
628
629/// Builder for constructing `AudioStreamInfo`.
630#[derive(Debug, Clone, Default)]
631pub struct AudioStreamInfoBuilder {
632    index: u32,
633    codec: AudioCodec,
634    codec_name: String,
635    sample_rate: u32,
636    channels: u32,
637    channel_layout: Option<ChannelLayout>,
638    sample_format: SampleFormat,
639    duration: Option<Duration>,
640    bitrate: Option<u64>,
641    language: Option<String>,
642}
643
644impl AudioStreamInfoBuilder {
645    /// Sets the stream index.
646    #[must_use]
647    pub fn index(mut self, index: u32) -> Self {
648        self.index = index;
649        self
650    }
651
652    /// Sets the audio codec.
653    #[must_use]
654    pub fn codec(mut self, codec: AudioCodec) -> Self {
655        self.codec = codec;
656        self
657    }
658
659    /// Sets the codec name string.
660    #[must_use]
661    pub fn codec_name(mut self, name: impl Into<String>) -> Self {
662        self.codec_name = name.into();
663        self
664    }
665
666    /// Sets the sample rate in Hz.
667    #[must_use]
668    pub fn sample_rate(mut self, rate: u32) -> Self {
669        self.sample_rate = rate;
670        self
671    }
672
673    /// Sets the number of channels.
674    ///
675    /// This also updates the channel layout if not explicitly set.
676    #[must_use]
677    pub fn channels(mut self, channels: u32) -> Self {
678        self.channels = channels;
679        self
680    }
681
682    /// Sets the channel layout explicitly.
683    #[must_use]
684    pub fn channel_layout(mut self, layout: ChannelLayout) -> Self {
685        self.channel_layout = Some(layout);
686        self
687    }
688
689    /// Sets the sample format.
690    #[must_use]
691    pub fn sample_format(mut self, format: SampleFormat) -> Self {
692        self.sample_format = format;
693        self
694    }
695
696    /// Sets the stream duration.
697    #[must_use]
698    pub fn duration(mut self, duration: Duration) -> Self {
699        self.duration = Some(duration);
700        self
701    }
702
703    /// Sets the bitrate in bits per second.
704    #[must_use]
705    pub fn bitrate(mut self, bitrate: u64) -> Self {
706        self.bitrate = Some(bitrate);
707        self
708    }
709
710    /// Sets the language code.
711    #[must_use]
712    pub fn language(mut self, lang: impl Into<String>) -> Self {
713        self.language = Some(lang.into());
714        self
715    }
716
717    /// Builds the `AudioStreamInfo`.
718    #[must_use]
719    pub fn build(self) -> AudioStreamInfo {
720        let channel_layout = self
721            .channel_layout
722            .unwrap_or_else(|| ChannelLayout::from_channels(self.channels));
723
724        AudioStreamInfo {
725            index: self.index,
726            codec: self.codec,
727            codec_name: self.codec_name,
728            sample_rate: self.sample_rate,
729            channels: self.channels,
730            channel_layout,
731            sample_format: self.sample_format,
732            duration: self.duration,
733            bitrate: self.bitrate,
734            language: self.language,
735        }
736    }
737}
738
739#[cfg(test)]
740mod tests {
741    use super::*;
742
743    mod video_stream_info_tests {
744        use super::*;
745
746        #[test]
747        fn test_builder_basic() {
748            let info = VideoStreamInfo::builder()
749                .index(0)
750                .codec(VideoCodec::H264)
751                .codec_name("h264")
752                .width(1920)
753                .height(1080)
754                .frame_rate(Rational::new(30, 1))
755                .pixel_format(PixelFormat::Yuv420p)
756                .build();
757
758            assert_eq!(info.index(), 0);
759            assert_eq!(info.codec(), VideoCodec::H264);
760            assert_eq!(info.codec_name(), "h264");
761            assert_eq!(info.width(), 1920);
762            assert_eq!(info.height(), 1080);
763            assert!((info.fps() - 30.0).abs() < 0.001);
764            assert_eq!(info.pixel_format(), PixelFormat::Yuv420p);
765        }
766
767        #[test]
768        fn test_builder_full() {
769            let info = VideoStreamInfo::builder()
770                .index(0)
771                .codec(VideoCodec::H265)
772                .codec_name("hevc")
773                .width(3840)
774                .height(2160)
775                .frame_rate(Rational::new(60, 1))
776                .pixel_format(PixelFormat::Yuv420p10le)
777                .duration(Duration::from_secs(120))
778                .bitrate(50_000_000)
779                .frame_count(7200)
780                .color_space(ColorSpace::Bt2020)
781                .color_range(ColorRange::Full)
782                .color_primaries(ColorPrimaries::Bt2020)
783                .build();
784
785            assert_eq!(info.codec(), VideoCodec::H265);
786            assert_eq!(info.width(), 3840);
787            assert_eq!(info.height(), 2160);
788            assert_eq!(info.duration(), Some(Duration::from_secs(120)));
789            assert_eq!(info.bitrate(), Some(50_000_000));
790            assert_eq!(info.frame_count(), Some(7200));
791            assert_eq!(info.color_space(), ColorSpace::Bt2020);
792            assert_eq!(info.color_range(), ColorRange::Full);
793            assert_eq!(info.color_primaries(), ColorPrimaries::Bt2020);
794        }
795
796        #[test]
797        fn test_default() {
798            let info = VideoStreamInfo::default();
799            assert_eq!(info.index(), 0);
800            assert_eq!(info.codec(), VideoCodec::default());
801            assert_eq!(info.width(), 0);
802            assert_eq!(info.height(), 0);
803            assert!(info.duration().is_none());
804        }
805
806        #[test]
807        fn test_aspect_ratio() {
808            let info = VideoStreamInfo::builder().width(1920).height(1080).build();
809            assert!((info.aspect_ratio() - (16.0 / 9.0)).abs() < 0.01);
810
811            let info = VideoStreamInfo::builder().width(1280).height(720).build();
812            assert!((info.aspect_ratio() - (16.0 / 9.0)).abs() < 0.01);
813
814            // Zero height
815            let info = VideoStreamInfo::builder().width(1920).height(0).build();
816            assert_eq!(info.aspect_ratio(), 0.0);
817        }
818
819        #[test]
820        fn test_resolution_checks() {
821            // SD
822            let sd = VideoStreamInfo::builder().width(720).height(480).build();
823            assert!(!sd.is_hd());
824            assert!(!sd.is_full_hd());
825            assert!(!sd.is_4k());
826
827            // HD
828            let hd = VideoStreamInfo::builder().width(1280).height(720).build();
829            assert!(hd.is_hd());
830            assert!(!hd.is_full_hd());
831            assert!(!hd.is_4k());
832
833            // Full HD
834            let fhd = VideoStreamInfo::builder().width(1920).height(1080).build();
835            assert!(fhd.is_hd());
836            assert!(fhd.is_full_hd());
837            assert!(!fhd.is_4k());
838
839            // 4K
840            let uhd = VideoStreamInfo::builder().width(3840).height(2160).build();
841            assert!(uhd.is_hd());
842            assert!(uhd.is_full_hd());
843            assert!(uhd.is_4k());
844        }
845
846        #[test]
847        fn test_is_hdr() {
848            // HDR video: BT.2020 color primaries + 10-bit pixel format
849            let hdr = VideoStreamInfo::builder()
850                .width(3840)
851                .height(2160)
852                .color_primaries(ColorPrimaries::Bt2020)
853                .pixel_format(PixelFormat::Yuv420p10le)
854                .build();
855            assert!(hdr.is_hdr());
856
857            // HDR video with P010le format
858            let hdr_p010 = VideoStreamInfo::builder()
859                .width(3840)
860                .height(2160)
861                .color_primaries(ColorPrimaries::Bt2020)
862                .pixel_format(PixelFormat::P010le)
863                .build();
864            assert!(hdr_p010.is_hdr());
865
866            // SDR video: BT.709 color primaries (standard HD)
867            let sdr_hd = VideoStreamInfo::builder()
868                .width(1920)
869                .height(1080)
870                .color_primaries(ColorPrimaries::Bt709)
871                .pixel_format(PixelFormat::Yuv420p)
872                .build();
873            assert!(!sdr_hd.is_hdr());
874
875            // BT.2020 but 8-bit (not HDR - missing high bit depth)
876            let wide_gamut_8bit = VideoStreamInfo::builder()
877                .width(3840)
878                .height(2160)
879                .color_primaries(ColorPrimaries::Bt2020)
880                .pixel_format(PixelFormat::Yuv420p) // 8-bit
881                .build();
882            assert!(!wide_gamut_8bit.is_hdr());
883
884            // 10-bit but BT.709 (not HDR - missing wide gamut)
885            let hd_10bit = VideoStreamInfo::builder()
886                .width(1920)
887                .height(1080)
888                .color_primaries(ColorPrimaries::Bt709)
889                .pixel_format(PixelFormat::Yuv420p10le)
890                .build();
891            assert!(!hd_10bit.is_hdr());
892
893            // Default video stream is not HDR
894            let default = VideoStreamInfo::default();
895            assert!(!default.is_hdr());
896        }
897
898        #[test]
899        fn test_debug() {
900            let info = VideoStreamInfo::builder()
901                .index(0)
902                .codec(VideoCodec::H264)
903                .width(1920)
904                .height(1080)
905                .build();
906            let debug = format!("{info:?}");
907            assert!(debug.contains("VideoStreamInfo"));
908            assert!(debug.contains("1920"));
909            assert!(debug.contains("1080"));
910        }
911
912        #[test]
913        fn test_clone() {
914            let info = VideoStreamInfo::builder()
915                .index(0)
916                .codec(VideoCodec::H264)
917                .codec_name("h264")
918                .width(1920)
919                .height(1080)
920                .build();
921            let cloned = info.clone();
922            assert_eq!(info.width(), cloned.width());
923            assert_eq!(info.height(), cloned.height());
924            assert_eq!(info.codec_name(), cloned.codec_name());
925        }
926    }
927
928    mod audio_stream_info_tests {
929        use super::*;
930
931        #[test]
932        fn test_builder_basic() {
933            let info = AudioStreamInfo::builder()
934                .index(1)
935                .codec(AudioCodec::Aac)
936                .codec_name("aac")
937                .sample_rate(48000)
938                .channels(2)
939                .sample_format(SampleFormat::F32)
940                .build();
941
942            assert_eq!(info.index(), 1);
943            assert_eq!(info.codec(), AudioCodec::Aac);
944            assert_eq!(info.codec_name(), "aac");
945            assert_eq!(info.sample_rate(), 48000);
946            assert_eq!(info.channels(), 2);
947            assert_eq!(info.sample_format(), SampleFormat::F32);
948            assert_eq!(info.channel_layout(), ChannelLayout::Stereo);
949        }
950
951        #[test]
952        fn test_builder_full() {
953            let info = AudioStreamInfo::builder()
954                .index(2)
955                .codec(AudioCodec::Flac)
956                .codec_name("flac")
957                .sample_rate(96000)
958                .channels(6)
959                .channel_layout(ChannelLayout::Surround5_1)
960                .sample_format(SampleFormat::I32)
961                .duration(Duration::from_secs(300))
962                .bitrate(1_411_200)
963                .language("jpn")
964                .build();
965
966            assert_eq!(info.codec(), AudioCodec::Flac);
967            assert_eq!(info.sample_rate(), 96000);
968            assert_eq!(info.channels(), 6);
969            assert_eq!(info.channel_layout(), ChannelLayout::Surround5_1);
970            assert_eq!(info.duration(), Some(Duration::from_secs(300)));
971            assert_eq!(info.bitrate(), Some(1_411_200));
972            assert_eq!(info.language(), Some("jpn"));
973        }
974
975        #[test]
976        fn test_default() {
977            let info = AudioStreamInfo::default();
978            assert_eq!(info.index(), 0);
979            assert_eq!(info.codec(), AudioCodec::default());
980            assert_eq!(info.sample_rate(), 48000);
981            assert_eq!(info.channels(), 2);
982            assert!(info.duration().is_none());
983        }
984
985        #[test]
986        fn test_auto_channel_layout() {
987            // Should auto-detect layout from channel count
988            let mono = AudioStreamInfo::builder().channels(1).build();
989            assert_eq!(mono.channel_layout(), ChannelLayout::Mono);
990
991            let stereo = AudioStreamInfo::builder().channels(2).build();
992            assert_eq!(stereo.channel_layout(), ChannelLayout::Stereo);
993
994            let surround = AudioStreamInfo::builder().channels(6).build();
995            assert_eq!(surround.channel_layout(), ChannelLayout::Surround5_1);
996
997            // Explicit layout should override
998            let custom = AudioStreamInfo::builder()
999                .channels(6)
1000                .channel_layout(ChannelLayout::Other(6))
1001                .build();
1002            assert_eq!(custom.channel_layout(), ChannelLayout::Other(6));
1003        }
1004
1005        #[test]
1006        fn test_channel_checks() {
1007            let mono = AudioStreamInfo::builder().channels(1).build();
1008            assert!(mono.is_mono());
1009            assert!(!mono.is_stereo());
1010            assert!(!mono.is_surround());
1011
1012            let stereo = AudioStreamInfo::builder().channels(2).build();
1013            assert!(!stereo.is_mono());
1014            assert!(stereo.is_stereo());
1015            assert!(!stereo.is_surround());
1016
1017            let surround = AudioStreamInfo::builder().channels(6).build();
1018            assert!(!surround.is_mono());
1019            assert!(!surround.is_stereo());
1020            assert!(surround.is_surround());
1021        }
1022
1023        #[test]
1024        fn test_debug() {
1025            let info = AudioStreamInfo::builder()
1026                .index(1)
1027                .codec(AudioCodec::Aac)
1028                .sample_rate(48000)
1029                .channels(2)
1030                .build();
1031            let debug = format!("{info:?}");
1032            assert!(debug.contains("AudioStreamInfo"));
1033            assert!(debug.contains("48000"));
1034        }
1035
1036        #[test]
1037        fn test_clone() {
1038            let info = AudioStreamInfo::builder()
1039                .index(1)
1040                .codec(AudioCodec::Aac)
1041                .codec_name("aac")
1042                .sample_rate(48000)
1043                .channels(2)
1044                .language("eng")
1045                .build();
1046            let cloned = info.clone();
1047            assert_eq!(info.sample_rate(), cloned.sample_rate());
1048            assert_eq!(info.channels(), cloned.channels());
1049            assert_eq!(info.language(), cloned.language());
1050            assert_eq!(info.codec_name(), cloned.codec_name());
1051        }
1052    }
1053}