openipc_video/backends/macos/
decoder.rs1use crate::{
2 runtime::{LatestFrameMailbox, StatsHandle},
3 CodecCapability, CodecConfig, CodecConfigTracker, ConfigUpdate, DecodedFrame,
4 DecoderCapabilities, DecoderOptions, DecoderStats, EncodedAccessUnit, SubmitOutcome,
5 VideoCodec, VideoDecoder, VideoError,
6};
7
8use super::{callback::CallbackState, session::VideoToolboxSession, MacOsVideoFrame};
9
10pub struct MacOsDecoder {
12 options: DecoderOptions,
13 tracker: CodecConfigTracker,
14 session: Option<VideoToolboxSession>,
15 active_config: Option<CodecConfig>,
16 waiting_for_keyframe: bool,
17 frames: LatestFrameMailbox<DecodedFrame<MacOsVideoFrame>>,
18 stats: StatsHandle,
19 callback: CallbackState,
20}
21
22impl MacOsDecoder {
23 pub fn new(options: DecoderOptions) -> Result<Self, VideoError> {
25 if options.max_frames_in_flight == 0 {
26 return Err(VideoError::InvalidOption(
27 "max_frames_in_flight must be greater than zero",
28 ));
29 }
30 let frames = LatestFrameMailbox::default();
31 let stats = StatsHandle::default();
32 let callback = CallbackState::new(frames.clone(), stats.clone());
33 Ok(Self {
34 options,
35 tracker: CodecConfigTracker::default(),
36 session: None,
37 active_config: None,
38 waiting_for_keyframe: true,
39 frames,
40 stats,
41 callback,
42 })
43 }
44
45 pub fn probe_capabilities() -> DecoderCapabilities {
47 use objc2_core_media::{kCMVideoCodecType_H264, kCMVideoCodecType_HEVC};
48 use objc2_video_toolbox::VTIsHardwareDecodeSupported;
49
50 let h264 = unsafe { VTIsHardwareDecodeSupported(kCMVideoCodecType_H264) };
53 let h265 = unsafe { VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) };
55 DecoderCapabilities {
56 backend: "videotoolbox",
57 codecs: vec![
58 CodecCapability {
59 codec: VideoCodec::H264,
60 supported: true,
61 hardware_accelerated: h264,
62 hardware_acceleration_known: true,
63 },
64 CodecCapability {
65 codec: VideoCodec::H265,
66 supported: true,
67 hardware_accelerated: h265,
68 hardware_acceleration_known: true,
69 },
70 ],
71 native_surfaces: true,
72 }
73 }
74
75 fn supports_requested_codec(&self, codec: VideoCodec) -> Result<(), VideoError> {
76 let capability = Self::probe_capabilities().codec(codec);
77 let Some(capability) = capability.filter(|entry| entry.supported) else {
78 return Err(VideoError::UnsupportedCodec {
79 codec,
80 backend: "videotoolbox",
81 });
82 };
83 if self.options.require_hardware && !capability.hardware_accelerated {
84 return Err(VideoError::HardwareDecoderUnavailable {
85 codec,
86 backend: "videotoolbox",
87 });
88 }
89 Ok(())
90 }
91
92 fn replace_session(&mut self, config: CodecConfig) -> Result<(), VideoError> {
93 log::info!(target: "openipc_video::videotoolbox", "configuring decoder codec={}", config.codec());
94 self.supports_requested_codec(config.codec())?;
95 if let Some(session) = self.session.take() {
96 session.finish()?;
97 }
98 self.frames.clear();
99 self.session = Some(VideoToolboxSession::new(
100 &config,
101 self.options,
102 self.callback.clone(),
103 )?);
104 self.active_config = Some(config);
105 self.waiting_for_keyframe = true;
106 self.stats.update(|stats| stats.reconfigurations += 1);
107 Ok(())
108 }
109}
110
111impl VideoDecoder for MacOsDecoder {
112 type Surface = MacOsVideoFrame;
113
114 fn capabilities(&self) -> DecoderCapabilities {
115 Self::probe_capabilities()
116 }
117
118 fn configure(&mut self, config: CodecConfig) -> Result<(), VideoError> {
119 self.replace_session(config)
120 }
121
122 fn submit(&mut self, mut frame: EncodedAccessUnit) -> Result<SubmitOutcome, VideoError> {
123 self.stats.update(|stats| stats.access_units_received += 1);
124 let (update, observed_keyframe) = self.tracker.inspect(frame.codec, &frame.data)?;
125 frame.keyframe |= observed_keyframe;
126 let mut reconfigured = false;
127 if let ConfigUpdate::Changed(config) = update {
128 self.replace_session(config)?;
129 reconfigured = true;
130 } else if self.session.is_none() {
131 let Some(config) = self.tracker.config(frame.codec).cloned() else {
132 self.stats.update(|stats| stats.waiting_drops += 1);
133 return Ok(SubmitOutcome::WaitingForConfiguration);
134 };
135 self.replace_session(config)?;
136 reconfigured = true;
137 }
138
139 let configured = self
140 .active_config
141 .as_ref()
142 .map(CodecConfig::codec)
143 .expect("configured VideoToolbox session must have an active codec");
144 if configured != frame.codec {
145 return Err(VideoError::CodecMismatch {
146 configured,
147 received: frame.codec,
148 });
149 }
150 if self.waiting_for_keyframe && !frame.can_resynchronize() {
151 self.stats.update(|stats| stats.waiting_drops += 1);
152 return Ok(SubmitOutcome::WaitingForKeyframe);
153 }
154 if self.stats.snapshot().frames_in_flight >= self.options.max_frames_in_flight {
155 log::warn!(target: "openipc_video::videotoolbox", "decoder backpressure; waiting for the next keyframe");
156 self.frames.clear();
157 self.waiting_for_keyframe = true;
158 self.stats.update(|stats| stats.backpressure_drops += 1);
159 return Ok(SubmitOutcome::DroppedForBackpressure);
160 }
161
162 self.session
163 .as_mut()
164 .expect("configured VideoToolbox session must exist")
165 .submit(&mut frame)?;
166 if frame.keyframe {
167 self.waiting_for_keyframe = false;
168 }
169 Ok(if reconfigured {
170 SubmitOutcome::Reconfigured
171 } else {
172 SubmitOutcome::Submitted
173 })
174 }
175
176 fn latest_frame(&mut self) -> Option<DecodedFrame<Self::Surface>> {
177 self.frames.take()
178 }
179
180 fn flush(&mut self) -> Result<(), VideoError> {
181 if let Some(session) = self.session.take() {
182 session.finish()?;
183 }
184 self.frames.clear();
185 self.tracker.reset();
186 self.active_config = None;
187 self.waiting_for_keyframe = true;
188 Ok(())
189 }
190
191 fn stats(&self) -> DecoderStats {
192 self.stats.snapshot()
193 }
194}
195
196impl Drop for MacOsDecoder {
197 fn drop(&mut self) {
198 if let Some(session) = self.session.take() {
199 let _ = session.finish();
200 }
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::MacOsDecoder;
207
208 #[test]
209 fn decoder_can_move_to_a_worker_thread() {
210 fn assert_send_sync<T: Send + Sync>() {}
211 assert_send_sync::<MacOsDecoder>();
212 }
213}