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