Skip to main content

ff_format/stream/
video.rs

1//! Video stream info and builder.
2
3use std::time::Duration;
4
5use crate::codec::VideoCodec;
6use crate::color::{ColorPrimaries, ColorRange, ColorSpace};
7use crate::pixel::PixelFormat;
8use crate::time::Rational;
9
10/// Information about a video stream within a media file.
11///
12/// This struct contains all metadata needed to understand and process
13/// a video stream, including resolution, codec, frame rate, and color
14/// characteristics.
15///
16/// # Construction
17///
18/// Use [`VideoStreamInfo::builder()`] for fluent construction:
19///
20/// ```
21/// use ff_format::stream::VideoStreamInfo;
22/// use ff_format::{PixelFormat, Rational};
23/// use ff_format::codec::VideoCodec;
24///
25/// let info = VideoStreamInfo::builder()
26///     .index(0)
27///     .codec(VideoCodec::H264)
28///     .width(1920)
29///     .height(1080)
30///     .frame_rate(Rational::new(30, 1))
31///     .build();
32/// ```
33#[derive(Debug, Clone)]
34pub struct VideoStreamInfo {
35    /// Stream index within the container
36    index: u32,
37    /// Video codec
38    codec: VideoCodec,
39    /// Codec name as reported by the demuxer
40    codec_name: String,
41    /// Frame width in pixels
42    width: u32,
43    /// Frame height in pixels
44    height: u32,
45    /// Pixel format
46    pixel_format: PixelFormat,
47    /// Frame rate (frames per second)
48    frame_rate: Rational,
49    /// Stream duration (if known)
50    duration: Option<Duration>,
51    /// Bitrate in bits per second (if known)
52    bitrate: Option<u64>,
53    /// Total number of frames (if known)
54    frame_count: Option<u64>,
55    /// Color space (matrix coefficients)
56    color_space: ColorSpace,
57    /// Color range (limited/full)
58    color_range: ColorRange,
59    /// Color primaries
60    color_primaries: ColorPrimaries,
61}
62
63impl VideoStreamInfo {
64    /// Creates a new builder for constructing `VideoStreamInfo`.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// use ff_format::stream::VideoStreamInfo;
70    /// use ff_format::codec::VideoCodec;
71    /// use ff_format::{PixelFormat, Rational};
72    ///
73    /// let info = VideoStreamInfo::builder()
74    ///     .index(0)
75    ///     .codec(VideoCodec::H264)
76    ///     .width(1920)
77    ///     .height(1080)
78    ///     .frame_rate(Rational::new(30, 1))
79    ///     .build();
80    /// ```
81    #[must_use]
82    pub fn builder() -> VideoStreamInfoBuilder {
83        VideoStreamInfoBuilder::default()
84    }
85
86    /// Returns the stream index within the container.
87    #[must_use]
88    #[inline]
89    pub const fn index(&self) -> u32 {
90        self.index
91    }
92
93    /// Returns the video codec.
94    #[must_use]
95    #[inline]
96    pub const fn codec(&self) -> VideoCodec {
97        self.codec
98    }
99
100    /// Returns the codec name as reported by the demuxer.
101    #[must_use]
102    #[inline]
103    pub fn codec_name(&self) -> &str {
104        &self.codec_name
105    }
106
107    /// Returns the frame width in pixels.
108    #[must_use]
109    #[inline]
110    pub const fn width(&self) -> u32 {
111        self.width
112    }
113
114    /// Returns the frame height in pixels.
115    #[must_use]
116    #[inline]
117    pub const fn height(&self) -> u32 {
118        self.height
119    }
120
121    /// Returns the pixel format.
122    #[must_use]
123    #[inline]
124    pub const fn pixel_format(&self) -> PixelFormat {
125        self.pixel_format
126    }
127
128    /// Returns the frame rate as a rational number.
129    #[must_use]
130    #[inline]
131    pub const fn frame_rate(&self) -> Rational {
132        self.frame_rate
133    }
134
135    /// Returns the frame rate as frames per second (f64).
136    #[must_use]
137    #[inline]
138    pub fn fps(&self) -> f64 {
139        self.frame_rate.as_f64()
140    }
141
142    /// Returns the stream duration, if known.
143    #[must_use]
144    #[inline]
145    pub const fn duration(&self) -> Option<Duration> {
146        self.duration
147    }
148
149    /// Returns the bitrate in bits per second, if known.
150    #[must_use]
151    #[inline]
152    pub const fn bitrate(&self) -> Option<u64> {
153        self.bitrate
154    }
155
156    /// Returns the total number of frames, if known.
157    #[must_use]
158    #[inline]
159    pub const fn frame_count(&self) -> Option<u64> {
160        self.frame_count
161    }
162
163    /// Returns the color space (matrix coefficients).
164    #[must_use]
165    #[inline]
166    pub const fn color_space(&self) -> ColorSpace {
167        self.color_space
168    }
169
170    /// Returns the color range (limited/full).
171    #[must_use]
172    #[inline]
173    pub const fn color_range(&self) -> ColorRange {
174        self.color_range
175    }
176
177    /// Returns the color primaries.
178    #[must_use]
179    #[inline]
180    pub const fn color_primaries(&self) -> ColorPrimaries {
181        self.color_primaries
182    }
183
184    /// Returns the aspect ratio as width/height.
185    #[must_use]
186    #[inline]
187    pub fn aspect_ratio(&self) -> f64 {
188        if self.height == 0 {
189            log::warn!(
190                "aspect_ratio unavailable, height is 0, returning 0.0 \
191                 width={} height=0 fallback=0.0",
192                self.width
193            );
194            0.0
195        } else {
196            f64::from(self.width) / f64::from(self.height)
197        }
198    }
199
200    /// Returns `true` if the video is HD (720p or higher).
201    #[must_use]
202    #[inline]
203    pub const fn is_hd(&self) -> bool {
204        self.height >= 720
205    }
206
207    /// Returns `true` if the video is Full HD (1080p or higher).
208    #[must_use]
209    #[inline]
210    pub const fn is_full_hd(&self) -> bool {
211        self.height >= 1080
212    }
213
214    /// Returns `true` if the video is 4K UHD (2160p or higher).
215    #[must_use]
216    #[inline]
217    pub const fn is_4k(&self) -> bool {
218        self.height >= 2160
219    }
220
221    /// Returns `true` if this video stream appears to be HDR (High Dynamic Range).
222    ///
223    /// HDR detection is based on two primary indicators:
224    /// 1. **Wide color gamut**: BT.2020 color primaries
225    /// 2. **High bit depth**: 10-bit or higher pixel format
226    ///
227    /// Both conditions must be met for a stream to be considered HDR.
228    /// This is a heuristic detection - for definitive HDR identification,
229    /// additional metadata like transfer characteristics (PQ/HLG) should be checked.
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use ff_format::stream::VideoStreamInfo;
235    /// use ff_format::color::ColorPrimaries;
236    /// use ff_format::PixelFormat;
237    ///
238    /// let hdr_video = VideoStreamInfo::builder()
239    ///     .width(3840)
240    ///     .height(2160)
241    ///     .color_primaries(ColorPrimaries::Bt2020)
242    ///     .pixel_format(PixelFormat::Yuv420p10le)
243    ///     .build();
244    ///
245    /// assert!(hdr_video.is_hdr());
246    ///
247    /// // Standard HD video with BT.709 is not HDR
248    /// let sdr_video = VideoStreamInfo::builder()
249    ///     .width(1920)
250    ///     .height(1080)
251    ///     .color_primaries(ColorPrimaries::Bt709)
252    ///     .pixel_format(PixelFormat::Yuv420p)
253    ///     .build();
254    ///
255    /// assert!(!sdr_video.is_hdr());
256    /// ```
257    #[must_use]
258    #[inline]
259    pub fn is_hdr(&self) -> bool {
260        // HDR requires wide color gamut (BT.2020) and high bit depth (10-bit or higher)
261        self.color_primaries.is_wide_gamut() && self.pixel_format.is_high_bit_depth()
262    }
263}
264
265impl Default for VideoStreamInfo {
266    fn default() -> Self {
267        Self {
268            index: 0,
269            codec: VideoCodec::default(),
270            codec_name: String::new(),
271            width: 0,
272            height: 0,
273            pixel_format: PixelFormat::default(),
274            frame_rate: Rational::new(30, 1),
275            duration: None,
276            bitrate: None,
277            frame_count: None,
278            color_space: ColorSpace::default(),
279            color_range: ColorRange::default(),
280            color_primaries: ColorPrimaries::default(),
281        }
282    }
283}
284
285/// Builder for constructing `VideoStreamInfo`.
286#[derive(Debug, Clone, Default)]
287pub struct VideoStreamInfoBuilder {
288    index: u32,
289    codec: VideoCodec,
290    codec_name: String,
291    width: u32,
292    height: u32,
293    pixel_format: PixelFormat,
294    frame_rate: Rational,
295    duration: Option<Duration>,
296    bitrate: Option<u64>,
297    frame_count: Option<u64>,
298    color_space: ColorSpace,
299    color_range: ColorRange,
300    color_primaries: ColorPrimaries,
301}
302
303impl VideoStreamInfoBuilder {
304    /// Sets the stream index.
305    #[must_use]
306    pub fn index(mut self, index: u32) -> Self {
307        self.index = index;
308        self
309    }
310
311    /// Sets the video codec.
312    #[must_use]
313    pub fn codec(mut self, codec: VideoCodec) -> Self {
314        self.codec = codec;
315        self
316    }
317
318    /// Sets the codec name string.
319    #[must_use]
320    pub fn codec_name(mut self, name: impl Into<String>) -> Self {
321        self.codec_name = name.into();
322        self
323    }
324
325    /// Sets the frame width in pixels.
326    #[must_use]
327    pub fn width(mut self, width: u32) -> Self {
328        self.width = width;
329        self
330    }
331
332    /// Sets the frame height in pixels.
333    #[must_use]
334    pub fn height(mut self, height: u32) -> Self {
335        self.height = height;
336        self
337    }
338
339    /// Sets the pixel format.
340    #[must_use]
341    pub fn pixel_format(mut self, format: PixelFormat) -> Self {
342        self.pixel_format = format;
343        self
344    }
345
346    /// Sets the frame rate.
347    #[must_use]
348    pub fn frame_rate(mut self, rate: Rational) -> Self {
349        self.frame_rate = rate;
350        self
351    }
352
353    /// Sets the stream duration.
354    #[must_use]
355    pub fn duration(mut self, duration: Duration) -> Self {
356        self.duration = Some(duration);
357        self
358    }
359
360    /// Sets the bitrate in bits per second.
361    #[must_use]
362    pub fn bitrate(mut self, bitrate: u64) -> Self {
363        self.bitrate = Some(bitrate);
364        self
365    }
366
367    /// Sets the total frame count.
368    #[must_use]
369    pub fn frame_count(mut self, count: u64) -> Self {
370        self.frame_count = Some(count);
371        self
372    }
373
374    /// Sets the color space.
375    #[must_use]
376    pub fn color_space(mut self, space: ColorSpace) -> Self {
377        self.color_space = space;
378        self
379    }
380
381    /// Sets the color range.
382    #[must_use]
383    pub fn color_range(mut self, range: ColorRange) -> Self {
384        self.color_range = range;
385        self
386    }
387
388    /// Sets the color primaries.
389    #[must_use]
390    pub fn color_primaries(mut self, primaries: ColorPrimaries) -> Self {
391        self.color_primaries = primaries;
392        self
393    }
394
395    /// Builds the `VideoStreamInfo`.
396    #[must_use]
397    pub fn build(self) -> VideoStreamInfo {
398        VideoStreamInfo {
399            index: self.index,
400            codec: self.codec,
401            codec_name: self.codec_name,
402            width: self.width,
403            height: self.height,
404            pixel_format: self.pixel_format,
405            frame_rate: self.frame_rate,
406            duration: self.duration,
407            bitrate: self.bitrate,
408            frame_count: self.frame_count,
409            color_space: self.color_space,
410            color_range: self.color_range,
411            color_primaries: self.color_primaries,
412        }
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419
420    #[test]
421    fn test_builder_basic() {
422        let info = VideoStreamInfo::builder()
423            .index(0)
424            .codec(VideoCodec::H264)
425            .codec_name("h264")
426            .width(1920)
427            .height(1080)
428            .frame_rate(Rational::new(30, 1))
429            .pixel_format(PixelFormat::Yuv420p)
430            .build();
431
432        assert_eq!(info.index(), 0);
433        assert_eq!(info.codec(), VideoCodec::H264);
434        assert_eq!(info.codec_name(), "h264");
435        assert_eq!(info.width(), 1920);
436        assert_eq!(info.height(), 1080);
437        assert!((info.fps() - 30.0).abs() < 0.001);
438        assert_eq!(info.pixel_format(), PixelFormat::Yuv420p);
439    }
440
441    #[test]
442    fn test_builder_full() {
443        let info = VideoStreamInfo::builder()
444            .index(0)
445            .codec(VideoCodec::H265)
446            .codec_name("hevc")
447            .width(3840)
448            .height(2160)
449            .frame_rate(Rational::new(60, 1))
450            .pixel_format(PixelFormat::Yuv420p10le)
451            .duration(Duration::from_secs(120))
452            .bitrate(50_000_000)
453            .frame_count(7200)
454            .color_space(ColorSpace::Bt2020)
455            .color_range(ColorRange::Full)
456            .color_primaries(ColorPrimaries::Bt2020)
457            .build();
458
459        assert_eq!(info.codec(), VideoCodec::H265);
460        assert_eq!(info.width(), 3840);
461        assert_eq!(info.height(), 2160);
462        assert_eq!(info.duration(), Some(Duration::from_secs(120)));
463        assert_eq!(info.bitrate(), Some(50_000_000));
464        assert_eq!(info.frame_count(), Some(7200));
465        assert_eq!(info.color_space(), ColorSpace::Bt2020);
466        assert_eq!(info.color_range(), ColorRange::Full);
467        assert_eq!(info.color_primaries(), ColorPrimaries::Bt2020);
468    }
469
470    #[test]
471    fn test_default() {
472        let info = VideoStreamInfo::default();
473        assert_eq!(info.index(), 0);
474        assert_eq!(info.codec(), VideoCodec::default());
475        assert_eq!(info.width(), 0);
476        assert_eq!(info.height(), 0);
477        assert!(info.duration().is_none());
478    }
479
480    #[test]
481    fn test_aspect_ratio() {
482        let info = VideoStreamInfo::builder().width(1920).height(1080).build();
483        assert!((info.aspect_ratio() - (16.0 / 9.0)).abs() < 0.01);
484
485        let info = VideoStreamInfo::builder().width(1280).height(720).build();
486        assert!((info.aspect_ratio() - (16.0 / 9.0)).abs() < 0.01);
487
488        // Zero height
489        let info = VideoStreamInfo::builder().width(1920).height(0).build();
490        assert_eq!(info.aspect_ratio(), 0.0);
491    }
492
493    #[test]
494    fn test_resolution_checks() {
495        // SD
496        let sd = VideoStreamInfo::builder().width(720).height(480).build();
497        assert!(!sd.is_hd());
498        assert!(!sd.is_full_hd());
499        assert!(!sd.is_4k());
500
501        // HD
502        let hd = VideoStreamInfo::builder().width(1280).height(720).build();
503        assert!(hd.is_hd());
504        assert!(!hd.is_full_hd());
505        assert!(!hd.is_4k());
506
507        // Full HD
508        let fhd = VideoStreamInfo::builder().width(1920).height(1080).build();
509        assert!(fhd.is_hd());
510        assert!(fhd.is_full_hd());
511        assert!(!fhd.is_4k());
512
513        // 4K
514        let uhd = VideoStreamInfo::builder().width(3840).height(2160).build();
515        assert!(uhd.is_hd());
516        assert!(uhd.is_full_hd());
517        assert!(uhd.is_4k());
518    }
519
520    #[test]
521    fn test_is_hdr() {
522        // HDR video: BT.2020 color primaries + 10-bit pixel format
523        let hdr = VideoStreamInfo::builder()
524            .width(3840)
525            .height(2160)
526            .color_primaries(ColorPrimaries::Bt2020)
527            .pixel_format(PixelFormat::Yuv420p10le)
528            .build();
529        assert!(hdr.is_hdr());
530
531        // HDR video with P010le format
532        let hdr_p010 = VideoStreamInfo::builder()
533            .width(3840)
534            .height(2160)
535            .color_primaries(ColorPrimaries::Bt2020)
536            .pixel_format(PixelFormat::P010le)
537            .build();
538        assert!(hdr_p010.is_hdr());
539
540        // SDR video: BT.709 color primaries (standard HD)
541        let sdr_hd = VideoStreamInfo::builder()
542            .width(1920)
543            .height(1080)
544            .color_primaries(ColorPrimaries::Bt709)
545            .pixel_format(PixelFormat::Yuv420p)
546            .build();
547        assert!(!sdr_hd.is_hdr());
548
549        // BT.2020 but 8-bit (not HDR - missing high bit depth)
550        let wide_gamut_8bit = VideoStreamInfo::builder()
551            .width(3840)
552            .height(2160)
553            .color_primaries(ColorPrimaries::Bt2020)
554            .pixel_format(PixelFormat::Yuv420p) // 8-bit
555            .build();
556        assert!(!wide_gamut_8bit.is_hdr());
557
558        // 10-bit but BT.709 (not HDR - missing wide gamut)
559        let hd_10bit = VideoStreamInfo::builder()
560            .width(1920)
561            .height(1080)
562            .color_primaries(ColorPrimaries::Bt709)
563            .pixel_format(PixelFormat::Yuv420p10le)
564            .build();
565        assert!(!hd_10bit.is_hdr());
566
567        // Default video stream is not HDR
568        let default = VideoStreamInfo::default();
569        assert!(!default.is_hdr());
570    }
571
572    #[test]
573    fn test_debug() {
574        let info = VideoStreamInfo::builder()
575            .index(0)
576            .codec(VideoCodec::H264)
577            .width(1920)
578            .height(1080)
579            .build();
580        let debug = format!("{info:?}");
581        assert!(debug.contains("VideoStreamInfo"));
582        assert!(debug.contains("1920"));
583        assert!(debug.contains("1080"));
584    }
585
586    #[test]
587    fn test_clone() {
588        let info = VideoStreamInfo::builder()
589            .index(0)
590            .codec(VideoCodec::H264)
591            .codec_name("h264")
592            .width(1920)
593            .height(1080)
594            .build();
595        let cloned = info.clone();
596        assert_eq!(info.width(), cloned.width());
597        assert_eq!(info.height(), cloned.height());
598        assert_eq!(info.codec_name(), cloned.codec_name());
599    }
600}