camera_stream/platform/macos/
stream.rs1use std::panic::AssertUnwindSafe;
2use std::sync::{Arc, Mutex};
3
4use objc2::rc::Retained;
5use objc2::runtime::AnyObject;
6use objc2::runtime::ProtocolObject;
7use objc2::{AllocAnyThread, DefinedClass, define_class, msg_send};
8use objc2_av_foundation::{
9 AVCaptureConnection, AVCaptureDevice, AVCaptureDeviceFormat, AVCaptureDeviceInput,
10 AVCaptureOutput, AVCaptureSession, AVCaptureVideoDataOutput,
11 AVCaptureVideoDataOutputSampleBufferDelegate,
12};
13use objc2_core_media::CMSampleBuffer;
14use objc2_core_video::{
15 CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
16 kCVPixelBufferPixelFormatTypeKey,
17};
18use objc2_foundation::{NSDictionary, NSNumber, NSObjectProtocol, NSString};
19
20use crate::error::{Error, PlatformError};
21use crate::platform::macos::device::pixel_format_to_fourcc;
22use crate::platform::macos::frame::{MacosFrame, MacosTimestamp};
23use crate::stream::CameraStream;
24use crate::types::StreamConfig;
25
26fn catch_objc<R>(f: impl FnOnce() -> R + std::panic::UnwindSafe) -> Result<R, Error> {
28 objc2::exception::catch(f).map_err(|exception| {
29 let msg = exception
30 .map(|e| e.to_string())
31 .unwrap_or_else(|| "unknown Objective-C exception".into());
32 Error::Platform(PlatformError::Message(msg))
33 })
34}
35
36type FrameCallback = Box<dyn FnMut(&MacosFrame<'_>) + Send + 'static>;
37
38struct DelegateIvars {
39 callback: Arc<Mutex<Option<FrameCallback>>>,
40}
41
42define_class!(
43 #[unsafe(super(objc2_foundation::NSObject))]
44 #[ivars = DelegateIvars]
45 #[name = "CameraStreamSampleBufferDelegate"]
46 struct SampleBufferDelegate;
47
48 impl SampleBufferDelegate {
49 }
50
51 unsafe impl NSObjectProtocol for SampleBufferDelegate {}
52
53 unsafe impl AVCaptureVideoDataOutputSampleBufferDelegate for SampleBufferDelegate {
54 #[unsafe(method(captureOutput:didOutputSampleBuffer:fromConnection:))]
55 #[allow(non_snake_case)]
56 unsafe fn captureOutput_didOutputSampleBuffer_fromConnection(
57 &self,
58 _output: &AVCaptureOutput,
59 sample_buffer: &CMSampleBuffer,
60 _connection: &AVCaptureConnection,
61 ) {
62 let pixel_buffer = match unsafe { sample_buffer.image_buffer() } {
64 Some(pb) => pb,
65 None => return,
66 };
67
68 let cm_time = unsafe { sample_buffer.presentation_time_stamp() };
70 let timestamp = MacosTimestamp {
71 value: cm_time.value,
72 timescale: cm_time.timescale,
73 flags: cm_time.flags.0,
74 epoch: cm_time.epoch,
75 };
76
77 let lock_flags = CVPixelBufferLockFlags::ReadOnly;
79 unsafe {
80 CVPixelBufferLockBaseAddress(&pixel_buffer, lock_flags);
81 }
82
83 let frame = unsafe { MacosFrame::from_locked_pixel_buffer(&pixel_buffer, timestamp) };
84
85 if let Ok(mut guard) = self.ivars().callback.lock()
86 && let Some(ref mut cb) = *guard {
87 cb(&frame);
88 }
89
90 unsafe {
91 CVPixelBufferUnlockBaseAddress(&pixel_buffer, lock_flags);
92 }
93 }
94 }
95);
96
97impl SampleBufferDelegate {
98 fn new(callback: FrameCallback) -> Retained<Self> {
99 let ivars = DelegateIvars {
100 callback: Arc::new(Mutex::new(Some(callback))),
101 };
102 let obj = Self::alloc().set_ivars(ivars);
103 unsafe { msg_send![super(obj), init] }
104 }
105}
106
107pub struct MacosCameraStream {
109 session: Retained<AVCaptureSession>,
110 device: Retained<AVCaptureDevice>,
111 output: Retained<AVCaptureVideoDataOutput>,
112 delegate: Option<Retained<SampleBufferDelegate>>,
113 config_locked: bool,
115 running: bool,
116}
117
118impl MacosCameraStream {
119 pub(crate) fn new(
120 device: Retained<AVCaptureDevice>,
121 config: &StreamConfig,
122 ) -> Result<Self, Error> {
123 let session = unsafe { AVCaptureSession::new() };
124
125 let input = unsafe { AVCaptureDeviceInput::deviceInputWithDevice_error(&device) }
127 .map_err(|e| Error::Platform(PlatformError::Message(e.to_string())))?;
128
129 let output = unsafe { AVCaptureVideoDataOutput::new() };
131
132 let target_fourcc = pixel_format_to_fourcc(&config.pixel_format);
135 unsafe {
136 let key: &NSString = std::mem::transmute::<&objc2_core_foundation::CFString, &NSString>(
137 kCVPixelBufferPixelFormatTypeKey,
138 );
139 let value = NSNumber::new_u32(target_fourcc);
140 let settings: Retained<NSDictionary<NSString, AnyObject>> =
141 NSDictionary::dictionaryWithObject_forKey(&value, ProtocolObject::from_ref(key));
142 output.setVideoSettings(Some(&settings));
143 }
144
145 let formats = unsafe { device.formats() };
147 let mut matched_format: Option<Retained<AVCaptureDeviceFormat>> = None;
148
149 for format in formats.iter() {
150 let desc = unsafe { format.formatDescription() };
151 let sub_type = unsafe { desc.media_sub_type() };
152 let dims = unsafe { objc2_core_media::CMVideoFormatDescriptionGetDimensions(&desc) };
153
154 if sub_type == target_fourcc
155 && dims.width as u32 == config.size.width
156 && dims.height as u32 == config.size.height
157 {
158 matched_format = Some(format.clone());
159 break;
160 }
161 }
162
163 let matched = matched_format.ok_or(Error::UnsupportedFormat)?;
164
165 let frame_duration = objc2_core_media::CMTime {
166 value: config.frame_rate.denominator as i64,
167 timescale: config.frame_rate.numerator as i32,
168 flags: objc2_core_media::CMTimeFlags(1), epoch: 0,
170 };
171
172 catch_objc(AssertUnwindSafe(|| unsafe {
173 session.beginConfiguration();
174
175 if !session.canAddInput(&input) {
177 session.commitConfiguration();
178 return Err(Error::Platform(PlatformError::Message(
179 "cannot add input to session".into(),
180 )));
181 }
182 session.addInput(&input);
183
184 if !session.canAddOutput(&output) {
186 session.commitConfiguration();
187 return Err(Error::Platform(PlatformError::Message(
188 "cannot add output to session".into(),
189 )));
190 }
191 session.addOutput(&output);
192
193 session.commitConfiguration();
194 Ok::<(), Error>(())
195 }))??;
196
197 unsafe { device.lockForConfiguration() }
202 .map_err(|e| Error::Platform(PlatformError::Message(e.to_string())))?;
203
204 catch_objc(AssertUnwindSafe(|| unsafe {
205 device.setActiveFormat(&matched);
206 device.setActiveVideoMinFrameDuration(frame_duration);
207 device.setActiveVideoMaxFrameDuration(frame_duration);
208 }))?;
209
210 Ok(MacosCameraStream {
211 session,
212 device,
213 output,
214 delegate: None,
215 config_locked: true,
216 running: false,
217 })
218 }
219}
220
221impl CameraStream for MacosCameraStream {
222 type Frame<'a> = MacosFrame<'a>;
223 type Error = Error;
224
225 fn start<F>(&mut self, callback: F) -> Result<(), Self::Error>
226 where
227 F: FnMut(&Self::Frame<'_>) + Send + 'static,
228 {
229 if self.running {
230 return Err(Error::AlreadyStarted);
231 }
232
233 let delegate = SampleBufferDelegate::new(Box::new(callback));
234
235 let queue = dispatch2::DispatchQueue::new(
236 "camera-stream.callback",
237 dispatch2::DispatchQueueAttr::SERIAL,
238 );
239
240 unsafe {
241 self.output.setSampleBufferDelegate_queue(
242 Some(ProtocolObject::from_ref(&*delegate)),
243 Some(&queue),
244 );
245 }
246
247 self.delegate = Some(delegate);
248
249 catch_objc(AssertUnwindSafe(|| unsafe { self.session.startRunning() }))?;
250 self.running = true;
251
252 if self.config_locked {
255 unsafe { self.device.unlockForConfiguration() };
256 self.config_locked = false;
257 }
258
259 Ok(())
260 }
261
262 fn stop(&mut self) -> Result<(), Self::Error> {
263 if !self.running {
264 return Err(Error::NotStarted);
265 }
266
267 unsafe { self.session.stopRunning() };
268
269 unsafe {
270 self.output.setSampleBufferDelegate_queue(None, None);
271 }
272
273 if let Some(ref delegate) = self.delegate
275 && let Ok(mut guard) = delegate.ivars().callback.lock()
276 {
277 *guard = None;
278 }
279 self.delegate = None;
280 self.running = false;
281
282 Ok(())
283 }
284}
285
286impl Drop for MacosCameraStream {
287 fn drop(&mut self) {
288 if self.running {
289 let _ = self.stop();
290 }
291 if self.config_locked {
292 unsafe { self.device.unlockForConfiguration() };
293 self.config_locked = false;
294 }
295 }
296}