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, SubtitleCodec, 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/// Information about a subtitle stream within a media file.
750///
751/// This struct contains all metadata needed to identify and categorize
752/// a subtitle stream, including codec, language, and forced flag.
753///
754/// # Construction
755///
756/// Use [`SubtitleStreamInfo::builder()`] for fluent construction:
757///
758/// ```
759/// use ff_format::stream::SubtitleStreamInfo;
760/// use ff_format::codec::SubtitleCodec;
761///
762/// let info = SubtitleStreamInfo::builder()
763///     .index(2)
764///     .codec(SubtitleCodec::Srt)
765///     .codec_name("srt")
766///     .language("eng")
767///     .build();
768/// ```
769#[derive(Debug, Clone)]
770pub struct SubtitleStreamInfo {
771    /// Stream index within the container
772    index: u32,
773    /// Subtitle codec
774    codec: SubtitleCodec,
775    /// Codec name as reported by the demuxer
776    codec_name: String,
777    /// Language code (e.g., "eng", "jpn")
778    language: Option<String>,
779    /// Stream title (e.g., "English (Forced)")
780    title: Option<String>,
781    /// Stream duration (if known)
782    duration: Option<Duration>,
783    /// Whether this is a forced subtitle track
784    forced: bool,
785}
786
787impl SubtitleStreamInfo {
788    /// Creates a new builder for constructing `SubtitleStreamInfo`.
789    #[must_use]
790    pub fn builder() -> SubtitleStreamInfoBuilder {
791        SubtitleStreamInfoBuilder::default()
792    }
793
794    /// Returns the stream index within the container.
795    #[must_use]
796    #[inline]
797    pub const fn index(&self) -> u32 {
798        self.index
799    }
800
801    /// Returns the subtitle codec.
802    #[must_use]
803    #[inline]
804    pub fn codec(&self) -> &SubtitleCodec {
805        &self.codec
806    }
807
808    /// Returns the codec name as reported by the demuxer.
809    #[must_use]
810    #[inline]
811    pub fn codec_name(&self) -> &str {
812        &self.codec_name
813    }
814
815    /// Returns the language code, if specified.
816    #[must_use]
817    #[inline]
818    pub fn language(&self) -> Option<&str> {
819        self.language.as_deref()
820    }
821
822    /// Returns the stream title, if specified.
823    #[must_use]
824    #[inline]
825    pub fn title(&self) -> Option<&str> {
826        self.title.as_deref()
827    }
828
829    /// Returns the stream duration, if known.
830    #[must_use]
831    #[inline]
832    pub const fn duration(&self) -> Option<Duration> {
833        self.duration
834    }
835
836    /// Returns `true` if this is a forced subtitle track.
837    #[must_use]
838    #[inline]
839    pub const fn is_forced(&self) -> bool {
840        self.forced
841    }
842
843    /// Returns `true` if the codec is text-based.
844    #[must_use]
845    #[inline]
846    pub fn is_text_based(&self) -> bool {
847        self.codec.is_text_based()
848    }
849}
850
851/// Builder for constructing `SubtitleStreamInfo`.
852#[derive(Debug, Clone)]
853pub struct SubtitleStreamInfoBuilder {
854    index: u32,
855    codec: SubtitleCodec,
856    codec_name: String,
857    language: Option<String>,
858    title: Option<String>,
859    duration: Option<Duration>,
860    forced: bool,
861}
862
863impl Default for SubtitleStreamInfoBuilder {
864    fn default() -> Self {
865        Self {
866            index: 0,
867            codec: SubtitleCodec::Other(String::new()),
868            codec_name: String::new(),
869            language: None,
870            title: None,
871            duration: None,
872            forced: false,
873        }
874    }
875}
876
877impl SubtitleStreamInfoBuilder {
878    /// Sets the stream index.
879    #[must_use]
880    pub fn index(mut self, index: u32) -> Self {
881        self.index = index;
882        self
883    }
884
885    /// Sets the subtitle codec.
886    #[must_use]
887    pub fn codec(mut self, codec: SubtitleCodec) -> Self {
888        self.codec = codec;
889        self
890    }
891
892    /// Sets the codec name string.
893    #[must_use]
894    pub fn codec_name(mut self, name: impl Into<String>) -> Self {
895        self.codec_name = name.into();
896        self
897    }
898
899    /// Sets the language code.
900    #[must_use]
901    pub fn language(mut self, lang: impl Into<String>) -> Self {
902        self.language = Some(lang.into());
903        self
904    }
905
906    /// Sets the stream title.
907    #[must_use]
908    pub fn title(mut self, title: impl Into<String>) -> Self {
909        self.title = Some(title.into());
910        self
911    }
912
913    /// Sets the stream duration.
914    #[must_use]
915    pub fn duration(mut self, duration: Duration) -> Self {
916        self.duration = Some(duration);
917        self
918    }
919
920    /// Sets the forced flag.
921    #[must_use]
922    pub fn forced(mut self, forced: bool) -> Self {
923        self.forced = forced;
924        self
925    }
926
927    /// Builds the `SubtitleStreamInfo`.
928    #[must_use]
929    pub fn build(self) -> SubtitleStreamInfo {
930        SubtitleStreamInfo {
931            index: self.index,
932            codec: self.codec,
933            codec_name: self.codec_name,
934            language: self.language,
935            title: self.title,
936            duration: self.duration,
937            forced: self.forced,
938        }
939    }
940}
941
942#[cfg(test)]
943mod tests {
944    use super::*;
945
946    mod video_stream_info_tests {
947        use super::*;
948
949        #[test]
950        fn test_builder_basic() {
951            let info = VideoStreamInfo::builder()
952                .index(0)
953                .codec(VideoCodec::H264)
954                .codec_name("h264")
955                .width(1920)
956                .height(1080)
957                .frame_rate(Rational::new(30, 1))
958                .pixel_format(PixelFormat::Yuv420p)
959                .build();
960
961            assert_eq!(info.index(), 0);
962            assert_eq!(info.codec(), VideoCodec::H264);
963            assert_eq!(info.codec_name(), "h264");
964            assert_eq!(info.width(), 1920);
965            assert_eq!(info.height(), 1080);
966            assert!((info.fps() - 30.0).abs() < 0.001);
967            assert_eq!(info.pixel_format(), PixelFormat::Yuv420p);
968        }
969
970        #[test]
971        fn test_builder_full() {
972            let info = VideoStreamInfo::builder()
973                .index(0)
974                .codec(VideoCodec::H265)
975                .codec_name("hevc")
976                .width(3840)
977                .height(2160)
978                .frame_rate(Rational::new(60, 1))
979                .pixel_format(PixelFormat::Yuv420p10le)
980                .duration(Duration::from_secs(120))
981                .bitrate(50_000_000)
982                .frame_count(7200)
983                .color_space(ColorSpace::Bt2020)
984                .color_range(ColorRange::Full)
985                .color_primaries(ColorPrimaries::Bt2020)
986                .build();
987
988            assert_eq!(info.codec(), VideoCodec::H265);
989            assert_eq!(info.width(), 3840);
990            assert_eq!(info.height(), 2160);
991            assert_eq!(info.duration(), Some(Duration::from_secs(120)));
992            assert_eq!(info.bitrate(), Some(50_000_000));
993            assert_eq!(info.frame_count(), Some(7200));
994            assert_eq!(info.color_space(), ColorSpace::Bt2020);
995            assert_eq!(info.color_range(), ColorRange::Full);
996            assert_eq!(info.color_primaries(), ColorPrimaries::Bt2020);
997        }
998
999        #[test]
1000        fn test_default() {
1001            let info = VideoStreamInfo::default();
1002            assert_eq!(info.index(), 0);
1003            assert_eq!(info.codec(), VideoCodec::default());
1004            assert_eq!(info.width(), 0);
1005            assert_eq!(info.height(), 0);
1006            assert!(info.duration().is_none());
1007        }
1008
1009        #[test]
1010        fn test_aspect_ratio() {
1011            let info = VideoStreamInfo::builder().width(1920).height(1080).build();
1012            assert!((info.aspect_ratio() - (16.0 / 9.0)).abs() < 0.01);
1013
1014            let info = VideoStreamInfo::builder().width(1280).height(720).build();
1015            assert!((info.aspect_ratio() - (16.0 / 9.0)).abs() < 0.01);
1016
1017            // Zero height
1018            let info = VideoStreamInfo::builder().width(1920).height(0).build();
1019            assert_eq!(info.aspect_ratio(), 0.0);
1020        }
1021
1022        #[test]
1023        fn test_resolution_checks() {
1024            // SD
1025            let sd = VideoStreamInfo::builder().width(720).height(480).build();
1026            assert!(!sd.is_hd());
1027            assert!(!sd.is_full_hd());
1028            assert!(!sd.is_4k());
1029
1030            // HD
1031            let hd = VideoStreamInfo::builder().width(1280).height(720).build();
1032            assert!(hd.is_hd());
1033            assert!(!hd.is_full_hd());
1034            assert!(!hd.is_4k());
1035
1036            // Full HD
1037            let fhd = VideoStreamInfo::builder().width(1920).height(1080).build();
1038            assert!(fhd.is_hd());
1039            assert!(fhd.is_full_hd());
1040            assert!(!fhd.is_4k());
1041
1042            // 4K
1043            let uhd = VideoStreamInfo::builder().width(3840).height(2160).build();
1044            assert!(uhd.is_hd());
1045            assert!(uhd.is_full_hd());
1046            assert!(uhd.is_4k());
1047        }
1048
1049        #[test]
1050        fn test_is_hdr() {
1051            // HDR video: BT.2020 color primaries + 10-bit pixel format
1052            let hdr = VideoStreamInfo::builder()
1053                .width(3840)
1054                .height(2160)
1055                .color_primaries(ColorPrimaries::Bt2020)
1056                .pixel_format(PixelFormat::Yuv420p10le)
1057                .build();
1058            assert!(hdr.is_hdr());
1059
1060            // HDR video with P010le format
1061            let hdr_p010 = VideoStreamInfo::builder()
1062                .width(3840)
1063                .height(2160)
1064                .color_primaries(ColorPrimaries::Bt2020)
1065                .pixel_format(PixelFormat::P010le)
1066                .build();
1067            assert!(hdr_p010.is_hdr());
1068
1069            // SDR video: BT.709 color primaries (standard HD)
1070            let sdr_hd = VideoStreamInfo::builder()
1071                .width(1920)
1072                .height(1080)
1073                .color_primaries(ColorPrimaries::Bt709)
1074                .pixel_format(PixelFormat::Yuv420p)
1075                .build();
1076            assert!(!sdr_hd.is_hdr());
1077
1078            // BT.2020 but 8-bit (not HDR - missing high bit depth)
1079            let wide_gamut_8bit = VideoStreamInfo::builder()
1080                .width(3840)
1081                .height(2160)
1082                .color_primaries(ColorPrimaries::Bt2020)
1083                .pixel_format(PixelFormat::Yuv420p) // 8-bit
1084                .build();
1085            assert!(!wide_gamut_8bit.is_hdr());
1086
1087            // 10-bit but BT.709 (not HDR - missing wide gamut)
1088            let hd_10bit = VideoStreamInfo::builder()
1089                .width(1920)
1090                .height(1080)
1091                .color_primaries(ColorPrimaries::Bt709)
1092                .pixel_format(PixelFormat::Yuv420p10le)
1093                .build();
1094            assert!(!hd_10bit.is_hdr());
1095
1096            // Default video stream is not HDR
1097            let default = VideoStreamInfo::default();
1098            assert!(!default.is_hdr());
1099        }
1100
1101        #[test]
1102        fn test_debug() {
1103            let info = VideoStreamInfo::builder()
1104                .index(0)
1105                .codec(VideoCodec::H264)
1106                .width(1920)
1107                .height(1080)
1108                .build();
1109            let debug = format!("{info:?}");
1110            assert!(debug.contains("VideoStreamInfo"));
1111            assert!(debug.contains("1920"));
1112            assert!(debug.contains("1080"));
1113        }
1114
1115        #[test]
1116        fn test_clone() {
1117            let info = VideoStreamInfo::builder()
1118                .index(0)
1119                .codec(VideoCodec::H264)
1120                .codec_name("h264")
1121                .width(1920)
1122                .height(1080)
1123                .build();
1124            let cloned = info.clone();
1125            assert_eq!(info.width(), cloned.width());
1126            assert_eq!(info.height(), cloned.height());
1127            assert_eq!(info.codec_name(), cloned.codec_name());
1128        }
1129    }
1130
1131    mod audio_stream_info_tests {
1132        use super::*;
1133
1134        #[test]
1135        fn test_builder_basic() {
1136            let info = AudioStreamInfo::builder()
1137                .index(1)
1138                .codec(AudioCodec::Aac)
1139                .codec_name("aac")
1140                .sample_rate(48000)
1141                .channels(2)
1142                .sample_format(SampleFormat::F32)
1143                .build();
1144
1145            assert_eq!(info.index(), 1);
1146            assert_eq!(info.codec(), AudioCodec::Aac);
1147            assert_eq!(info.codec_name(), "aac");
1148            assert_eq!(info.sample_rate(), 48000);
1149            assert_eq!(info.channels(), 2);
1150            assert_eq!(info.sample_format(), SampleFormat::F32);
1151            assert_eq!(info.channel_layout(), ChannelLayout::Stereo);
1152        }
1153
1154        #[test]
1155        fn test_builder_full() {
1156            let info = AudioStreamInfo::builder()
1157                .index(2)
1158                .codec(AudioCodec::Flac)
1159                .codec_name("flac")
1160                .sample_rate(96000)
1161                .channels(6)
1162                .channel_layout(ChannelLayout::Surround5_1)
1163                .sample_format(SampleFormat::I32)
1164                .duration(Duration::from_secs(300))
1165                .bitrate(1_411_200)
1166                .language("jpn")
1167                .build();
1168
1169            assert_eq!(info.codec(), AudioCodec::Flac);
1170            assert_eq!(info.sample_rate(), 96000);
1171            assert_eq!(info.channels(), 6);
1172            assert_eq!(info.channel_layout(), ChannelLayout::Surround5_1);
1173            assert_eq!(info.duration(), Some(Duration::from_secs(300)));
1174            assert_eq!(info.bitrate(), Some(1_411_200));
1175            assert_eq!(info.language(), Some("jpn"));
1176        }
1177
1178        #[test]
1179        fn test_default() {
1180            let info = AudioStreamInfo::default();
1181            assert_eq!(info.index(), 0);
1182            assert_eq!(info.codec(), AudioCodec::default());
1183            assert_eq!(info.sample_rate(), 48000);
1184            assert_eq!(info.channels(), 2);
1185            assert!(info.duration().is_none());
1186        }
1187
1188        #[test]
1189        fn test_auto_channel_layout() {
1190            // Should auto-detect layout from channel count
1191            let mono = AudioStreamInfo::builder().channels(1).build();
1192            assert_eq!(mono.channel_layout(), ChannelLayout::Mono);
1193
1194            let stereo = AudioStreamInfo::builder().channels(2).build();
1195            assert_eq!(stereo.channel_layout(), ChannelLayout::Stereo);
1196
1197            let surround = AudioStreamInfo::builder().channels(6).build();
1198            assert_eq!(surround.channel_layout(), ChannelLayout::Surround5_1);
1199
1200            // Explicit layout should override
1201            let custom = AudioStreamInfo::builder()
1202                .channels(6)
1203                .channel_layout(ChannelLayout::Other(6))
1204                .build();
1205            assert_eq!(custom.channel_layout(), ChannelLayout::Other(6));
1206        }
1207
1208        #[test]
1209        fn test_channel_checks() {
1210            let mono = AudioStreamInfo::builder().channels(1).build();
1211            assert!(mono.is_mono());
1212            assert!(!mono.is_stereo());
1213            assert!(!mono.is_surround());
1214
1215            let stereo = AudioStreamInfo::builder().channels(2).build();
1216            assert!(!stereo.is_mono());
1217            assert!(stereo.is_stereo());
1218            assert!(!stereo.is_surround());
1219
1220            let surround = AudioStreamInfo::builder().channels(6).build();
1221            assert!(!surround.is_mono());
1222            assert!(!surround.is_stereo());
1223            assert!(surround.is_surround());
1224        }
1225
1226        #[test]
1227        fn test_debug() {
1228            let info = AudioStreamInfo::builder()
1229                .index(1)
1230                .codec(AudioCodec::Aac)
1231                .sample_rate(48000)
1232                .channels(2)
1233                .build();
1234            let debug = format!("{info:?}");
1235            assert!(debug.contains("AudioStreamInfo"));
1236            assert!(debug.contains("48000"));
1237        }
1238
1239        #[test]
1240        fn test_clone() {
1241            let info = AudioStreamInfo::builder()
1242                .index(1)
1243                .codec(AudioCodec::Aac)
1244                .codec_name("aac")
1245                .sample_rate(48000)
1246                .channels(2)
1247                .language("eng")
1248                .build();
1249            let cloned = info.clone();
1250            assert_eq!(info.sample_rate(), cloned.sample_rate());
1251            assert_eq!(info.channels(), cloned.channels());
1252            assert_eq!(info.language(), cloned.language());
1253            assert_eq!(info.codec_name(), cloned.codec_name());
1254        }
1255    }
1256
1257    mod subtitle_stream_info_tests {
1258        use super::*;
1259
1260        #[test]
1261        fn builder_should_store_all_fields() {
1262            let info = SubtitleStreamInfo::builder()
1263                .index(2)
1264                .codec(SubtitleCodec::Srt)
1265                .codec_name("srt")
1266                .language("eng")
1267                .title("English")
1268                .duration(Duration::from_secs(120))
1269                .forced(true)
1270                .build();
1271
1272            assert_eq!(info.index(), 2);
1273            assert_eq!(info.codec(), &SubtitleCodec::Srt);
1274            assert_eq!(info.codec_name(), "srt");
1275            assert_eq!(info.language(), Some("eng"));
1276            assert_eq!(info.title(), Some("English"));
1277            assert_eq!(info.duration(), Some(Duration::from_secs(120)));
1278            assert!(info.is_forced());
1279        }
1280
1281        #[test]
1282        fn is_forced_should_default_to_false() {
1283            let info = SubtitleStreamInfo::builder()
1284                .codec(SubtitleCodec::Ass)
1285                .build();
1286            assert!(!info.is_forced());
1287        }
1288
1289        #[test]
1290        fn is_text_based_should_delegate_to_codec() {
1291            let text = SubtitleStreamInfo::builder()
1292                .codec(SubtitleCodec::Srt)
1293                .build();
1294            assert!(text.is_text_based());
1295
1296            let bitmap = SubtitleStreamInfo::builder()
1297                .codec(SubtitleCodec::Hdmv)
1298                .build();
1299            assert!(!bitmap.is_text_based());
1300        }
1301
1302        #[test]
1303        fn optional_fields_should_default_to_none() {
1304            let info = SubtitleStreamInfo::builder().build();
1305            assert!(info.language().is_none());
1306            assert!(info.title().is_none());
1307            assert!(info.duration().is_none());
1308        }
1309    }
1310}