1use anyhow::{anyhow, Context, Result};
2use core_foundation::{
3 array::{CFArray, CFArrayRef},
4 base::{CFRelease, CFRetain, TCFType},
5 string::{CFString, CFStringRef},
6};
7use futures::{
8 channel::{mpsc, oneshot},
9 Future,
10};
11pub use media::core_video::CVImageBuffer;
12use media::core_video::CVImageBufferRef;
13use parking_lot::Mutex;
14use postage::watch;
15use std::{
16 ffi::c_void,
17 sync::{Arc, Weak},
18};
19
20extern "C" {
21 fn LKRoomDelegateCreate(
22 callback_data: *mut c_void,
23 on_did_disconnect: extern "C" fn(callback_data: *mut c_void),
24 on_did_subscribe_to_remote_audio_track: extern "C" fn(
25 callback_data: *mut c_void,
26 publisher_id: CFStringRef,
27 track_id: CFStringRef,
28 remote_track: *const c_void,
29 ),
30 on_did_unsubscribe_from_remote_audio_track: extern "C" fn(
31 callback_data: *mut c_void,
32 publisher_id: CFStringRef,
33 track_id: CFStringRef,
34 ),
35 on_did_subscribe_to_remote_video_track: extern "C" fn(
36 callback_data: *mut c_void,
37 publisher_id: CFStringRef,
38 track_id: CFStringRef,
39 remote_track: *const c_void,
40 ),
41 on_did_unsubscribe_from_remote_video_track: extern "C" fn(
42 callback_data: *mut c_void,
43 publisher_id: CFStringRef,
44 track_id: CFStringRef,
45 ),
46 ) -> *const c_void;
47
48 fn LKRoomCreate(delegate: *const c_void) -> *const c_void;
49 fn LKRoomConnect(
50 room: *const c_void,
51 url: CFStringRef,
52 token: CFStringRef,
53 callback: extern "C" fn(*mut c_void, CFStringRef),
54 callback_data: *mut c_void,
55 );
56 fn LKRoomDisconnect(room: *const c_void);
57 fn LKRoomPublishVideoTrack(
58 room: *const c_void,
59 track: *const c_void,
60 callback: extern "C" fn(*mut c_void, *mut c_void, CFStringRef),
61 callback_data: *mut c_void,
62 );
63 fn LKRoomPublishAudioTrack(
64 room: *const c_void,
65 track: *const c_void,
66 callback: extern "C" fn(*mut c_void, *mut c_void, CFStringRef),
67 callback_data: *mut c_void,
68 );
69 fn LKRoomUnpublishTrack(room: *const c_void, publication: *const c_void);
70 fn LKRoomAudioTracksForRemoteParticipant(
71 room: *const c_void,
72 participant_id: CFStringRef,
73 ) -> CFArrayRef;
74
75 fn LKRoomVideoTracksForRemoteParticipant(
76 room: *const c_void,
77 participant_id: CFStringRef,
78 ) -> CFArrayRef;
79
80 fn LKVideoRendererCreate(
81 callback_data: *mut c_void,
82 on_frame: extern "C" fn(callback_data: *mut c_void, frame: CVImageBufferRef) -> bool,
83 on_drop: extern "C" fn(callback_data: *mut c_void),
84 ) -> *const c_void;
85
86 fn LKRemoteAudioTrackGetSid(track: *const c_void) -> CFStringRef;
87 fn LKVideoTrackAddRenderer(track: *const c_void, renderer: *const c_void);
88 fn LKRemoteVideoTrackGetSid(track: *const c_void) -> CFStringRef;
89
90 fn LKDisplaySources(
91 callback_data: *mut c_void,
92 callback: extern "C" fn(
93 callback_data: *mut c_void,
94 sources: CFArrayRef,
95 error: CFStringRef,
96 ),
97 );
98 fn LKCreateScreenShareTrackForDisplay(display: *const c_void) -> *const c_void;
99 fn LKLocalAudioTrackCreateTrack() -> *const c_void;
100
101 fn LKLocalTrackPublicationMute(
102 publication: *const c_void,
103 on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef),
104 callback_data: *mut c_void,
105 );
106 fn LKLocalTrackPublicationUnmute(
107 publication: *const c_void,
108 on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef),
109 callback_data: *mut c_void,
110 );
111}
112
113pub type Sid = String;
114
115#[derive(Clone, Eq, PartialEq)]
116pub enum ConnectionState {
117 Disconnected,
118 Connected { url: String, token: String },
119}
120
121pub struct Room {
122 native_room: *const c_void,
123 connection: Mutex<(
124 watch::Sender<ConnectionState>,
125 watch::Receiver<ConnectionState>,
126 )>,
127 remote_audio_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteAudioTrackUpdate>>>,
128 remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteVideoTrackUpdate>>>,
129 _delegate: RoomDelegate,
130}
131
132impl Room {
133 pub fn new() -> Arc<Self> {
134 Arc::new_cyclic(|weak_room| {
135 let delegate = RoomDelegate::new(weak_room.clone());
136 Self {
137 native_room: unsafe { LKRoomCreate(delegate.native_delegate) },
138 connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)),
139 remote_audio_track_subscribers: Default::default(),
140 remote_video_track_subscribers: Default::default(),
141 _delegate: delegate,
142 }
143 })
144 }
145
146 pub fn status(&self) -> watch::Receiver<ConnectionState> {
147 self.connection.lock().1.clone()
148 }
149
150 pub fn connect(self: &Arc<Self>, url: &str, token: &str) -> impl Future<Output = Result<()>> {
151 let url = CFString::new(url);
152 let token = CFString::new(token);
153 let (did_connect, tx, rx) = Self::build_done_callback();
154 unsafe {
155 LKRoomConnect(
156 self.native_room,
157 url.as_concrete_TypeRef(),
158 token.as_concrete_TypeRef(),
159 did_connect,
160 tx,
161 )
162 }
163
164 let this = self.clone();
165 let url = url.to_string();
166 let token = token.to_string();
167 async move {
168 rx.await.unwrap().context("error connecting to room")?;
169 *this.connection.lock().0.borrow_mut() = ConnectionState::Connected { url, token };
170 Ok(())
171 }
172 }
173
174 fn did_disconnect(&self) {
175 *self.connection.lock().0.borrow_mut() = ConnectionState::Disconnected;
176 }
177
178 pub fn display_sources(self: &Arc<Self>) -> impl Future<Output = Result<Vec<MacOSDisplay>>> {
179 extern "C" fn callback(tx: *mut c_void, sources: CFArrayRef, error: CFStringRef) {
180 unsafe {
181 let tx = Box::from_raw(tx as *mut oneshot::Sender<Result<Vec<MacOSDisplay>>>);
182
183 if sources.is_null() {
184 let _ = tx.send(Err(anyhow!("{}", CFString::wrap_under_get_rule(error))));
185 } else {
186 let sources = CFArray::wrap_under_get_rule(sources)
187 .into_iter()
188 .map(|source| MacOSDisplay::new(*source))
189 .collect();
190
191 let _ = tx.send(Ok(sources));
192 }
193 }
194 }
195
196 let (tx, rx) = oneshot::channel();
197
198 unsafe {
199 LKDisplaySources(Box::into_raw(Box::new(tx)) as *mut _, callback);
200 }
201
202 async move { rx.await.unwrap() }
203 }
204
205 pub fn publish_video_track(
206 self: &Arc<Self>,
207 track: &LocalVideoTrack,
208 ) -> impl Future<Output = Result<LocalTrackPublication>> {
209 let (tx, rx) = oneshot::channel::<Result<LocalTrackPublication>>();
210 extern "C" fn callback(tx: *mut c_void, publication: *mut c_void, error: CFStringRef) {
211 let tx =
212 unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<LocalTrackPublication>>) };
213 if error.is_null() {
214 let _ = tx.send(Ok(LocalTrackPublication(publication)));
215 } else {
216 let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
217 let _ = tx.send(Err(anyhow!(error)));
218 }
219 }
220 unsafe {
221 LKRoomPublishVideoTrack(
222 self.native_room,
223 track.0,
224 callback,
225 Box::into_raw(Box::new(tx)) as *mut c_void,
226 );
227 }
228 async { rx.await.unwrap().context("error publishing video track") }
229 }
230
231 pub fn publish_audio_track(
232 self: &Arc<Self>,
233 track: &LocalAudioTrack,
234 ) -> impl Future<Output = Result<LocalTrackPublication>> {
235 let (tx, rx) = oneshot::channel::<Result<LocalTrackPublication>>();
236 extern "C" fn callback(tx: *mut c_void, publication: *mut c_void, error: CFStringRef) {
237 let tx =
238 unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<LocalTrackPublication>>) };
239 if error.is_null() {
240 let _ = tx.send(Ok(LocalTrackPublication(publication)));
241 } else {
242 let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
243 let _ = tx.send(Err(anyhow!(error)));
244 }
245 }
246 unsafe {
247 LKRoomPublishAudioTrack(
248 self.native_room,
249 track.0,
250 callback,
251 Box::into_raw(Box::new(tx)) as *mut c_void,
252 );
253 }
254 async { rx.await.unwrap().context("error publishing video track") }
255 }
256
257 pub fn unpublish_track(&self, publication: LocalTrackPublication) {
258 unsafe {
259 LKRoomUnpublishTrack(self.native_room, publication.0);
260 }
261 }
262
263 pub fn remote_video_tracks(&self, participant_id: &str) -> Vec<Arc<RemoteVideoTrack>> {
264 unsafe {
265 let tracks = LKRoomVideoTracksForRemoteParticipant(
266 self.native_room,
267 CFString::new(participant_id).as_concrete_TypeRef(),
268 );
269
270 if tracks.is_null() {
271 Vec::new()
272 } else {
273 let tracks = CFArray::wrap_under_get_rule(tracks);
274 tracks
275 .into_iter()
276 .map(|native_track| {
277 let native_track = *native_track;
278 let id =
279 CFString::wrap_under_get_rule(LKRemoteVideoTrackGetSid(native_track))
280 .to_string();
281 Arc::new(RemoteVideoTrack::new(
282 native_track,
283 id,
284 participant_id.into(),
285 ))
286 })
287 .collect()
288 }
289 }
290 }
291
292 pub fn remote_audio_tracks(&self, participant_id: &str) -> Vec<Arc<RemoteAudioTrack>> {
293 unsafe {
294 let tracks = LKRoomAudioTracksForRemoteParticipant(
295 self.native_room,
296 CFString::new(participant_id).as_concrete_TypeRef(),
297 );
298
299 if tracks.is_null() {
300 Vec::new()
301 } else {
302 let tracks = CFArray::wrap_under_get_rule(tracks);
303 tracks
304 .into_iter()
305 .map(|native_track| {
306 let native_track = *native_track;
307 let id =
308 CFString::wrap_under_get_rule(LKRemoteAudioTrackGetSid(native_track))
309 .to_string();
310 Arc::new(RemoteAudioTrack::new(
311 native_track,
312 id,
313 participant_id.into(),
314 ))
315 })
316 .collect()
317 }
318 }
319 }
320
321 pub fn remote_audio_track_updates(&self) -> mpsc::UnboundedReceiver<RemoteAudioTrackUpdate> {
322 let (tx, rx) = mpsc::unbounded();
323 self.remote_audio_track_subscribers.lock().push(tx);
324 rx
325 }
326
327 pub fn remote_video_track_updates(&self) -> mpsc::UnboundedReceiver<RemoteVideoTrackUpdate> {
328 let (tx, rx) = mpsc::unbounded();
329 self.remote_video_track_subscribers.lock().push(tx);
330 rx
331 }
332
333 fn did_subscribe_to_remote_audio_track(&self, track: RemoteAudioTrack) {
334 let track = Arc::new(track);
335 self.remote_audio_track_subscribers.lock().retain(|tx| {
336 tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(track.clone()))
337 .is_ok()
338 });
339 }
340
341 fn did_unsubscribe_from_remote_audio_track(&self, publisher_id: String, track_id: String) {
342 self.remote_audio_track_subscribers.lock().retain(|tx| {
343 tx.unbounded_send(RemoteAudioTrackUpdate::Unsubscribed {
344 publisher_id: publisher_id.clone(),
345 track_id: track_id.clone(),
346 })
347 .is_ok()
348 });
349 }
350
351 fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) {
352 let track = Arc::new(track);
353 self.remote_video_track_subscribers.lock().retain(|tx| {
354 tx.unbounded_send(RemoteVideoTrackUpdate::Subscribed(track.clone()))
355 .is_ok()
356 });
357 }
358
359 fn did_unsubscribe_from_remote_video_track(&self, publisher_id: String, track_id: String) {
360 self.remote_video_track_subscribers.lock().retain(|tx| {
361 tx.unbounded_send(RemoteVideoTrackUpdate::Unsubscribed {
362 publisher_id: publisher_id.clone(),
363 track_id: track_id.clone(),
364 })
365 .is_ok()
366 });
367 }
368
369 fn build_done_callback() -> (
370 extern "C" fn(*mut c_void, CFStringRef),
371 *mut c_void,
372 oneshot::Receiver<Result<()>>,
373 ) {
374 let (tx, rx) = oneshot::channel();
375 extern "C" fn done_callback(tx: *mut c_void, error: CFStringRef) {
376 let tx = unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<()>>) };
377 if error.is_null() {
378 let _ = tx.send(Ok(()));
379 } else {
380 let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
381 let _ = tx.send(Err(anyhow!(error)));
382 }
383 }
384 (
385 done_callback,
386 Box::into_raw(Box::new(tx)) as *mut c_void,
387 rx,
388 )
389 }
390}
391
392impl Drop for Room {
393 fn drop(&mut self) {
394 unsafe {
395 LKRoomDisconnect(self.native_room);
396 CFRelease(self.native_room);
397 }
398 }
399}
400
401struct RoomDelegate {
402 native_delegate: *const c_void,
403 weak_room: *const Room,
404}
405
406impl RoomDelegate {
407 fn new(weak_room: Weak<Room>) -> Self {
408 let weak_room = Weak::into_raw(weak_room);
409 let native_delegate = unsafe {
410 LKRoomDelegateCreate(
411 weak_room as *mut c_void,
412 Self::on_did_disconnect,
413 Self::on_did_subscribe_to_remote_audio_track,
414 Self::on_did_unsubscribe_from_remote_audio_track,
415 Self::on_did_subscribe_to_remote_video_track,
416 Self::on_did_unsubscribe_from_remote_video_track,
417 )
418 };
419 Self {
420 native_delegate,
421 weak_room,
422 }
423 }
424
425 extern "C" fn on_did_disconnect(room: *mut c_void) {
426 let room = unsafe { Weak::from_raw(room as *mut Room) };
427 if let Some(room) = room.upgrade() {
428 room.did_disconnect();
429 }
430 let _ = Weak::into_raw(room);
431 }
432
433 extern "C" fn on_did_subscribe_to_remote_audio_track(
434 room: *mut c_void,
435 publisher_id: CFStringRef,
436 track_id: CFStringRef,
437 track: *const c_void,
438 ) {
439 let room = unsafe { Weak::from_raw(room as *mut Room) };
440 let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() };
441 let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
442 let track = RemoteAudioTrack::new(track, track_id, publisher_id);
443 if let Some(room) = room.upgrade() {
444 room.did_subscribe_to_remote_audio_track(track);
445 }
446 let _ = Weak::into_raw(room);
447 }
448
449 extern "C" fn on_did_unsubscribe_from_remote_audio_track(
450 room: *mut c_void,
451 publisher_id: CFStringRef,
452 track_id: CFStringRef,
453 ) {
454 let room = unsafe { Weak::from_raw(room as *mut Room) };
455 let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() };
456 let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
457 if let Some(room) = room.upgrade() {
458 room.did_unsubscribe_from_remote_audio_track(publisher_id, track_id);
459 }
460 let _ = Weak::into_raw(room);
461 }
462
463 extern "C" fn on_did_subscribe_to_remote_video_track(
464 room: *mut c_void,
465 publisher_id: CFStringRef,
466 track_id: CFStringRef,
467 track: *const c_void,
468 ) {
469 let room = unsafe { Weak::from_raw(room as *mut Room) };
470 let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() };
471 let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
472 let track = RemoteVideoTrack::new(track, track_id, publisher_id);
473 if let Some(room) = room.upgrade() {
474 room.did_subscribe_to_remote_video_track(track);
475 }
476 let _ = Weak::into_raw(room);
477 }
478
479 extern "C" fn on_did_unsubscribe_from_remote_video_track(
480 room: *mut c_void,
481 publisher_id: CFStringRef,
482 track_id: CFStringRef,
483 ) {
484 let room = unsafe { Weak::from_raw(room as *mut Room) };
485 let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() };
486 let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
487 if let Some(room) = room.upgrade() {
488 room.did_unsubscribe_from_remote_video_track(publisher_id, track_id);
489 }
490 let _ = Weak::into_raw(room);
491 }
492}
493
494impl Drop for RoomDelegate {
495 fn drop(&mut self) {
496 unsafe {
497 CFRelease(self.native_delegate);
498 let _ = Weak::from_raw(self.weak_room);
499 }
500 }
501}
502
503pub struct LocalAudioTrack(*const c_void);
504
505impl LocalAudioTrack {
506 pub fn create() -> Self {
507 Self(unsafe { LKLocalAudioTrackCreateTrack() })
508 }
509}
510
511impl Drop for LocalAudioTrack {
512 fn drop(&mut self) {
513 unsafe { CFRelease(self.0) }
514 }
515}
516
517pub struct LocalVideoTrack(*const c_void);
518
519impl LocalVideoTrack {
520 pub fn screen_share_for_display(display: &MacOSDisplay) -> Self {
521 Self(unsafe { LKCreateScreenShareTrackForDisplay(display.0) })
522 }
523}
524
525impl Drop for LocalVideoTrack {
526 fn drop(&mut self) {
527 unsafe { CFRelease(self.0) }
528 }
529}
530
531pub struct LocalTrackPublication(*const c_void);
532
533impl LocalTrackPublication {
534 pub fn mute(&self) -> impl Future<Output = Result<()>> {
535 let (tx, rx) = futures::channel::oneshot::channel();
536
537 extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) {
538 let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender<Result<()>>) };
539 if error.is_null() {
540 tx.send(Ok(())).ok();
541 } else {
542 let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
543 tx.send(Err(anyhow!(error))).ok();
544 }
545 }
546
547 unsafe {
548 LKLocalTrackPublicationMute(
549 self.0,
550 complete_callback,
551 Box::into_raw(Box::new(tx)) as *mut c_void,
552 )
553 }
554
555 async move { rx.await.unwrap() }
556 }
557
558 pub fn unmute(&self) -> impl Future<Output = Result<()>> {
559 let (tx, rx) = futures::channel::oneshot::channel();
560
561 extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) {
562 let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender<Result<()>>) };
563 if error.is_null() {
564 tx.send(Ok(())).ok();
565 } else {
566 let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
567 tx.send(Err(anyhow!(error))).ok();
568 }
569 }
570
571 unsafe {
572 LKLocalTrackPublicationUnmute(
573 self.0,
574 complete_callback,
575 Box::into_raw(Box::new(tx)) as *mut c_void,
576 )
577 }
578
579 async move { rx.await.unwrap() }
580 }
581}
582
583impl Drop for LocalTrackPublication {
584 fn drop(&mut self) {
585 unsafe { CFRelease(self.0) }
586 }
587}
588
589#[derive(Debug)]
590pub struct RemoteAudioTrack {
591 _native_track: *const c_void,
592 sid: Sid,
593 publisher_id: String,
594}
595
596impl RemoteAudioTrack {
597 fn new(native_track: *const c_void, sid: Sid, publisher_id: String) -> Self {
598 unsafe {
599 CFRetain(native_track);
600 }
601 Self {
602 _native_track: native_track,
603 sid,
604 publisher_id,
605 }
606 }
607
608 pub fn sid(&self) -> &str {
609 &self.sid
610 }
611
612 pub fn publisher_id(&self) -> &str {
613 &self.publisher_id
614 }
615}
616
617#[derive(Debug)]
618pub struct RemoteVideoTrack {
619 native_track: *const c_void,
620 sid: Sid,
621 publisher_id: String,
622}
623
624impl RemoteVideoTrack {
625 fn new(native_track: *const c_void, sid: Sid, publisher_id: String) -> Self {
626 unsafe {
627 CFRetain(native_track);
628 }
629 Self {
630 native_track,
631 sid,
632 publisher_id,
633 }
634 }
635
636 pub fn sid(&self) -> &str {
637 &self.sid
638 }
639
640 pub fn publisher_id(&self) -> &str {
641 &self.publisher_id
642 }
643
644 pub fn frames(&self) -> async_broadcast::Receiver<Frame> {
645 extern "C" fn on_frame(callback_data: *mut c_void, frame: CVImageBufferRef) -> bool {
646 unsafe {
647 let tx = Box::from_raw(callback_data as *mut async_broadcast::Sender<Frame>);
648 let buffer = CVImageBuffer::wrap_under_get_rule(frame);
649 let result = tx.try_broadcast(Frame(buffer));
650 let _ = Box::into_raw(tx);
651 match result {
652 Ok(_) => true,
653 Err(async_broadcast::TrySendError::Closed(_))
654 | Err(async_broadcast::TrySendError::Inactive(_)) => {
655 log::warn!("no active receiver for frame");
656 false
657 }
658 Err(async_broadcast::TrySendError::Full(_)) => {
659 log::warn!("skipping frame as receiver is not keeping up");
660 true
661 }
662 }
663 }
664 }
665
666 extern "C" fn on_drop(callback_data: *mut c_void) {
667 unsafe {
668 let _ = Box::from_raw(callback_data as *mut async_broadcast::Sender<Frame>);
669 }
670 }
671
672 let (tx, rx) = async_broadcast::broadcast(64);
673 unsafe {
674 let renderer = LKVideoRendererCreate(
675 Box::into_raw(Box::new(tx)) as *mut c_void,
676 on_frame,
677 on_drop,
678 );
679 LKVideoTrackAddRenderer(self.native_track, renderer);
680 rx
681 }
682 }
683}
684
685impl Drop for RemoteVideoTrack {
686 fn drop(&mut self) {
687 unsafe { CFRelease(self.native_track) }
688 }
689}
690
691pub enum RemoteVideoTrackUpdate {
692 Subscribed(Arc<RemoteVideoTrack>),
693 Unsubscribed { publisher_id: Sid, track_id: Sid },
694}
695
696pub enum RemoteAudioTrackUpdate {
697 Subscribed(Arc<RemoteAudioTrack>),
698 Unsubscribed { publisher_id: Sid, track_id: Sid },
699}
700
701pub struct MacOSDisplay(*const c_void);
702
703impl MacOSDisplay {
704 fn new(ptr: *const c_void) -> Self {
705 unsafe {
706 CFRetain(ptr);
707 }
708 Self(ptr)
709 }
710}
711
712impl Drop for MacOSDisplay {
713 fn drop(&mut self) {
714 unsafe { CFRelease(self.0) }
715 }
716}
717
718#[derive(Clone)]
719pub struct Frame(CVImageBuffer);
720
721impl Frame {
722 pub fn width(&self) -> usize {
723 self.0.width()
724 }
725
726 pub fn height(&self) -> usize {
727 self.0.height()
728 }
729
730 pub fn image(&self) -> CVImageBuffer {
731 self.0.clone()
732 }
733}