FreeRDP
Loading...
Searching...
No Matches
camera_avfoundation.m
1
18#import <AVFoundation/AVFoundation.h>
19#import <VideoToolbox/VideoToolbox.h>
20#include <stdatomic.h>
21#include <winpr/assert.h>
22#include "../camera.h"
23#include "modes.h"
24#include "nv12.h"
25
26#define TAG CHANNELS_TAG("rdpecam-avfoundation.client")
27
28#define CAM_AVFOUNDATION_MEDIA_FORMAT CAM_MEDIA_FORMAT_NV12
29
30/* Prompt on first channel use; AVFoundation withholds camera frames until consent. */
31WINPR_ATTR_NODISCARD
32static BOOL cam_avfoundation_request_access(void)
33{
34 const AVAuthorizationStatus status =
35 [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
36 if (status == AVAuthorizationStatusAuthorized)
37 return TRUE;
38 if (status == AVAuthorizationStatusNotDetermined)
39 {
40 NSString *usage =
41 [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSCameraUsageDescription"];
42 if (![usage isKindOfClass:[NSString class]] || (usage.length == 0))
43 {
44 WLog_ERR(
45 TAG,
46 "Camera access requires NSCameraUsageDescription in the application's Info.plist");
47 return FALSE;
48 }
49 /* Coalesce requests without retaining HAL or channel pointers across the prompt. */
50 static atomic_bool pending;
51 bool expected = false;
52 if (atomic_compare_exchange_strong(&pending, &expected, true))
53 {
54 WLog_INFO(TAG, "Requesting camera access");
55 [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo
56 completionHandler:^(BOOL granted) {
57 if (!granted)
58 WLog_WARN(TAG, "Camera access denied by user");
59 atomic_store(&pending, false);
60 }];
61 }
62 return TRUE;
63 }
64 const char *reason = "unknown";
65 switch (status)
66 {
67 case AVAuthorizationStatusNotDetermined:
68 reason = "not determined";
69 break;
70 case AVAuthorizationStatusRestricted:
71 reason = "restricted";
72 break;
73 case AVAuthorizationStatusDenied:
74 reason = "denied";
75 break;
76 case AVAuthorizationStatusAuthorized:
77 break;
78 }
79 WLog_WARN(TAG, "Camera access is %s", reason);
80 return FALSE;
81}
82
83#if TARGET_OS_OSX && (__MAC_OS_X_VERSION_MIN_REQUIRED < 140000)
84/* Describe the legacy API's lifetime so it is only used on systems before macOS 14. */
85WINPR_ATTR_NODISCARD
86static AVCaptureDeviceType cam_avfoundation_legacy_external_type(void)
87 API_DEPRECATED_WITH_REPLACEMENT("AVCaptureDeviceTypeExternal", macos(10.15, 14.0));
88
89static AVCaptureDeviceType cam_avfoundation_legacy_external_type(void)
90{
91 return AVCaptureDeviceTypeExternalUnknown;
92}
93#endif
94
95WINPR_ATTR_NODISCARD
96static BOOL cam_avfoundation_configure_camera(AVCaptureDevice *camera,
97 AVCaptureDeviceFormat *format, CMTime duration)
98{
99 NSError *error = nil;
100 if (![camera lockForConfiguration:&error])
101 {
102 WLog_ERR(TAG, "Unable to configure camera: %s", error.localizedDescription.UTF8String);
103 return FALSE;
104 }
105 camera.activeFormat = format;
106 camera.activeVideoMinFrameDuration = duration;
107 camera.activeVideoMaxFrameDuration = duration;
108 [camera unlockForConfiguration];
109 return TRUE;
110}
111
112@interface FreeRDPCameraAVFoundationStream : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
113{
114 @public
115 CameraDevice *_device;
116 dispatch_queue_t _sampleQueue;
117 dispatch_queue_t _controlQueue;
118 size_t _streamIndex;
119 ICamHalSampleCapturedCallback _callback;
120 CFTypeRef _transfer; /* Pixel transfer is available on iOS 16 and later. */
121 CVPixelBufferRef _rawImage;
122 wStream *_rawSample;
123 UINT32 _width;
124 UINT32 _height;
125 BOOL _captureLogged;
126 BOOL _sampleLogged;
127 atomic_bool _drained;
128}
129@property(nonatomic, retain) AVCaptureSession *session;
130@property(nonatomic, retain) AVCaptureVideoDataOutput *output;
131- (void)stop;
132@end
133
134static void cam_avfoundation_deliver(FreeRDPCameraAVFoundationStream *stream, const BYTE *data,
135 size_t length)
136{
137 WINPR_ASSERT(stream);
138 WINPR_ASSERT(data);
139 if (!stream->_sampleLogged)
140 {
141 WLog_INFO(TAG, "First camera sample: format %u, %" PRIu32 "x%" PRIu32 ", %zu bytes",
142 CAM_AVFOUNDATION_MEDIA_FORMAT, stream->_width, stream->_height, length);
143 stream->_sampleLogged = TRUE;
144 }
145 const UINT rc = stream->_callback(stream->_device, stream->_streamIndex, data, length);
146 if (rc != CHANNEL_RC_OK)
147 {
148 atomic_store(&stream->_drained, true);
149 if (rc != ECAM_SAMPLE_CAPTURE_DRAINED)
150 WLog_WARN(TAG, "Sample callback failed: %" PRIu32, rc);
151 }
152}
153
154WINPR_ATTR_NODISCARD
155static CAM_ERROR_CODE cam_avfoundation_stream_start(FreeRDPCameraAVFoundationStream *stream,
156 AVCaptureDevice *camera,
157 const CAM_MEDIA_TYPE_DESCRIPTION *mediaType,
158 AVCaptureDeviceFormat *selected,
159 CMTime duration)
160{
161 WINPR_ASSERT(stream);
162 WINPR_ASSERT(camera);
163 WINPR_ASSERT(mediaType);
164 WINPR_ASSERT(selected);
165 /* Suppress startup samples until the cached native mode is applied. The protocol
166 * stream remains active during an idle capture restart. */
167 atomic_init(&stream->_drained, true);
168 stream->_width = mediaType->Width;
169 stream->_height = mediaType->Height;
170 WLog_INFO(TAG,
171 "Starting camera %s: format %u, %" PRIu32 "x%" PRIu32 ", %" PRIu32 "/%" PRIu32 " fps",
172 camera.localizedName.UTF8String, CAM_AVFOUNDATION_MEDIA_FORMAT, stream->_width,
173 stream->_height, mediaType->FrameRateNumerator, mediaType->FrameRateDenominator);
174 stream->_sampleQueue =
175 dispatch_queue_create("org.freerdp.camera.samples", DISPATCH_QUEUE_SERIAL);
176 stream->_controlQueue =
177 dispatch_queue_create("org.freerdp.camera.control", DISPATCH_QUEUE_SERIAL);
178 stream.session = [[[AVCaptureSession alloc] init] autorelease];
179 stream.output = [[[AVCaptureVideoDataOutput alloc] init] autorelease];
180 if (!stream->_sampleQueue || !stream->_controlQueue || !stream.session || !stream.output)
181 return CAM_ERROR_CODE_OutOfMemory;
182
183 NSError *error = nil;
184 AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:camera error:&error];
185 if (!input)
186 {
187 WLog_ERR(TAG, "Unable to open camera: %s", error.localizedDescription.UTF8String);
188 return CAM_ERROR_CODE_UnexpectedError;
189 }
190 [stream.session beginConfiguration];
191#if !TARGET_OS_OSX
192 stream.session.sessionPreset = AVCaptureSessionPresetInputPriority;
193#endif
194 if (![stream.session canAddInput:input])
195 {
196 [stream.session commitConfiguration];
197 return CAM_ERROR_CODE_OperationNotSupported;
198 }
199 [stream.session addInput:input];
200 if (![stream.session canAddOutput:stream.output])
201 {
202 [stream.session commitConfiguration];
203 return CAM_ERROR_CODE_OperationNotSupported;
204 }
205 [stream.session addOutput:stream.output];
206 const OSType pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
207 if (![stream.output.availableVideoCVPixelFormatTypes containsObject:@(pixelFormat)])
208 {
209 [stream.session commitConfiguration];
210 return CAM_ERROR_CODE_InvalidMediaType;
211 }
212 stream.output.videoSettings = @{(NSString *)kCVPixelBufferPixelFormatTypeKey: @(pixelFormat)};
213 stream.output.alwaysDiscardsLateVideoFrames = YES;
214#if !TARGET_OS_OSX
215 if (!cam_avfoundation_configure_camera(camera, selected, duration))
216 {
217 [stream.session commitConfiguration];
218 return CAM_ERROR_CODE_UnexpectedError;
219 }
220#endif
221 [stream.session commitConfiguration];
222
223#if (TARGET_OS_OSX && (__MAC_OS_X_VERSION_MAX_ALLOWED >= 140000)) || \
224 (!TARGET_OS_OSX && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 170000))
225 if (@available(macOS 14.0, iOS 17.0, *))
226 {
227 AVCaptureConnection *connection = [stream.output connectionWithMediaType:AVMediaTypeVideo];
228 if (!connection)
229 return CAM_ERROR_CODE_OperationNotSupported;
230 AVCaptureDeviceRotationCoordinator *coordinator =
231 [[AVCaptureDeviceRotationCoordinator alloc] initWithDevice:camera previewLayer:nil];
232 if (!coordinator)
233 return CAM_ERROR_CODE_OutOfMemory;
234 const CGFloat angle = coordinator.videoRotationAngleForHorizonLevelCapture;
235 [coordinator release];
236 /* Fix rotation for this capture session before starting the render pipeline.
237 * Use the absolute capture angle, including on cameras defaulting to 180 degrees. */
238 if ([connection isVideoRotationAngleSupported:angle])
239 {
240 connection.videoRotationAngle = angle;
241 WLog_INFO(TAG, "Camera capture rotation: %.0f degrees", angle);
242 }
243 else
244 WLog_WARN(TAG, "Camera does not support rotation by %.0f degrees", angle);
245 }
246#endif
247
248 stream->_rawSample = Stream_New(NULL, (size_t)stream->_width * stream->_height * 3 / 2);
249 if (!stream->_rawSample)
250 return CAM_ERROR_CODE_OutOfMemory;
251 OSStatus status = CVPixelBufferCreate(NULL, stream->_width, stream->_height, pixelFormat, NULL,
252 &stream->_rawImage);
253 if (status == noErr)
254 {
255 if (@available(iOS 16.0, *))
256 {
257 status = cam_avfoundation_pixel_transfer_create(&stream->_transfer);
258 }
259 }
260 if (status != noErr)
261 {
262 WLog_ERR(TAG, "Unable to create NV12 output buffer: %d", (int)status);
263 return CAM_ERROR_CODE_UnexpectedError;
264 }
265 [stream.output setSampleBufferDelegate:stream queue:stream->_sampleQueue];
266 /* startRunning/stopRunning are blocking and must stay off the application's main queue. */
267 dispatch_sync(stream->_controlQueue, ^{
268 @autoreleasepool
269 {
270 [stream.session startRunning];
271 }
272 });
273#if TARGET_OS_OSX
274 /* macOS has no InputPriority preset and startRunning replaces the device's
275 * format/rate with the session preset. Apply the cached native selection after
276 * startup, before enabling sample delivery. */
277 if (stream.session.running && !cam_avfoundation_configure_camera(camera, selected, duration))
278 return CAM_ERROR_CODE_UnexpectedError;
279#endif
280 if (!stream.session.running)
281 return CAM_ERROR_CODE_UnexpectedError;
282 atomic_store(&stream->_drained, false);
283 return CAM_ERROR_CODE_None;
284}
285
286@implementation FreeRDPCameraAVFoundationStream
287- (void)captureOutput:(AVCaptureOutput *)output
288 didOutputSampleBuffer:(CMSampleBufferRef)sample
289 fromConnection:(AVCaptureConnection *)connection
290{
291 @autoreleasepool
292 {
293 WINPR_ASSERT(output);
294 WINPR_ASSERT(sample);
295 WINPR_ASSERT(connection);
296 /* StartStream returns before RDPECAM marks the stream active. */
297 if (atomic_load(&_drained) || !_device->streams[_streamIndex].streaming)
298 return;
299 CVImageBufferRef image = CMSampleBufferGetImageBuffer(sample);
300 if (!image)
301 return;
302 if (!_captureLogged)
303 {
304 WLog_INFO(TAG, "Camera capture input: %zux%zu, pixel format 0x%08" PRIx32,
305 CVPixelBufferGetWidth(image), CVPixelBufferGetHeight(image),
306 CVPixelBufferGetPixelFormatType(image));
307 _captureLogged = TRUE;
308 }
309 CVPixelBufferRef raw = image;
310 if ((CVPixelBufferGetWidth(image) != _width) || (CVPixelBufferGetHeight(image) != _height))
311 {
312 OSStatus status = kVTParameterErr;
313 if (@available(iOS 16.0, *))
314 {
315 if (_transfer)
316 status = VTPixelTransferSessionTransferImage(
317 (VTPixelTransferSessionRef)_transfer, image, _rawImage);
318 }
319 if (status != noErr)
320 {
321 WLog_WARN(TAG, "Unable to resize NV12 camera sample: %d", (int)status);
322 return;
323 }
324 raw = _rawImage;
325 }
326 if (!cam_avfoundation_pixel_buffer_to_nv12(raw, _rawSample))
327 {
328 WLog_WARN(TAG, "Unable to pack native NV12 camera sample");
329 return;
330 }
331 cam_avfoundation_deliver(self, Stream_Buffer(_rawSample), Stream_Length(_rawSample));
332 if (atomic_load(&_drained))
333 {
334 AVCaptureSession *session = self.session;
335 dispatch_async(_controlQueue, ^{
336 @autoreleasepool
337 {
338 [session stopRunning];
339 }
340 });
341 }
342 }
343}
344
345- (void)stop
346{
347 [self.output setSampleBufferDelegate:nil queue:NULL];
348 AVCaptureSession *session = self.session;
349 if (_controlQueue)
350 dispatch_sync(_controlQueue, ^{
351 @autoreleasepool
352 {
353 [session stopRunning];
354 }
355 });
356 /* Drain the delegate before freeing buffers; do not capture self during deallocation. */
357 if (_sampleQueue)
358 dispatch_sync(_sampleQueue, ^{
359 });
360 if (_transfer)
361 {
362 if (@available(iOS 16.0, *))
363 VTPixelTransferSessionInvalidate((VTPixelTransferSessionRef)_transfer);
364 CFRelease(_transfer);
365 _transfer = NULL;
366 }
367 if (_rawImage)
368 {
369 CFRelease(_rawImage);
370 _rawImage = NULL;
371 }
372 Stream_Free(_rawSample, TRUE);
373 _rawSample = NULL;
374 _device = NULL;
375}
376
377- (void)dealloc
378{
379 [self stop];
380 [_session release];
381 [_output release];
382 if (_sampleQueue)
383 dispatch_release(_sampleQueue);
384 if (_controlQueue)
385 dispatch_release(_controlQueue);
386 [super dealloc];
387}
388@end
389
390@interface FreeRDPCameraAVFoundation : NSObject
391@property(nonatomic, retain) NSMutableDictionary<NSString *, AVCaptureDevice *> *cameras;
392@property(nonatomic, retain)
393 NSMutableDictionary<NSString *, FreeRDPCameraAVFoundationStream *> *streams;
394@property(nonatomic, retain)
395 NSMutableDictionary<NSString *, FreeRDPCameraAVFoundationModes *> *modes;
396@end
397@implementation FreeRDPCameraAVFoundation
398- (void)dealloc
399{
400 [_cameras release];
401 [_streams release];
402 [_modes release];
403 [super dealloc];
404}
405@end
406
407typedef struct
408{
409 ICamHal iface;
410 void *context; /* Owned by the HAL until Free. */
411} CamAVFoundationHal;
412
413WINPR_ATTR_NODISCARD
414static FreeRDPCameraAVFoundation *cam_avfoundation_context(ICamHal *hal)
415{
416 WINPR_ASSERT(hal);
417 return (FreeRDPCameraAVFoundation *)((CamAVFoundationHal *)hal)->context;
418}
419
420WINPR_ATTR_NODISCARD
421static UINT cam_avfoundation_enumerate(ICamHal *hal, ICamHalEnumCallback callback,
423{
424 WINPR_ASSERT(hal);
425 WINPR_ASSERT(callback);
426 WINPR_ASSERT(ecam);
427 WINPR_ASSERT(channel);
428 @autoreleasepool
429 {
430 /* Request permission when the server first enumerates cameras. */
431 if (!cam_avfoundation_request_access())
432 return 0;
433 NSMutableArray<AVCaptureDeviceType> *types =
434 [NSMutableArray arrayWithObject:AVCaptureDeviceTypeBuiltInWideAngleCamera];
435#if TARGET_OS_OSX
436#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 140000
437 [types addObject:AVCaptureDeviceTypeExternal];
438#else
439#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 140000
440 if (@available(macOS 14.0, *))
441 [types addObject:AVCaptureDeviceTypeExternal];
442 else
443#endif
444 [types addObject:cam_avfoundation_legacy_external_type()];
445#endif
446#else
447#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 170000
448 if (@available(iOS 17.0, *))
449 [types addObject:AVCaptureDeviceTypeExternal];
450#endif
451#endif
452 AVCaptureDeviceDiscoverySession *discovery = [AVCaptureDeviceDiscoverySession
453 discoverySessionWithDeviceTypes:types
454 mediaType:AVMediaTypeVideo
455 position:AVCaptureDevicePositionUnspecified];
456 FreeRDPCameraAVFoundation *context = cam_avfoundation_context(hal);
457 UINT count = 0;
458 for (AVCaptureDevice *camera in discovery.devices)
459 {
460 NSString *deviceId = nil;
461 @synchronized(context)
462 {
463 for (NSString *key in context.cameras)
464 {
465 if ([context.cameras[key].uniqueID isEqualToString:camera.uniqueID])
466 {
467 deviceId = key;
468 break;
469 }
470 }
471 /* Apple's uniqueID can exceed the channel's 32-byte device ID buffer. */
472 if (!deviceId)
473 deviceId = [NSString
474 stringWithFormat:@"avfoundation-%lu", (unsigned long)context.cameras.count];
475 context.cameras[deviceId] = camera;
476 /* A new device enumeration refreshes the native mode snapshot. */
477 [context.modes removeObjectForKey:deviceId];
478 }
479 if (callback(ecam, channel, deviceId.UTF8String, camera.localizedName.UTF8String) !=
480 CHANNEL_RC_OK)
481 break;
482 count++;
483 }
484 return count;
485 }
486}
487
488WINPR_ATTR_NODISCARD
489static BOOL cam_avfoundation_activate(ICamHal *hal, const char *deviceId, CAM_ERROR_CODE *error)
490{
491 WINPR_ASSERT(hal);
492 WINPR_ASSERT(deviceId);
493 WINPR_ASSERT(error);
494 @autoreleasepool
495 {
496 const BOOL found = cam_avfoundation_context(hal).cameras[@(deviceId)] != nil;
497 *error = found ? CAM_ERROR_CODE_None : CAM_ERROR_CODE_ItemNotFound;
498 return found;
499 }
500}
501
502WINPR_ATTR_NODISCARD
503static INT16 cam_avfoundation_media_types(ICamHal *hal, const char *deviceId, size_t streamIndex,
504 const CAM_MEDIA_FORMAT_INFO *formats, size_t formatCount,
505 CAM_MEDIA_TYPE_DESCRIPTION *mediaTypes,
506 size_t *mediaCount)
507{
508 WINPR_ASSERT(hal);
509 WINPR_ASSERT(deviceId);
510 WINPR_ASSERT(formats);
511 WINPR_ASSERT(mediaTypes);
512 WINPR_ASSERT(mediaCount);
513 @autoreleasepool
514 {
515 const size_t capacity = *mediaCount;
516 *mediaCount = 0;
517 if (streamIndex != 0)
518 return -1;
519 INT16 selected = -1;
520 /* Follow the shared format preference: encode NV12 as H.264 when offered,
521 * otherwise use NV12 passthrough. Encoder selection stays in the shared codec. */
522 for (size_t i = 0; i < formatCount; i++)
523 {
524 if ((formats[i].inputFormat == CAM_AVFOUNDATION_MEDIA_FORMAT) &&
525 ((formats[i].outputFormat == CAM_MEDIA_FORMAT_H264) ||
526 (formats[i].outputFormat == CAM_AVFOUNDATION_MEDIA_FORMAT)))
527 {
528 selected = (INT16)i;
529 break;
530 }
531 }
532 if (selected < 0)
533 return -1;
534 FreeRDPCameraAVFoundation *context = cam_avfoundation_context(hal);
536 NSString *key = @(deviceId);
537 @synchronized(context)
538 {
539 modes = context.modes[key];
540 AVCaptureDevice *camera = context.cameras[key];
541 if (!modes && camera)
542 {
543 modes = [[[FreeRDPCameraAVFoundationModes alloc] initWithFormats:camera.formats]
544 autorelease];
545 if (modes)
546 context.modes[key] = modes;
547 }
548 /* Discovery may replace this snapshot after we release the lock. */
549 modes = [[modes retain] autorelease];
550 }
551 if (!modes)
552 return -1;
553 *mediaCount = [modes copyMediaTypes:mediaTypes capacity:capacity];
554 return selected;
555 }
556}
557
558WINPR_ATTR_NODISCARD
559static CAM_ERROR_CODE cam_avfoundation_stop(ICamHal *hal, const char *deviceId, size_t streamIndex)
560{
561 WINPR_ASSERT(hal);
562 WINPR_ASSERT(deviceId);
563 @autoreleasepool
564 {
565 if (streamIndex != 0)
566 return CAM_ERROR_CODE_InvalidStreamNumber;
567 FreeRDPCameraAVFoundation *context = cam_avfoundation_context(hal);
568 NSString *key = @(deviceId);
569 [context.streams[key] stop];
570 [context.streams removeObjectForKey:key];
571 return CAM_ERROR_CODE_None;
572 }
573}
574
575WINPR_ATTR_NODISCARD
576static BOOL cam_avfoundation_deactivate(ICamHal *hal, const char *deviceId, CAM_ERROR_CODE *error)
577{
578 WINPR_ASSERT(hal);
579 WINPR_ASSERT(deviceId);
580 WINPR_ASSERT(error);
581 *error = cam_avfoundation_stop(hal, deviceId, 0);
582 return *error == CAM_ERROR_CODE_None;
583}
584
585WINPR_ATTR_NODISCARD
586static CAM_ERROR_CODE cam_avfoundation_start(ICamHal *hal, CameraDevice *device, size_t streamIndex,
587 const CAM_MEDIA_TYPE_DESCRIPTION *mediaType,
588 ICamHalSampleCapturedCallback callback)
589{
590 WINPR_ASSERT(hal);
591 WINPR_ASSERT(device);
592 WINPR_ASSERT(mediaType);
593 WINPR_ASSERT(callback);
594 @autoreleasepool
595 {
596 if (streamIndex != 0)
597 return CAM_ERROR_CODE_InvalidStreamNumber;
598 if ((mediaType->Format != CAM_AVFOUNDATION_MEDIA_FORMAT) ||
599 !mediaType->FrameRateNumerator || !mediaType->FrameRateDenominator)
600 return CAM_ERROR_CODE_InvalidMediaType;
601 if (!cam_avfoundation_request_access())
602 return CAM_ERROR_CODE_OperationNotSupported;
603 FreeRDPCameraAVFoundation *context = cam_avfoundation_context(hal);
604 NSString *key = @(device->deviceId);
605 AVCaptureDevice *camera = nil;
607 /* Hold the matching camera/snapshot through startup and the surrounding HAL pool. */
608 @synchronized(context)
609 {
610 camera = [[context.cameras[key] retain] autorelease];
611 modes = [[context.modes[key] retain] autorelease];
612 }
613 if (!camera || !camera.connected)
614 return CAM_ERROR_CODE_ItemNotFound;
615 CMTime duration = kCMTimeInvalid;
616 AVCaptureDeviceFormat *selected = [modes formatForMediaType:mediaType
617 frameDuration:&duration];
618 if (!selected)
619 return CAM_ERROR_CODE_InvalidMediaType;
620 /* Restart after request-driven idle release; drain the old delegate before replacing it. */
621 const CAM_ERROR_CODE stopError = cam_avfoundation_stop(hal, device->deviceId, streamIndex);
622 if (stopError != CAM_ERROR_CODE_None)
623 return stopError;
624 FreeRDPCameraAVFoundationStream *stream = [[FreeRDPCameraAVFoundationStream alloc] init];
625 if (!stream)
626 return CAM_ERROR_CODE_OutOfMemory;
627 stream->_device = device;
628 stream->_streamIndex = streamIndex;
629 stream->_callback = callback;
630 const CAM_ERROR_CODE error =
631 cam_avfoundation_stream_start(stream, camera, mediaType, selected, duration);
632 if (error != CAM_ERROR_CODE_None)
633 {
634 [stream release];
635 return error;
636 }
637 context.streams[key] = stream;
638 [stream release];
639 return CAM_ERROR_CODE_None;
640 }
641}
642
643WINPR_ATTR_NODISCARD
644static CAM_ERROR_CODE cam_avfoundation_free(ICamHal *hal)
645{
646 WINPR_ASSERT(hal);
647 @autoreleasepool
648 {
649 CamAVFoundationHal *av = (CamAVFoundationHal *)hal;
650 FreeRDPCameraAVFoundation *context = (FreeRDPCameraAVFoundation *)av->context;
651 for (FreeRDPCameraAVFoundationStream *stream in context.streams.allValues)
652 [stream stop];
653 av->context = NULL;
654 [context release];
655 free(av);
656 return CAM_ERROR_CODE_None;
657 }
658}
659
660WINPR_ATTR_NODISCARD
661FREERDP_ENTRY_POINT(UINT VCAPITYPE avfoundation_freerdp_rdpecam_client_subsystem_entry(
663{
664 @autoreleasepool
665 {
666 WINPR_ASSERT(entryPoints);
667 WINPR_ASSERT(entryPoints->plugin);
668 WINPR_ASSERT(entryPoints->pRegisterCameraHal);
669 CamAVFoundationHal *hal = calloc(1, sizeof(CamAVFoundationHal));
670 if (!hal)
671 return CHANNEL_RC_NO_MEMORY;
672 FreeRDPCameraAVFoundation *context = [[FreeRDPCameraAVFoundation alloc] init];
673 if (!context)
674 {
675 free(hal);
676 return CHANNEL_RC_NO_MEMORY;
677 }
678 context.cameras = [NSMutableDictionary dictionary];
679 context.streams = [NSMutableDictionary dictionary];
680 context.modes = [NSMutableDictionary dictionary];
681 hal->context = context;
682 hal->iface.RequestDrivenCapture = TRUE;
683 hal->iface.Enumerate = cam_avfoundation_enumerate;
684 hal->iface.Activate = cam_avfoundation_activate;
685 hal->iface.Deactivate = cam_avfoundation_deactivate;
686 hal->iface.GetMediaTypeDescriptions = cam_avfoundation_media_types;
687 hal->iface.StartStream = cam_avfoundation_start;
688 hal->iface.StopStream = cam_avfoundation_stop;
689 hal->iface.Free = cam_avfoundation_free;
690 const UINT rc = entryPoints->pRegisterCameraHal(entryPoints->plugin, &hal->iface);
691 if (rc != CHANNEL_RC_OK)
692 {
693 if (cam_avfoundation_free(&hal->iface) != CAM_ERROR_CODE_None)
694 WLog_ERR(TAG, "Unable to release unregistered camera HAL");
695 }
696 return rc;
697 }
698}
Definition camera.h:237