1use crate::{Codec, Hdr10Metadata, StreamInfo};
4
5#[derive(Debug, Clone, Default, PartialEq, Eq)]
7pub struct SourceFormat {
8 pub pix_fmt: String,
10 pub bit_depth: u8,
12 pub color_primaries: String,
14 pub color_transfer: String,
16 pub color_space: String,
18 pub is_hdr: bool,
20 pub hdr10: Option<Hdr10Metadata>,
22}
23
24impl SourceFormat {
25 pub fn from_stream(stream: &StreamInfo) -> Self {
27 let bit_depth = bit_depth(stream);
28 let pix_fmt = if stream.pix_fmt.is_empty() {
29 yuv420p_for_depth(bit_depth).to_string()
30 } else {
31 stream.pix_fmt.clone()
32 };
33 Self {
34 pix_fmt,
35 bit_depth,
36 color_primaries: stream.color_primaries.clone(),
37 color_transfer: stream.color_transfer.clone(),
38 color_space: stream.color_space.clone(),
39 is_hdr: stream.is_hdr(),
40 hdr10: None,
41 }
42 }
43
44 pub fn is_high_bit_depth(&self) -> bool {
46 self.bit_depth > 8
47 }
48
49 pub async fn enrich_hdr10(mut self, path: &str) -> Self {
56 if self.is_hdr {
57 if let Ok(Some(md)) = crate::probe_hdr10_metadata(path).await {
58 self.hdr10 = Some(md);
59 }
60 }
61 self
62 }
63}
64
65pub fn bit_depth(stream: &StreamInfo) -> u8 {
67 if stream.bits_per_raw_sample >= 10 {
68 return stream.bits_per_raw_sample.clamp(8, 16) as u8;
69 }
70 if stream.pix_fmt.contains("16") {
71 return 16;
72 }
73 if stream.pix_fmt.contains("12") {
74 return 12;
75 }
76 if stream.pix_fmt.contains("10") {
77 return 10;
78 }
79 8
80}
81
82pub fn yuv420p_for_depth(depth: u8) -> &'static str {
84 match depth {
85 10 => "yuv420p10le",
86 12 => "yuv420p12le",
87 16 => "yuv420p16le",
88 _ => "yuv420p",
89 }
90}
91
92pub fn psnr_peak(depth: u8) -> f64 {
94 match depth {
95 10 => 1023.0,
96 12 => 4095.0,
97 16 => 65535.0,
98 _ => 255.0,
99 }
100}
101
102pub fn codec_supports_bit_depth(codec: Codec, depth: u8) -> bool {
104 if depth <= 8 {
105 return true;
106 }
107 match codec {
108 Codec::X264 | Codec::X265 | Codec::SvtAv1 | Codec::Vp9 => true,
109 _ => false,
110 }
111}
112
113pub fn encode_color_args(codec: Codec, format: &SourceFormat) -> Vec<String> {
115 let mut args = Vec::new();
116
117 if format.is_high_bit_depth()
118 && codec.is_software()
119 && codec_supports_bit_depth(codec, format.bit_depth)
120 {
121 args.extend(["-pix_fmt".into(), format.pix_fmt.clone()]);
122 match codec {
123 Codec::X264 => args.extend(["-profile:v".into(), "high10".into()]),
124 Codec::X265 => args.extend(["-x265-params".into(), x265_params(format)]),
125 Codec::SvtAv1 => {}
126 _ => {}
127 }
128 }
129
130 if format.is_hdr {
131 append_color_metadata(&mut args, format);
132 match codec {
133 Codec::X265 => merge_x265_color_params(&mut args, format),
134 Codec::SvtAv1 => {
135 let params = svtav1_hdr_params(format);
136 if !params.is_empty() {
137 args.extend(["-svtav1-params".into(), params]);
141 }
142 }
143 _ => {}
144 }
145 }
146
147 args
148}
149
150fn append_color_metadata(args: &mut Vec<String>, format: &SourceFormat) {
151 if !format.color_primaries.is_empty() {
152 args.extend(["-color_primaries".into(), format.color_primaries.clone()]);
153 }
154 if !format.color_transfer.is_empty() {
155 args.extend(["-color_trc".into(), format.color_transfer.clone()]);
156 }
157 if !format.color_space.is_empty() {
158 args.extend(["-colorspace".into(), format.color_space.clone()]);
159 }
160}
161
162fn x265_params(format: &SourceFormat) -> String {
163 let mut parts = Vec::new();
164 if format.bit_depth > 8 {
165 parts.push("profile=main10".into());
166 }
167 if format.is_hdr {
168 if !format.color_primaries.is_empty() {
169 parts.push(format!("colorprim={}", format.color_primaries));
170 }
171 if !format.color_transfer.is_empty() {
172 parts.push(format!("transfer={}", format.color_transfer));
173 }
174 if !format.color_space.is_empty() {
175 parts.push(format!("colormatrix={}", format.color_space));
176 }
177 if let Some(hdr10) = &format.hdr10 {
178 if let Some(display) = &hdr10.mastering_display {
179 parts.push(format!("master-display={}", display.to_x265_string()));
180 }
181 if let Some(max_cll) = hdr10.max_cll {
182 parts.push(format!("max-cll={},{}", max_cll, hdr10.max_fall.unwrap_or(0)));
184 }
185 }
186 }
187 parts.join(":")
188}
189
190fn svtav1_hdr_params(format: &SourceFormat) -> String {
196 let mut parts = Vec::new();
197 if let Some(hdr10) = &format.hdr10 {
198 if let Some(display) = &hdr10.mastering_display {
199 parts.push(format!("mastering-display={}", display.to_svtav1_string()));
200 }
201 if let Some(max_cll) = hdr10.max_cll {
202 parts.push(format!("content-light={},{}", max_cll, hdr10.max_fall.unwrap_or(0)));
203 }
204 }
205 parts.join(":")
206}
207
208fn merge_x265_color_params(args: &mut Vec<String>, format: &SourceFormat) {
209 let color = x265_params(format);
210 if color.is_empty() {
211 return;
212 }
213 if let Some(idx) = args.iter().position(|a| a == "-x265-params") {
214 let existing = args.get(idx + 1).cloned().unwrap_or_default();
215 let merged = if existing.is_empty() { color } else { format!("{existing}:{color}") };
216 args[idx + 1] = merged;
217 } else {
218 args.extend(["-x265-params".into(), color]);
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::StreamInfo;
226
227 fn base_stream() -> StreamInfo {
228 StreamInfo {
229 index: 0,
230 codec_name: "h264".into(),
231 codec_long_name: String::new(),
232 codec_type: "video".into(),
233 profile: String::new(),
234 width: 1920,
235 height: 1080,
236 pix_fmt: "yuv420p".into(),
237 level: 0,
238 field_order: String::new(),
239 color_range: String::new(),
240 color_space: "bt709".into(),
241 color_transfer: "bt709".into(),
242 color_primaries: "bt709".into(),
243 duration: 0.0,
244 bit_rate: 0,
245 nb_frames: 0,
246 r_frame_rate: "24/1".into(),
247 avg_frame_rate: "24/1".into(),
248 sample_rate: 0,
249 channels: 0,
250 channel_layout: String::new(),
251 bits_per_raw_sample: 8,
252 }
253 }
254
255 #[test]
256 fn test_bit_depth_from_pix_fmt() {
257 let mut stream = base_stream();
258 stream.pix_fmt = "yuv420p10le".into();
259 assert_eq!(bit_depth(&stream), 10);
260 }
261
262 #[test]
263 fn test_source_format_high_bit_depth() {
264 let mut stream = base_stream();
265 stream.pix_fmt = "yuv420p10le".into();
266 let format = SourceFormat::from_stream(&stream);
267 assert_eq!(format.bit_depth, 10);
268 assert_eq!(format.pix_fmt, "yuv420p10le");
269 assert!(format.is_high_bit_depth());
270 }
271
272 #[test]
273 fn test_encode_color_args_x265_10bit_hdr() {
274 let mut stream = base_stream();
275 stream.pix_fmt = "yuv420p10le".into();
276 stream.color_transfer = "smpte2084".into();
277 stream.color_primaries = "bt2020".into();
278 stream.color_space = "bt2020nc".into();
279 let format = SourceFormat::from_stream(&stream);
280 let args = encode_color_args(Codec::X265, &format);
281 assert!(args.windows(2).any(|w| w[0] == "-pix_fmt" && w[1] == "yuv420p10le"));
282 assert!(args.iter().any(|a| a.contains("profile=main10")));
283 assert!(args.iter().any(|a| a.contains("transfer=smpte2084")));
284 }
285
286 #[test]
287 fn test_psnr_peak_scaling() {
288 assert_eq!(psnr_peak(8), 255.0);
289 assert_eq!(psnr_peak(10), 1023.0);
290 }
291
292 #[test]
293 fn test_encode_color_args_emits_hdr10_metadata() {
294 use crate::{Hdr10Metadata, MasteringDisplay};
295 let mut stream = base_stream();
296 stream.pix_fmt = "yuv420p10le".into();
297 stream.color_transfer = "smpte2084".into();
298 stream.color_primaries = "bt2020".into();
299 stream.color_space = "bt2020nc".into();
300 let mut format = SourceFormat::from_stream(&stream);
301 format.hdr10 = Some(Hdr10Metadata {
302 mastering_display: Some(MasteringDisplay {
303 green_x: 13250,
304 green_y: 34500,
305 blue_x: 7500,
306 blue_y: 3000,
307 red_x: 34000,
308 red_y: 16000,
309 white_x: 15635,
310 white_y: 16450,
311 max_luminance: 10_000_000,
312 min_luminance: 50,
313 }),
314 max_cll: Some(1000),
315 max_fall: Some(400),
316 });
317 let args = encode_color_args(Codec::X265, &format);
318 let params = args
319 .windows(2)
320 .find(|w| w[0] == "-x265-params")
321 .map(|w| w[1].clone())
322 .expect("x265-params present");
323 assert!(
324 params.contains("master-display=G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,50)"),
325 "got: {params}"
326 );
327 assert!(params.contains("max-cll=1000,400"), "got: {params}");
328 }
329
330 #[test]
331 fn test_encode_color_args_svtav1_hdr10_metadata() {
332 use crate::{Hdr10Metadata, MasteringDisplay};
333 let mut stream = base_stream();
334 stream.pix_fmt = "yuv420p10le".into();
335 stream.color_transfer = "smpte2084".into();
336 stream.color_primaries = "bt2020".into();
337 stream.color_space = "bt2020nc".into();
338 let mut format = SourceFormat::from_stream(&stream);
339 format.hdr10 = Some(Hdr10Metadata {
340 mastering_display: Some(MasteringDisplay {
341 green_x: 13250,
342 green_y: 34500,
343 blue_x: 7500,
344 blue_y: 3000,
345 red_x: 34000,
346 red_y: 16000,
347 white_x: 15635,
348 white_y: 16450,
349 max_luminance: 10_000_000,
350 min_luminance: 50,
351 }),
352 max_cll: Some(1000),
353 max_fall: Some(400),
354 });
355 let args = encode_color_args(Codec::SvtAv1, &format);
356 let params = args
357 .windows(2)
358 .find(|w| w[0] == "-svtav1-params")
359 .map(|w| w[1].clone())
360 .expect("svtav1-params present");
361 assert!(
362 params.contains("mastering-display=G(0.265,0.69)B(0.15,0.06)R(0.68,0.32)WP(0.3127,0.329)L(1000,0.005)"),
363 "got: {params}"
364 );
365 assert!(params.contains("content-light=1000,400"), "got: {params}");
366 assert!(args.windows(2).any(|w| w[0] == "-color_trc" && w[1] == "smpte2084"));
368 }
369
370 #[test]
371 fn test_encode_color_args_no_hdr10_when_sdr() {
372 let mut stream = base_stream();
373 stream.pix_fmt = "yuv420p10le".into();
374 let format = SourceFormat::from_stream(&stream);
375 let args = encode_color_args(Codec::X265, &format);
376 assert!(!args.iter().any(|a| a.contains("master-display")));
377 assert!(!args.iter().any(|a| a.contains("max-cll")));
378 }
379}