moq_video/color.rs
1//! [`Color`]: which YUV color space a frame's samples are in.
2
3use crate::Size;
4
5/// Which YUV color space a frame's samples are in.
6///
7/// Video carries luma and chroma, not RGB, and the matrix that converts between
8/// them differs by generation (BT.601 for standard definition, BT.709 for high
9/// definition) as does the numeric range (limited/studio swing pins luma to
10/// 16..235, full/full swing uses 0..255). Pairing samples with the wrong matrix
11/// is the classic tinted-video bug: it leaves grays untouched and skews
12/// saturated colors, so it survives a casual look at the picture.
13///
14/// [`Surface::color`](crate::Surface::color) reports it where the crate knows:
15/// when the crate did the conversion itself, or when the surface carries the
16/// answer (a macOS pixel buffer names its matrix, which VideoToolbox copies out
17/// of the stream's VUI). It is `None` for pixels that merely passed through with
18/// nothing naming their space, a camera's raw YUYV among them.
19/// [`Color::infer`] is the fallback then.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum Color {
23 /// BT.601 (standard definition), limited range.
24 Bt601Limited,
25 /// BT.601 (standard definition), full range.
26 Bt601Full,
27 /// BT.709 (high definition), limited range.
28 Bt709Limited,
29 /// BT.709 (high definition), full range.
30 Bt709Full,
31}
32
33impl Color {
34 /// The conventional guess for a frame of this size: BT.601 up to standard
35 /// definition (576 lines), BT.709 above it, both limited range.
36 ///
37 /// What a player does when the bitstream carries no VUI color description,
38 /// which is most of the time. A guess, so prefer a known [`Color`] whenever
39 /// one is available.
40 pub fn infer(size: Size) -> Self {
41 match size.height <= 576 {
42 true => Color::Bt601Limited,
43 false => Color::Bt709Limited,
44 }
45 }
46
47 /// The same matrix as `self` but in the given range, for a caller that knows
48 /// the range and not the matrix.
49 ///
50 /// Only a surface whose pixel format spells out its range reaches this, which
51 /// is why it is macOS-only: CoreVideo's video-range and full-range NV12 name
52 /// theirs.
53 #[cfg(target_os = "macos")]
54 pub(crate) fn with_range(self, limited: bool) -> Self {
55 match (self, limited) {
56 (Color::Bt601Limited | Color::Bt601Full, true) => Color::Bt601Limited,
57 (Color::Bt601Limited | Color::Bt601Full, false) => Color::Bt601Full,
58 (_, true) => Color::Bt709Limited,
59 (_, false) => Color::Bt709Full,
60 }
61 }
62
63 /// Whether luma is 16..235 rather than 0..255.
64 ///
65 /// The encoders need it for the VUI's `video_full_range_flag`, so unlike the
66 /// render module's `weights` it is not render-only.
67 pub(crate) fn limited(self) -> bool {
68 matches!(self, Color::Bt601Limited | Color::Bt709Limited)
69 }
70
71 /// How the `yuv` crate names this color space, for the RGB conversions.
72 pub(crate) fn yuv(self) -> (yuv::YuvRange, yuv::YuvStandardMatrix) {
73 let range = match self.limited() {
74 true => yuv::YuvRange::Limited,
75 false => yuv::YuvRange::Full,
76 };
77 let matrix = match self {
78 Color::Bt601Limited | Color::Bt601Full => yuv::YuvStandardMatrix::Bt601,
79 Color::Bt709Limited | Color::Bt709Full => yuv::YuvStandardMatrix::Bt709,
80 };
81 (range, matrix)
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn inference_splits_at_standard_definition() {
91 assert_eq!(Color::infer(Size::new(720, 480)), Color::Bt601Limited);
92 assert_eq!(Color::infer(Size::new(720, 576)), Color::Bt601Limited);
93 assert_eq!(Color::infer(Size::new(1280, 720)), Color::Bt709Limited);
94 }
95
96 #[cfg(target_os = "macos")]
97 #[test]
98 fn with_range_keeps_the_matrix() {
99 assert_eq!(Color::Bt709Limited.with_range(false), Color::Bt709Full);
100 assert_eq!(Color::Bt709Full.with_range(true), Color::Bt709Limited);
101 assert_eq!(Color::Bt601Limited.with_range(false), Color::Bt601Full);
102 }
103}