1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4use crate::{NativeFrame, NativeFrameMetadata, NativeFramePipelineProfile, NativeHandleKind};
5
6#[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 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 pub fn metadata(&self) -> &'a NativeFrameMetadata {
45 self.metadata
46 }
47
48 pub fn native_handle(&self) -> usize {
53 self.native_handle
54 }
55
56 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#[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#[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 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 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 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 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 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#[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 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 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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
296pub enum FrameProcessorSubmitStatus {
297 Accepted,
298 Bypassed,
299 Backpressure,
300 Rejected,
301}
302
303#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
328pub enum FrameProcessorReceiveStatus {
329 Frame,
330 Pending,
331 EndOfStream,
332}
333
334#[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#[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#[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#[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#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
414pub struct FrameProcessorOperationStatus {
415 pub completed: bool,
416}
417
418#[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
463pub 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
475pub 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}