Skip to main content

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}