Skip to main content

player_plugin/
frame_processor.rs

1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4use crate::{NativeFrame, NativeFrameMetadata, NativeFramePipelineProfile, NativeHandleKind};
5
6/// Call-scoped view of a host-owned native frame submitted to a processor.
7///
8/// The view is intentionally neither `Clone` nor `Copy`. Its native handle is
9/// valid only for the synchronous submit/receive sequence. A plugin may echo
10/// the handle through `borrowed_passthrough`, but it must retain the platform
11/// resource through an explicit platform API before performing asynchronous
12/// work or returning an owned output.
13///
14/// ```compile_fail
15/// use player_plugin::FrameProcessorInputFrame;
16///
17/// fn retain_input(frame: FrameProcessorInputFrame<'_>) {
18///     let _retained = frame.clone();
19/// }
20/// ```
21#[must_use = "the borrowed native frame is valid only for the submit callback"]
22pub struct FrameProcessorInputFrame<'a> {
23    metadata: &'a NativeFrameMetadata,
24    native_handle: usize,
25}
26
27impl<'a> FrameProcessorInputFrame<'a> {
28    /// Borrows an existing native frame for one synchronous submit call.
29    pub fn new(frame: &'a NativeFrame) -> Self {
30        Self {
31            metadata: &frame.metadata,
32            native_handle: frame.handle,
33        }
34    }
35
36    pub(crate) fn from_abi(metadata: &'a NativeFrameMetadata, native_handle: usize) -> Self {
37        Self {
38            metadata,
39            native_handle,
40        }
41    }
42
43    /// Returns metadata borrowed from the host-owned input frame.
44    pub fn metadata(&self) -> &'a NativeFrameMetadata {
45        self.metadata
46    }
47
48    /// Returns the call-scoped opaque native handle.
49    ///
50    /// Copying this integer does not retain the underlying platform resource;
51    /// using it after `submit_frame` returns violates the plugin contract.
52    pub fn native_handle(&self) -> usize {
53        self.native_handle
54    }
55
56    /// Creates a host-owned passthrough result without transferring ownership.
57    ///
58    /// The returned frame must only be used as the immediate output for this
59    /// input. The host keeps the upstream resource alive while consuming that
60    /// output and will not call `FrameProcessorSession::release_frame` for it.
61    pub fn borrowed_passthrough(&self) -> NativeFrame {
62        let mut metadata = self.metadata.clone();
63        metadata.release_tracking = Some(crate::NativeFrameReleaseTracking {
64            frame_id: metadata.frame_id,
65            requires_release: false,
66        });
67        NativeFrame {
68            metadata,
69            handle: self.native_handle,
70            lease_token: None,
71        }
72    }
73}
74
75/// Frame metadata and scheduling hints submitted to a frame processor.
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct FrameProcessorSubmitFrame {
78    pub metadata: NativeFrameMetadata,
79    #[serde(default)]
80    pub present_deadline_us: Option<i64>,
81}
82
83impl FrameProcessorSubmitFrame {
84    pub fn new(metadata: NativeFrameMetadata) -> Self {
85        Self {
86            metadata,
87            present_deadline_us: None,
88        }
89    }
90}
91
92/// Native-frame capabilities advertised by a frame processor plugin.
93#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
94pub struct FrameProcessorCapabilities {
95    pub accepted_input_handle_kinds: Vec<NativeHandleKind>,
96    pub output_handle_kinds: Vec<NativeHandleKind>,
97    #[serde(default)]
98    pub accepted_input_pipeline_profiles: Vec<NativeFramePipelineProfile>,
99    #[serde(default)]
100    pub output_pipeline_profiles: Vec<NativeFramePipelineProfile>,
101    pub supports_video_frames: bool,
102    pub supports_in_place_passthrough: bool,
103    pub preserves_dimensions: bool,
104    pub may_change_dimensions: bool,
105    #[serde(default)]
106    pub preserves_color_metadata: bool,
107    #[serde(default)]
108    pub preserves_hdr_metadata: bool,
109    pub supports_flush: bool,
110    pub max_sessions: Option<u32>,
111    pub max_in_flight_frames: Option<u32>,
112}
113
114impl FrameProcessorCapabilities {
115    /// Returns whether the processor accepts an input native handle kind.
116    pub fn supports_input_handle_kind(&self, handle_kind: &NativeHandleKind) -> bool {
117        self.accepted_input_handle_kinds.is_empty()
118            || self
119                .accepted_input_handle_kinds
120                .iter()
121                .any(|candidate| candidate == handle_kind)
122    }
123
124    /// Returns whether the processor accepts a native-frame pipeline profile.
125    pub fn supports_input_pipeline_profile(&self, profile: &NativeFramePipelineProfile) -> bool {
126        self.accepted_input_pipeline_profiles.is_empty()
127            || self
128                .accepted_input_pipeline_profiles
129                .iter()
130                .any(|candidate| candidate == profile)
131    }
132
133    /// Returns whether both handle kind and pipeline profile match the metadata.
134    pub fn supports_input_metadata(&self, metadata: &NativeFrameMetadata) -> bool {
135        self.supports_input_handle_kind(&metadata.handle_kind)
136            && self.supports_input_pipeline_profile(&metadata.effective_pipeline_profile())
137    }
138
139    /// Returns whether the processor can produce an output native handle kind.
140    pub fn supports_output_handle_kind(&self, handle_kind: &NativeHandleKind) -> bool {
141        self.output_handle_kinds.is_empty()
142            || self
143                .output_handle_kinds
144                .iter()
145                .any(|candidate| candidate == handle_kind)
146    }
147
148    /// Returns whether the processor can produce an output pipeline profile.
149    pub fn supports_output_pipeline_profile(&self, profile: &NativeFramePipelineProfile) -> bool {
150        self.output_pipeline_profiles.is_empty()
151            || self
152                .output_pipeline_profiles
153                .iter()
154                .any(|candidate| candidate == profile)
155    }
156}
157
158/// Capability requirements used when opening one frame processor session.
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub struct FrameProcessorSessionRequirements {
161    pub input_metadata: NativeFrameMetadata,
162    #[serde(default)]
163    pub output_handle_kind: Option<NativeHandleKind>,
164    #[serde(default)]
165    pub output_pipeline_profile: Option<NativeFramePipelineProfile>,
166    #[serde(default)]
167    pub require_video_frames: bool,
168    #[serde(default)]
169    pub require_native_first: bool,
170    #[serde(default)]
171    pub require_explicit_native_input: bool,
172    #[serde(default)]
173    pub require_flush: bool,
174    #[serde(default)]
175    pub reject_dimension_changes: bool,
176    #[serde(default)]
177    pub require_color_metadata_preservation: bool,
178    #[serde(default)]
179    pub require_hdr_metadata_preservation: bool,
180    #[serde(default)]
181    pub max_in_flight_frames: Option<u32>,
182}
183
184impl FrameProcessorSessionRequirements {
185    /// Builds native video requirements for a decoded frame processor chain.
186    pub fn native_video(input_metadata: NativeFrameMetadata) -> Self {
187        Self {
188            output_handle_kind: Some(input_metadata.handle_kind.clone()),
189            output_pipeline_profile: Some(input_metadata.effective_pipeline_profile()),
190            require_color_metadata_preservation: input_metadata.requires_color_preservation(),
191            require_hdr_metadata_preservation: input_metadata.requires_hdr_preservation(),
192            input_metadata,
193            require_video_frames: true,
194            require_native_first: true,
195            require_explicit_native_input: false,
196            require_flush: false,
197            reject_dimension_changes: true,
198            max_in_flight_frames: None,
199        }
200    }
201
202    /// Returns missing capability names for this requirement.
203    pub fn missing_capabilities(&self, capabilities: &FrameProcessorCapabilities) -> Vec<String> {
204        let mut missing = Vec::new();
205        if self.require_video_frames && !capabilities.supports_video_frames {
206            missing.push("video frames".to_owned());
207        }
208        if self.require_native_first
209            && !capabilities.supports_input_handle_kind(&self.input_metadata.handle_kind)
210        {
211            missing.push(format!(
212                "input handle kind {:?}",
213                self.input_metadata.handle_kind
214            ));
215        }
216        let input_profile = self.input_metadata.effective_pipeline_profile();
217        if self.require_native_first
218            && !capabilities.supports_input_pipeline_profile(&input_profile)
219        {
220            missing.push(format!("input pipeline profile {:?}", input_profile));
221        }
222        if self.require_explicit_native_input
223            && !capabilities
224                .accepted_input_handle_kinds
225                .contains(&self.input_metadata.handle_kind)
226        {
227            missing.push(format!(
228                "explicit input handle kind {:?}",
229                self.input_metadata.handle_kind
230            ));
231        }
232        if self.require_explicit_native_input
233            && !capabilities
234                .accepted_input_pipeline_profiles
235                .contains(&input_profile)
236        {
237            missing.push(format!(
238                "explicit input pipeline profile {:?}",
239                input_profile
240            ));
241        }
242        if let Some(handle_kind) = &self.output_handle_kind
243            && !capabilities.supports_output_handle_kind(handle_kind)
244        {
245            missing.push(format!("output handle kind {handle_kind:?}"));
246        }
247        if let Some(profile) = &self.output_pipeline_profile
248            && !capabilities.supports_output_pipeline_profile(profile)
249        {
250            missing.push(format!("output pipeline profile {profile:?}"));
251        }
252        if self.require_flush && !capabilities.supports_flush {
253            missing.push("flush support".to_owned());
254        }
255        if self.reject_dimension_changes && capabilities.may_change_dimensions {
256            missing.push("stable dimensions".to_owned());
257        }
258        if self.require_color_metadata_preservation && !capabilities.preserves_color_metadata {
259            missing.push("preservesColorMetadata".to_owned());
260        }
261        if self.require_hdr_metadata_preservation && !capabilities.preserves_hdr_metadata {
262            missing.push("preservesHdrMetadata".to_owned());
263        }
264        if let (Some(required), Some(limit)) =
265            (self.max_in_flight_frames, capabilities.max_in_flight_frames)
266            && limit < required
267        {
268            missing.push(format!("max in-flight frames >= {required}"));
269        }
270        missing
271    }
272}
273
274/// Configuration used to open one frame processor session.
275#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276pub struct FrameProcessorSessionConfig {
277    pub processor_index: usize,
278    pub input_metadata: NativeFrameMetadata,
279    #[serde(default)]
280    pub max_in_flight_frames: Option<u32>,
281}
282
283/// Optional session metadata returned after opening a frame processor session.
284#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
285pub struct FrameProcessorSessionInfo {
286    pub processor_name: Option<String>,
287    pub selected_backend: Option<String>,
288    pub output_handle_kind: Option<NativeHandleKind>,
289    #[serde(default)]
290    pub output_pipeline_profile: Option<NativeFramePipelineProfile>,
291    pub max_in_flight_frames: Option<u32>,
292}
293
294/// Submit state returned after handing a frame to a processor.
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
296pub enum FrameProcessorSubmitStatus {
297    Accepted,
298    Bypassed,
299    Backpressure,
300    Rejected,
301}
302
303/// Structured result returned by a submit operation.
304#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
305pub struct FrameProcessorSubmitResult {
306    pub status: FrameProcessorSubmitStatus,
307    #[serde(default)]
308    pub queue_depth: Option<u32>,
309    #[serde(default)]
310    pub in_flight_frames: Option<u32>,
311    #[serde(default)]
312    pub message: Option<String>,
313}
314
315impl Default for FrameProcessorSubmitResult {
316    fn default() -> Self {
317        Self {
318            status: FrameProcessorSubmitStatus::Accepted,
319            queue_depth: None,
320            in_flight_frames: None,
321            message: None,
322        }
323    }
324}
325
326/// Receive state encoded in frame processor output metadata over the C ABI.
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
328pub enum FrameProcessorReceiveStatus {
329    Frame,
330    Pending,
331    EndOfStream,
332}
333
334/// Timing metadata reported for one processed output.
335#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
336pub struct FrameProcessorFrameTimings {
337    #[serde(default)]
338    pub queue_wait_us: Option<u64>,
339    #[serde(default)]
340    pub process_time_us: Option<u64>,
341    #[serde(default)]
342    pub submit_to_ready_us: Option<u64>,
343}
344
345/// Metadata returned by the dynamic ABI receive call.
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
347pub struct FrameProcessorReceiveFrameMetadata {
348    pub status: FrameProcessorReceiveStatus,
349    #[serde(default)]
350    pub frame: Option<NativeFrameMetadata>,
351    #[serde(default)]
352    pub timings: FrameProcessorFrameTimings,
353    #[serde(default)]
354    pub source_frame_id: Option<u64>,
355    #[serde(default)]
356    pub message: Option<String>,
357}
358
359impl FrameProcessorReceiveFrameMetadata {
360    pub fn frame(frame: NativeFrameMetadata) -> Self {
361        Self {
362            status: FrameProcessorReceiveStatus::Frame,
363            frame: Some(frame),
364            timings: FrameProcessorFrameTimings::default(),
365            source_frame_id: None,
366            message: None,
367        }
368    }
369
370    pub fn pending() -> Self {
371        Self {
372            status: FrameProcessorReceiveStatus::Pending,
373            frame: None,
374            timings: FrameProcessorFrameTimings::default(),
375            source_frame_id: None,
376            message: None,
377        }
378    }
379
380    pub fn end_of_stream() -> Self {
381        Self {
382            status: FrameProcessorReceiveStatus::EndOfStream,
383            frame: None,
384            timings: FrameProcessorFrameTimings::default(),
385            source_frame_id: None,
386            message: None,
387        }
388    }
389}
390
391/// Processor-owned output frame returned by a frame processor session.
392#[derive(Debug, Clone, PartialEq, Eq)]
393pub struct FrameProcessorOutputFrame {
394    pub frame: NativeFrame,
395    pub timings: FrameProcessorFrameTimings,
396    pub source_frame_id: Option<u64>,
397    pub message: Option<String>,
398}
399
400/// Rust-side receive result returned by frame processor sessions.
401#[allow(
402    clippy::large_enum_variant,
403    reason = "boxing Frame would break the public frame processor session API"
404)]
405#[derive(Debug, Clone, PartialEq, Eq)]
406pub enum FrameProcessorReceiveOutput {
407    Frame(FrameProcessorOutputFrame),
408    Pending,
409    EndOfStream,
410}
411
412/// Empty success payload used by flush/close operations.
413#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
414pub struct FrameProcessorOperationStatus {
415    pub completed: bool,
416}
417
418/// Error payload shared by frame processor plugins and host-side adapters.
419#[derive(Debug, Error, Clone, PartialEq, Eq, Serialize, Deserialize)]
420pub enum FrameProcessorError {
421    #[error("unsupported native handle kind: {handle_kind}")]
422    UnsupportedHandle { handle_kind: String },
423    #[error("frame processor payload codec error: {message}")]
424    PayloadCodec { message: String },
425    #[error("frame processor ABI violation: {message}")]
426    AbiViolation { message: String },
427    #[error("frame processor session is not configured")]
428    NotConfigured,
429    #[error("frame processor backpressure: {message}")]
430    Backpressure { message: String },
431    #[error("frame processor timeout: {message}")]
432    Timeout { message: String },
433    #[error("frame processor internal error: {message}")]
434    Internal { message: String },
435}
436
437impl FrameProcessorError {
438    pub fn payload_codec(message: impl Into<String>) -> Self {
439        Self::PayloadCodec {
440            message: message.into(),
441        }
442    }
443
444    pub fn abi_violation(message: impl Into<String>) -> Self {
445        Self::AbiViolation {
446            message: message.into(),
447        }
448    }
449
450    pub fn unsupported_handle(handle_kind: impl Into<String>) -> Self {
451        Self::UnsupportedHandle {
452            handle_kind: handle_kind.into(),
453        }
454    }
455
456    pub fn internal(message: impl Into<String>) -> Self {
457        Self::Internal {
458            message: message.into(),
459        }
460    }
461}
462
463/// Creates frame processor sessions for one plugin.
464pub trait FrameProcessorPluginFactory: Send + Sync {
465    fn name(&self) -> &str;
466
467    fn capabilities(&self) -> FrameProcessorCapabilities;
468
469    fn open_session(
470        &self,
471        config: &FrameProcessorSessionConfig,
472    ) -> Result<Box<dyn FrameProcessorSession>, FrameProcessorError>;
473}
474
475/// Stateful native-frame processor session created by a frame processor plugin.
476pub trait FrameProcessorSession: Send {
477    fn session_info(&self) -> FrameProcessorSessionInfo;
478
479    fn submit_frame(
480        &mut self,
481        frame: FrameProcessorInputFrame<'_>,
482        submit: &FrameProcessorSubmitFrame,
483    ) -> Result<FrameProcessorSubmitResult, FrameProcessorError>;
484
485    fn receive_frame(&mut self) -> Result<FrameProcessorReceiveOutput, FrameProcessorError>;
486
487    fn release_frame(&mut self, frame: NativeFrame) -> Result<(), FrameProcessorError>;
488
489    fn flush(&mut self) -> Result<(), FrameProcessorError>;
490
491    fn close(&mut self) -> Result<(), FrameProcessorError>;
492}
493
494#[cfg(test)]
495mod tests {
496    use super::{
497        FrameProcessorCapabilities, FrameProcessorFrameTimings, FrameProcessorReceiveFrameMetadata,
498        FrameProcessorReceiveStatus, FrameProcessorSessionRequirements, FrameProcessorSubmitResult,
499        FrameProcessorSubmitStatus,
500    };
501    use crate::{
502        DecoderFrameFormat, DecoderMediaKind, NativeFrameColorMetadata, NativeFrameHdrMetadata,
503        NativeFrameMetadata, NativeFramePipelineProfile, NativeHandleKind, VisibleRect,
504    };
505
506    fn metadata() -> NativeFrameMetadata {
507        NativeFrameMetadata {
508            media_kind: DecoderMediaKind::Video,
509            format: DecoderFrameFormat::Nv12,
510            codec: "h264".to_owned(),
511            pts_us: Some(1_000),
512            duration_us: Some(16_667),
513            width: 1_920,
514            height: 1_080,
515            coded_width: Some(1_920),
516            coded_height: Some(1_088),
517            visible_rect: Some(VisibleRect {
518                x: 0,
519                y: 0,
520                width: 1_920,
521                height: 1_080,
522            }),
523            handle_kind: NativeHandleKind::CvPixelBuffer,
524            pipeline_profile: Some(NativeFramePipelineProfile::VideoToolboxCvPixelBuffer),
525            color_space: Some("bt709".to_owned()),
526            hdr_metadata: None,
527            color: Some(NativeFrameColorMetadata {
528                primaries: Some("bt709".to_owned()),
529                transfer: Some("bt709".to_owned()),
530                matrix: Some("bt709".to_owned()),
531                range: Some("limited".to_owned()),
532                bit_depth: Some(8),
533            }),
534            hdr: None,
535            sync_info: None,
536            transform: None,
537            frame_id: Some(42),
538            release_tracking: None,
539        }
540    }
541
542    #[test]
543    fn frame_processor_submit_result_round_trips_through_json() {
544        let result = FrameProcessorSubmitResult {
545            status: FrameProcessorSubmitStatus::Backpressure,
546            queue_depth: Some(2),
547            in_flight_frames: Some(1),
548            message: Some("queue full".to_owned()),
549        };
550
551        let encoded = serde_json::to_string(&result).expect("serialize submit result");
552        let decoded: FrameProcessorSubmitResult =
553            serde_json::from_str(&encoded).expect("deserialize submit result");
554
555        assert_eq!(decoded, result);
556    }
557
558    #[test]
559    fn frame_processor_receive_metadata_round_trips_through_json() {
560        let receive = FrameProcessorReceiveFrameMetadata {
561            status: FrameProcessorReceiveStatus::Frame,
562            frame: Some(metadata()),
563            timings: FrameProcessorFrameTimings {
564                queue_wait_us: Some(10),
565                process_time_us: Some(20),
566                submit_to_ready_us: Some(30),
567            },
568            source_frame_id: Some(42),
569            message: None,
570        };
571
572        let encoded = serde_json::to_string(&receive).expect("serialize receive metadata");
573        let decoded: FrameProcessorReceiveFrameMetadata =
574            serde_json::from_str(&encoded).expect("deserialize receive metadata");
575
576        assert_eq!(decoded, receive);
577    }
578
579    #[test]
580    fn frame_processor_capabilities_accept_empty_handle_kind_list_as_wildcard() {
581        let capabilities = FrameProcessorCapabilities::default();
582
583        assert!(capabilities.supports_input_handle_kind(&NativeHandleKind::D3D11Texture2D));
584    }
585
586    #[test]
587    fn frame_processor_capabilities_accept_empty_pipeline_profile_list_as_wildcard() {
588        let capabilities = FrameProcessorCapabilities::default();
589
590        assert!(
591            capabilities
592                .supports_input_pipeline_profile(&NativeFramePipelineProfile::D3D11Texture2D)
593        );
594    }
595
596    #[test]
597    fn frame_processor_capabilities_default_metadata_preservation_fields() {
598        let decoded: FrameProcessorCapabilities = serde_json::from_str(
599            r#"{
600                "accepted_input_handle_kinds": [],
601                "output_handle_kinds": [],
602                "supports_video_frames": true,
603                "supports_in_place_passthrough": true,
604                "preserves_dimensions": true,
605                "may_change_dimensions": false,
606                "supports_flush": false,
607                "max_sessions": null,
608                "max_in_flight_frames": null
609            }"#,
610        )
611        .expect("legacy capabilities should deserialize without preservation fields");
612
613        assert!(!decoded.preserves_color_metadata);
614        assert!(!decoded.preserves_hdr_metadata);
615    }
616
617    #[test]
618    fn frame_processor_capabilities_match_input_metadata_by_handle_and_profile() {
619        let capabilities = FrameProcessorCapabilities {
620            accepted_input_handle_kinds: vec![NativeHandleKind::CvPixelBuffer],
621            output_handle_kinds: vec![NativeHandleKind::CvPixelBuffer],
622            accepted_input_pipeline_profiles: vec![
623                NativeFramePipelineProfile::VideoToolboxCvPixelBuffer,
624            ],
625            output_pipeline_profiles: vec![NativeFramePipelineProfile::VideoToolboxCvPixelBuffer],
626            supports_video_frames: true,
627            ..Default::default()
628        };
629
630        assert!(capabilities.supports_input_metadata(&metadata()));
631
632        let mut mismatched = metadata();
633        mismatched.pipeline_profile = Some(NativeFramePipelineProfile::D3D11Texture2D);
634        assert!(!capabilities.supports_input_metadata(&mismatched));
635    }
636
637    #[test]
638    fn frame_processor_session_requirements_report_missing_capabilities() {
639        let requirements = FrameProcessorSessionRequirements {
640            require_flush: true,
641            require_explicit_native_input: true,
642            max_in_flight_frames: Some(4),
643            ..FrameProcessorSessionRequirements::native_video(metadata())
644        };
645        let capabilities = FrameProcessorCapabilities {
646            accepted_input_handle_kinds: vec![NativeHandleKind::D3D11Texture2D],
647            output_handle_kinds: vec![NativeHandleKind::D3D11Texture2D],
648            accepted_input_pipeline_profiles: vec![NativeFramePipelineProfile::D3D11Texture2D],
649            output_pipeline_profiles: vec![NativeFramePipelineProfile::D3D11Texture2D],
650            supports_video_frames: false,
651            supports_flush: false,
652            may_change_dimensions: true,
653            preserves_color_metadata: false,
654            preserves_hdr_metadata: false,
655            max_in_flight_frames: Some(1),
656            ..Default::default()
657        };
658
659        let missing = requirements.missing_capabilities(&capabilities);
660
661        assert!(missing.iter().any(|item| item == "video frames"));
662        assert!(
663            missing
664                .iter()
665                .any(|item| item.contains("input handle kind CvPixelBuffer"))
666        );
667        assert!(
668            missing
669                .iter()
670                .any(|item| item.contains("explicit input handle kind CvPixelBuffer"))
671        );
672        assert!(
673            missing
674                .iter()
675                .any(|item| item.contains("output pipeline profile VideoToolboxCvPixelBuffer"))
676        );
677        assert!(missing.iter().any(|item| item == "flush support"));
678        assert!(missing.iter().any(|item| item == "stable dimensions"));
679        assert!(!missing.iter().any(|item| item == "preservesColorMetadata"));
680        assert!(
681            missing
682                .iter()
683                .any(|item| item == "max in-flight frames >= 4")
684        );
685    }
686
687    #[test]
688    fn frame_processor_session_requirements_report_color_preservation_for_wide_color() {
689        let mut metadata = metadata();
690        metadata.color_space = Some("bt2020".to_owned());
691        metadata.color = Some(NativeFrameColorMetadata {
692            primaries: Some("bt2020".to_owned()),
693            transfer: Some("sdr-video".to_owned()),
694            matrix: Some("bt2020-ncl".to_owned()),
695            range: Some("limited".to_owned()),
696            bit_depth: Some(8),
697        });
698        let requirements = FrameProcessorSessionRequirements::native_video(metadata);
699        let capabilities = FrameProcessorCapabilities {
700            accepted_input_handle_kinds: vec![NativeHandleKind::CvPixelBuffer],
701            output_handle_kinds: vec![NativeHandleKind::CvPixelBuffer],
702            accepted_input_pipeline_profiles: vec![
703                NativeFramePipelineProfile::VideoToolboxCvPixelBuffer,
704            ],
705            output_pipeline_profiles: vec![NativeFramePipelineProfile::VideoToolboxCvPixelBuffer],
706            supports_video_frames: true,
707            preserves_color_metadata: false,
708            ..Default::default()
709        };
710
711        let missing = requirements.missing_capabilities(&capabilities);
712
713        assert!(missing.iter().any(|item| item == "preservesColorMetadata"));
714    }
715
716    #[test]
717    fn frame_processor_session_requirements_report_hdr_preservation() {
718        let mut metadata = metadata();
719        metadata.hdr = Some(NativeFrameHdrMetadata {
720            kind: "hlg".to_owned(),
721            mastering_display: None,
722            content_light: None,
723            dolby_vision: None,
724        });
725        let requirements = FrameProcessorSessionRequirements::native_video(metadata);
726        let capabilities = FrameProcessorCapabilities {
727            accepted_input_handle_kinds: vec![NativeHandleKind::CvPixelBuffer],
728            output_handle_kinds: vec![NativeHandleKind::CvPixelBuffer],
729            accepted_input_pipeline_profiles: vec![
730                NativeFramePipelineProfile::VideoToolboxCvPixelBuffer,
731            ],
732            output_pipeline_profiles: vec![NativeFramePipelineProfile::VideoToolboxCvPixelBuffer],
733            supports_video_frames: true,
734            preserves_color_metadata: true,
735            preserves_hdr_metadata: false,
736            ..Default::default()
737        };
738
739        let missing = requirements.missing_capabilities(&capabilities);
740
741        assert!(missing.iter().any(|item| item == "preservesHdrMetadata"));
742    }
743}