1#![allow(clippy::module_name_repetitions)]
22
23use std::time::Instant;
24
25use crate::hdr_passthrough::{
26 ColourPrimaries, HdrMetadata, HdrPassthroughMode, HdrProcessor, TransferFunction,
27};
28use crate::{Result, TranscodeError};
29
30#[derive(Debug, Clone)]
37pub struct Frame {
38 pub data: Vec<u8>,
40 pub pts_ms: i64,
42 pub is_audio: bool,
44 pub width: u32,
46 pub height: u32,
48 pub hdr_meta: Option<HdrMetadata>,
50}
51
52impl Frame {
53 #[must_use]
55 pub fn video(data: Vec<u8>, pts_ms: i64, width: u32, height: u32) -> Self {
56 Self {
57 data,
58 pts_ms,
59 is_audio: false,
60 width,
61 height,
62 hdr_meta: None,
63 }
64 }
65
66 #[must_use]
68 pub fn audio(data: Vec<u8>, pts_ms: i64) -> Self {
69 Self {
70 data,
71 pts_ms,
72 is_audio: true,
73 width: 0,
74 height: 0,
75 hdr_meta: None,
76 }
77 }
78
79 #[must_use]
81 pub fn with_hdr(mut self, meta: HdrMetadata) -> Self {
82 self.hdr_meta = Some(meta);
83 self
84 }
85}
86
87pub trait FrameDecoder: Send {
95 fn decode_next(&mut self) -> Option<Frame>;
99
100 fn eof(&self) -> bool;
102}
103
104pub trait FrameEncoder: Send {
108 fn encode_frame(&mut self, frame: &Frame) -> Result<Vec<u8>>;
118
119 fn flush(&mut self) -> Result<Vec<u8>>;
127}
128
129#[derive(Debug, Clone)]
133enum FilterOp {
134 VideoScale { width: u32, height: u32 },
136 AudioGainDb(f64),
138 HdrPassthrough(HdrPassthroughMode),
140}
141
142#[derive(Debug, Clone, Default)]
152pub struct FilterGraph {
153 ops: Vec<FilterOp>,
154}
155
156impl FilterGraph {
157 #[must_use]
159 pub fn new() -> Self {
160 Self { ops: Vec::new() }
161 }
162
163 #[must_use]
165 pub fn add_video_scale(mut self, width: u32, height: u32) -> Self {
166 self.ops.push(FilterOp::VideoScale { width, height });
167 self
168 }
169
170 #[must_use]
172 pub fn add_audio_gain_db(mut self, db: f64) -> Self {
173 self.ops.push(FilterOp::AudioGainDb(db));
174 self
175 }
176
177 #[must_use]
179 pub fn add_hdr_passthrough(mut self, mode: HdrPassthroughMode) -> Self {
180 self.ops.push(FilterOp::HdrPassthrough(mode));
181 self
182 }
183
184 pub fn apply(&self, mut frame: Frame) -> Result<Option<Frame>> {
194 for op in &self.ops {
195 match op {
196 FilterOp::VideoScale { width, height } => {
197 if !frame.is_audio {
198 apply_video_scale(&mut frame, *width, *height);
199 }
200 }
201 FilterOp::AudioGainDb(db) => {
202 if frame.is_audio {
203 apply_audio_gain_db(&mut frame, *db);
204 }
205 }
206 FilterOp::HdrPassthrough(mode) => {
207 if !frame.is_audio {
208 let processor = HdrProcessor::new(mode.clone());
209 let resolved = processor.process(frame.hdr_meta.as_ref()).map_err(|e| {
210 TranscodeError::CodecError(format!("HDR filter failed: {e}"))
211 })?;
212 frame.hdr_meta = resolved;
213 }
214 }
215 }
216 }
217 Ok(Some(frame))
218 }
219}
220
221fn apply_video_scale(frame: &mut Frame, dst_w: u32, dst_h: u32) {
229 if dst_w == 0 || dst_h == 0 || (dst_w == frame.width && dst_h == frame.height) {
230 return;
231 }
232 let src_w = frame.width;
233 let src_h = frame.height;
234 if src_w == 0 || src_h == 0 {
235 return;
236 }
237
238 let y_size = (src_w * src_h) as usize;
239 let uv_size = y_size / 4;
240 let expected_yuv = y_size + uv_size * 2;
241
242 if frame.data.len() == expected_yuv {
243 let dst_y_size = (dst_w * dst_h) as usize;
245 let dst_uv_size = dst_y_size / 4;
246 let mut out = vec![0u8; dst_y_size + dst_uv_size * 2];
247
248 scale_plane(
250 &frame.data[..y_size],
251 src_w,
252 src_h,
253 &mut out[..dst_y_size],
254 dst_w,
255 dst_h,
256 );
257 let uv_src_w = src_w / 2;
259 let uv_src_h = src_h / 2;
260 let dst_uv_w = dst_w / 2;
261 let dst_uv_h = dst_h / 2;
262 scale_plane(
263 &frame.data[y_size..y_size + uv_size],
264 uv_src_w,
265 uv_src_h,
266 &mut out[dst_y_size..dst_y_size + dst_uv_size],
267 dst_uv_w,
268 dst_uv_h,
269 );
270 scale_plane(
272 &frame.data[y_size + uv_size..],
273 uv_src_w,
274 uv_src_h,
275 &mut out[dst_y_size + dst_uv_size..],
276 dst_uv_w,
277 dst_uv_h,
278 );
279
280 frame.data = out;
281 frame.width = dst_w;
282 frame.height = dst_h;
283 } else {
284 let bytes_per_pixel = if frame.data.len() == (src_w * src_h * 4) as usize {
286 4usize
287 } else {
288 1usize
289 };
290
291 let dst_size = (dst_w * dst_h) as usize * bytes_per_pixel;
292 let mut out = vec![0u8; dst_size];
293
294 for dy in 0..dst_h {
295 for dx in 0..dst_w {
296 let sx = (f64::from(dx) * f64::from(src_w) / f64::from(dst_w)) as u32;
297 let sy = (f64::from(dy) * f64::from(src_h) / f64::from(dst_h)) as u32;
298 let src_off = ((sy * src_w + sx) as usize) * bytes_per_pixel;
299 let dst_off = ((dy * dst_w + dx) as usize) * bytes_per_pixel;
300 for b in 0..bytes_per_pixel {
301 if src_off + b < frame.data.len() && dst_off + b < out.len() {
302 out[dst_off + b] = frame.data[src_off + b];
303 }
304 }
305 }
306 }
307
308 frame.data = out;
309 frame.width = dst_w;
310 frame.height = dst_h;
311 }
312}
313
314fn scale_plane(src: &[u8], src_w: u32, src_h: u32, dst: &mut [u8], dst_w: u32, dst_h: u32) {
316 if src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0 {
317 return;
318 }
319 for dy in 0..dst_h {
320 for dx in 0..dst_w {
321 let sx = (f64::from(dx) * f64::from(src_w) / f64::from(dst_w)) as u32;
322 let sy = (f64::from(dy) * f64::from(src_h) / f64::from(dst_h)) as u32;
323 let src_idx = (sy * src_w + sx) as usize;
324 let dst_idx = (dy * dst_w + dx) as usize;
325 if src_idx < src.len() && dst_idx < dst.len() {
326 dst[dst_idx] = src[src_idx];
327 }
328 }
329 }
330}
331
332fn apply_audio_gain_db(frame: &mut Frame, db: f64) {
334 if db.abs() < 0.001 {
335 return;
336 }
337 let linear = 10f64.powf(db / 20.0) as f32;
338 let n_samples = frame.data.len() / 2;
339 for i in 0..n_samples {
340 let lo = frame.data[i * 2];
341 let hi = frame.data[i * 2 + 1];
342 let sample = i16::from_le_bytes([lo, hi]) as f32;
343 let gained = (sample * linear).clamp(i16::MIN as f32, i16::MAX as f32) as i16;
344 let bytes = gained.to_le_bytes();
345 frame.data[i * 2] = bytes[0];
346 frame.data[i * 2 + 1] = bytes[1];
347 }
348}
349
350#[derive(Debug, Clone, Default)]
354pub struct PassStats {
355 pub input_frames: u64,
357 pub output_frames: u64,
360 pub input_bytes: u64,
362 pub output_bytes: u64,
364 pub video_frames: u64,
366 pub audio_frames: u64,
368}
369
370#[derive(Debug, Clone, Default)]
374pub struct TranscodeStats {
375 pub pass: PassStats,
377 pub wall_time_secs: f64,
379}
380
381impl TranscodeStats {
382 #[must_use]
386 pub fn speed_factor(&self) -> f64 {
387 if self.wall_time_secs > 0.0 && self.pass.input_frames > 0 {
388 self.pass.input_frames as f64 / self.wall_time_secs
389 } else {
390 0.0
391 }
392 }
393}
394
395#[derive(Debug, Clone, Default)]
404pub struct HdrPassthroughConfig {
405 pub enabled: bool,
408 pub convert_hdr10_to_hlg: bool,
413 pub inject_sei: bool,
417}
418
419impl HdrPassthroughConfig {
420 #[must_use]
422 pub fn passthrough() -> Self {
423 Self {
424 enabled: true,
425 convert_hdr10_to_hlg: false,
426 inject_sei: false,
427 }
428 }
429
430 #[must_use]
432 pub fn strip() -> Self {
433 Self {
434 enabled: false,
435 convert_hdr10_to_hlg: false,
436 inject_sei: false,
437 }
438 }
439
440 #[must_use]
443 pub fn to_mode(&self) -> HdrPassthroughMode {
444 if !self.enabled {
445 return HdrPassthroughMode::Strip;
446 }
447 if self.convert_hdr10_to_hlg {
448 return HdrPassthroughMode::Convert {
449 target_tf: TransferFunction::Hlg,
450 target_primaries: ColourPrimaries::Bt2020,
451 };
452 }
453 HdrPassthroughMode::Passthrough
454 }
455}
456
457pub struct HdrSeiInjector {
462 config: HdrPassthroughConfig,
463 mastering_display_sei: Option<[u8; 24]>,
465 cll_sei: Option<[u8; 4]>,
467}
468
469impl HdrSeiInjector {
470 #[must_use]
472 pub fn new(config: HdrPassthroughConfig) -> Self {
473 Self {
474 config,
475 mastering_display_sei: None,
476 cll_sei: None,
477 }
478 }
479
480 pub fn store_from_metadata(&mut self, meta: &HdrMetadata) {
483 if let Some(md) = &meta.mastering_display {
484 self.mastering_display_sei =
485 Some(crate::hdr_passthrough::encode_mastering_display_sei(md));
486 }
487 if let Some(cll) = &meta.content_light_level {
488 self.cll_sei = Some(crate::hdr_passthrough::encode_cll_sei(cll));
489 }
490 }
491
492 #[must_use]
497 pub fn inject_into_packet(&self, data: &[u8]) -> Vec<u8> {
498 if !self.config.inject_sei
499 || (self.mastering_display_sei.is_none() && self.cll_sei.is_none())
500 {
501 return data.to_vec();
502 }
503 let mut out = Vec::with_capacity(
504 self.mastering_display_sei.as_ref().map_or(0, |s| s.len())
505 + self.cll_sei.as_ref().map_or(0, |c| c.len())
506 + data.len(),
507 );
508 if let Some(sei) = &self.mastering_display_sei {
509 out.extend_from_slice(sei.as_slice());
510 }
511 if let Some(cll) = &self.cll_sei {
512 out.extend_from_slice(cll.as_slice());
513 }
514 out.extend_from_slice(data);
515 out
516 }
517
518 pub fn resolve_output_metadata(
525 &self,
526 input: Option<&HdrMetadata>,
527 ) -> Result<Option<HdrMetadata>> {
528 let mode = self.config.to_mode();
529 let processor = HdrProcessor::new(mode);
530 processor
531 .process(input)
532 .map_err(|e| TranscodeError::CodecError(format!("HDR SEI resolve failed: {e}")))
533 }
534
535 #[must_use]
538 pub fn has_sei_data(&self) -> bool {
539 self.config.inject_sei && (self.mastering_display_sei.is_some() || self.cll_sei.is_some())
540 }
541}
542
543pub struct TranscodeContext {
560 pub decoder: Box<dyn FrameDecoder>,
562 pub filter_graph: FilterGraph,
564 pub encoder: Box<dyn FrameEncoder>,
566}
567
568impl TranscodeContext {
569 #[must_use]
571 pub fn new(
572 decoder: Box<dyn FrameDecoder>,
573 filter_graph: FilterGraph,
574 encoder: Box<dyn FrameEncoder>,
575 ) -> Self {
576 Self {
577 decoder,
578 filter_graph,
579 encoder,
580 }
581 }
582
583 pub fn execute(&mut self) -> Result<TranscodeStats> {
592 let start = Instant::now();
593 let mut stats = PassStats::default();
594
595 while !self.decoder.eof() {
596 match self.decoder.decode_next() {
597 Some(frame) => {
598 stats.input_bytes += frame.data.len() as u64;
599 stats.input_frames += 1;
600 if frame.is_audio {
601 stats.audio_frames += 1;
602 } else {
603 stats.video_frames += 1;
604 }
605
606 match self.filter_graph.apply(frame)? {
607 Some(filtered) => {
608 let encoded = self.encoder.encode_frame(&filtered)?;
609 stats.output_bytes += encoded.len() as u64;
610 stats.output_frames += 1;
611 }
612 None => {
613 }
615 }
616 }
617 None => {
618 break;
620 }
621 }
622 }
623
624 let flushed = self.encoder.flush()?;
626 stats.output_bytes += flushed.len() as u64;
627
628 Ok(TranscodeStats {
629 pass: stats,
630 wall_time_secs: start.elapsed().as_secs_f64(),
631 })
632 }
633}
634
635#[cfg(test)]
638mod tests {
639 use super::*;
640 use crate::hdr_passthrough::{ContentLightLevel, MasteringDisplay, TransferFunction};
641
642 #[test]
645 fn test_frame_video_defaults() {
646 let f = Frame::video(vec![0u8; 12], 42, 4, 3);
647 assert!(!f.is_audio);
648 assert_eq!(f.width, 4);
649 assert_eq!(f.height, 3);
650 assert_eq!(f.pts_ms, 42);
651 assert!(f.hdr_meta.is_none());
652 }
653
654 #[test]
655 fn test_frame_audio_defaults() {
656 let f = Frame::audio(vec![0u8; 16], 100);
657 assert!(f.is_audio);
658 assert_eq!(f.width, 0);
659 assert_eq!(f.height, 0);
660 assert_eq!(f.pts_ms, 100);
661 }
662
663 #[test]
664 fn test_frame_with_hdr() {
665 let meta = HdrMetadata::hlg();
666 let f = Frame::video(vec![0u8; 4], 0, 2, 2).with_hdr(meta.clone());
667 assert!(f.hdr_meta.is_some());
668 assert_eq!(
669 f.hdr_meta.as_ref().and_then(|m| m.transfer_function),
670 Some(TransferFunction::Hlg)
671 );
672 }
673
674 #[test]
677 fn test_filter_graph_empty_passthrough_video() {
678 let fg = FilterGraph::new();
679 let frame = Frame::video(vec![1u8, 2, 3, 4], 0, 2, 1);
680 let data_before = frame.data.clone();
681 let result = fg.apply(frame).expect("apply should succeed");
682 assert!(result.is_some());
683 assert_eq!(result.as_ref().map(|f| &f.data), Some(&data_before));
684 }
685
686 #[test]
687 fn test_filter_graph_empty_passthrough_audio() {
688 let fg = FilterGraph::new();
689 let frame = Frame::audio(vec![0x10u8, 0x00, 0x20, 0x00], 0);
690 let data_before = frame.data.clone();
691 let result = fg.apply(frame).expect("apply should succeed");
692 assert!(result.is_some());
693 assert_eq!(result.as_ref().map(|f| &f.data), Some(&data_before));
694 }
695
696 #[test]
699 fn test_filter_graph_video_scale_rgba() {
700 let src_w = 4u32;
702 let src_h = 4u32;
703 let data = vec![0u8; (src_w * src_h * 4) as usize];
704 let fg = FilterGraph::new().add_video_scale(2, 2);
705 let frame = Frame::video(data, 0, src_w, src_h);
706 let result = fg.apply(frame).expect("scale should succeed");
707 let out = result.expect("should produce a frame");
708 assert_eq!(out.width, 2);
709 assert_eq!(out.height, 2);
710 assert_eq!(out.data.len(), 2 * 2 * 4);
711 }
712
713 #[test]
714 fn test_filter_graph_video_scale_yuv420() {
715 let w = 4u32;
717 let h = 4u32;
718 let y_size = (w * h) as usize;
719 let uv_size = y_size / 4;
720 let data = vec![200u8; y_size + uv_size * 2]; let fg = FilterGraph::new().add_video_scale(2, 2);
722 let frame = Frame::video(data, 0, w, h);
723 let result = fg.apply(frame).expect("yuv420 scale should succeed");
724 let out = result.expect("should produce a frame");
725 assert_eq!(out.width, 2);
726 assert_eq!(out.height, 2);
727 let expected_size = (2 * 2 + 2 * (1 * 1)) as usize; assert_eq!(out.data.len(), expected_size);
729 }
730
731 #[test]
732 fn test_filter_graph_video_scale_noop_same_dims() {
733 let data = vec![42u8; 16 * 16 * 4];
734 let fg = FilterGraph::new().add_video_scale(16, 16);
735 let frame = Frame::video(data.clone(), 0, 16, 16);
736 let out = fg.apply(frame).expect("noop scale").expect("frame");
737 assert_eq!(out.data, data);
738 assert_eq!(out.width, 16);
739 assert_eq!(out.height, 16);
740 }
741
742 #[test]
745 fn test_filter_graph_audio_gain_double() {
746 let sample: i16 = 1000;
748 let mut data = sample.to_le_bytes().to_vec();
749 data.extend_from_slice(&sample.to_le_bytes());
750 let fg = FilterGraph::new().add_audio_gain_db(6.0206);
751 let frame = Frame::audio(data, 0);
752 let out = fg.apply(frame).expect("gain apply").expect("frame");
753 let s0 = i16::from_le_bytes([out.data[0], out.data[1]]);
754 assert!((s0 as i32 - 2000).abs() < 10, "expected ~2000, got {s0}");
755 }
756
757 #[test]
758 fn test_filter_graph_audio_gain_zero_db_noop() {
759 let sample: i16 = 5000;
760 let data = sample.to_le_bytes().to_vec();
761 let fg = FilterGraph::new().add_audio_gain_db(0.0);
762 let frame = Frame::audio(data.clone(), 0);
763 let out = fg.apply(frame).expect("0dB gain").expect("frame");
764 assert_eq!(out.data, data);
765 }
766
767 #[test]
768 fn test_filter_graph_audio_gain_skips_video() {
769 let data = vec![0xFFu8; 16];
771 let fg = FilterGraph::new().add_audio_gain_db(20.0);
772 let frame = Frame::video(data.clone(), 0, 4, 1);
773 let out = fg.apply(frame).expect("skip video").expect("frame");
774 assert_eq!(out.data, data);
775 }
776
777 #[test]
780 fn test_filter_graph_hdr_strip() {
781 let meta = HdrMetadata::hdr10(
782 MasteringDisplay::p3_d65_1000nit(),
783 ContentLightLevel::hdr10_default(),
784 );
785 let fg = FilterGraph::new().add_hdr_passthrough(HdrPassthroughMode::Strip);
786 let frame = Frame::video(vec![0u8; 4], 0, 2, 1).with_hdr(meta);
787 let out = fg.apply(frame).expect("strip hdr").expect("frame");
788 assert!(out.hdr_meta.is_none(), "HDR should be stripped");
789 }
790
791 #[test]
792 fn test_filter_graph_hdr_passthrough() {
793 let meta = HdrMetadata::hlg();
794 let fg = FilterGraph::new().add_hdr_passthrough(HdrPassthroughMode::Passthrough);
795 let frame = Frame::video(vec![0u8; 4], 0, 2, 1).with_hdr(meta);
796 let out = fg.apply(frame).expect("passthrough hdr").expect("frame");
797 assert!(out.hdr_meta.is_some(), "HDR should be preserved");
798 assert_eq!(
799 out.hdr_meta.as_ref().and_then(|m| m.transfer_function),
800 Some(TransferFunction::Hlg)
801 );
802 }
803
804 #[test]
807 fn test_pass_stats_default_zeroed() {
808 let s = PassStats::default();
809 assert_eq!(s.input_frames, 0);
810 assert_eq!(s.output_frames, 0);
811 assert_eq!(s.input_bytes, 0);
812 assert_eq!(s.output_bytes, 0);
813 assert_eq!(s.video_frames, 0);
814 assert_eq!(s.audio_frames, 0);
815 }
816
817 #[test]
818 fn test_transcode_stats_speed_factor_zero_when_no_time() {
819 let stats = TranscodeStats {
820 pass: PassStats {
821 input_frames: 100,
822 ..PassStats::default()
823 },
824 wall_time_secs: 0.0,
825 };
826 assert_eq!(stats.speed_factor(), 0.0);
827 }
828
829 #[test]
830 fn test_transcode_stats_speed_factor_computed() {
831 let stats = TranscodeStats {
832 pass: PassStats {
833 input_frames: 100,
834 ..PassStats::default()
835 },
836 wall_time_secs: 2.0,
837 };
838 assert!((stats.speed_factor() - 50.0).abs() < 0.001);
839 }
840
841 #[test]
844 fn test_hdr_passthrough_config_default() {
845 let cfg = HdrPassthroughConfig::default();
846 assert!(!cfg.enabled);
847 assert!(!cfg.convert_hdr10_to_hlg);
848 assert!(!cfg.inject_sei);
849 }
850
851 #[test]
852 fn test_hdr_passthrough_config_strip_mode() {
853 let cfg = HdrPassthroughConfig::strip();
854 assert!(matches!(cfg.to_mode(), HdrPassthroughMode::Strip));
855 }
856
857 #[test]
858 fn test_hdr_passthrough_config_passthrough_mode() {
859 let cfg = HdrPassthroughConfig::passthrough();
860 assert!(matches!(cfg.to_mode(), HdrPassthroughMode::Passthrough));
861 }
862
863 #[test]
864 fn test_hdr_passthrough_config_convert_hdr10_to_hlg() {
865 let cfg = HdrPassthroughConfig {
866 enabled: true,
867 convert_hdr10_to_hlg: true,
868 inject_sei: false,
869 };
870 let mode = cfg.to_mode();
871 match mode {
872 HdrPassthroughMode::Convert { target_tf, .. } => {
873 assert_eq!(target_tf, TransferFunction::Hlg);
874 }
875 _ => panic!("Expected Convert mode"),
876 }
877 }
878
879 #[test]
882 fn test_hdr_sei_injector_no_sei_inject_disabled() {
883 let cfg = HdrPassthroughConfig {
884 enabled: true,
885 inject_sei: false,
886 convert_hdr10_to_hlg: false,
887 };
888 let injector = HdrSeiInjector::new(cfg);
889 let data = vec![0xAAu8, 0xBB, 0xCC];
890 let result = injector.inject_into_packet(&data);
891 assert_eq!(result, data);
892 }
893
894 #[test]
895 fn test_hdr_sei_injector_no_sei_when_no_metadata_stored() {
896 let cfg = HdrPassthroughConfig {
897 enabled: true,
898 inject_sei: true,
899 convert_hdr10_to_hlg: false,
900 };
901 let injector = HdrSeiInjector::new(cfg);
902 let data = vec![0x01u8, 0x02, 0x03];
903 let result = injector.inject_into_packet(&data);
904 assert_eq!(result, data);
906 assert!(!injector.has_sei_data());
907 }
908
909 #[test]
910 fn test_hdr_sei_injector_stores_metadata_and_injects() {
911 let cfg = HdrPassthroughConfig {
912 enabled: true,
913 inject_sei: true,
914 convert_hdr10_to_hlg: false,
915 };
916 let mut injector = HdrSeiInjector::new(cfg);
917 let meta = HdrMetadata::hdr10(
918 MasteringDisplay::p3_d65_1000nit(),
919 ContentLightLevel::hdr10_default(),
920 );
921 injector.store_from_metadata(&meta);
922 assert!(injector.has_sei_data());
923
924 let payload = vec![0xDEu8, 0xAD];
925 let result = injector.inject_into_packet(&payload);
926 assert_eq!(result.len(), 28 + 2);
928 assert_eq!(&result[28..], &payload[..]);
930 }
931
932 #[test]
933 fn test_hdr_sei_injector_resolve_passthrough() {
934 let cfg = HdrPassthroughConfig::passthrough();
935 let injector = HdrSeiInjector::new(cfg);
936 let meta = HdrMetadata::hlg();
937 let resolved = injector
938 .resolve_output_metadata(Some(&meta))
939 .expect("resolve should succeed");
940 assert!(resolved.is_some());
941 assert_eq!(
942 resolved.as_ref().and_then(|m| m.transfer_function),
943 Some(TransferFunction::Hlg)
944 );
945 }
946
947 #[test]
948 fn test_hdr_sei_injector_resolve_strip() {
949 let cfg = HdrPassthroughConfig::strip();
950 let injector = HdrSeiInjector::new(cfg);
951 let meta = HdrMetadata::hdr10(
952 MasteringDisplay::p3_d65_1000nit(),
953 ContentLightLevel::hdr10_default(),
954 );
955 let resolved = injector
956 .resolve_output_metadata(Some(&meta))
957 .expect("resolve should succeed");
958 assert!(resolved.is_none(), "strip should produce None");
959 }
960
961 #[test]
962 fn test_hdr_sei_injector_resolve_convert_hdr10_to_hlg() {
963 let cfg = HdrPassthroughConfig {
964 enabled: true,
965 convert_hdr10_to_hlg: true,
966 inject_sei: false,
967 };
968 let injector = HdrSeiInjector::new(cfg);
969 let meta = HdrMetadata::hdr10(
970 MasteringDisplay::p3_d65_1000nit(),
971 ContentLightLevel::hdr10_default(),
972 );
973 let resolved = injector
974 .resolve_output_metadata(Some(&meta))
975 .expect("conversion should succeed");
976 assert_eq!(
977 resolved.as_ref().and_then(|m| m.transfer_function),
978 Some(TransferFunction::Hlg)
979 );
980 }
981}