Atlas - SDL_camera_coremedia.m
Home / ext / SDL / src / camera / coremedia Lines: 2 | Size: 27704 bytes [Download] [Show on GitHub] [Search similar files] [Raw] [Raw (proxy)][FILE BEGIN]1/* 2 Simple DirectMedia Layer 3 Copyright (C) 1997-2026 Sam Lantinga <[email protected]> 4 5 This software is provided 'as-is', without any express or implied 6 warranty. In no event will the authors be held liable for any damages 7 arising from the use of this software. 8 9 Permission is granted to anyone to use this software for any purpose, 10 including commercial applications, and to alter it and redistribute it 11 freely, subject to the following restrictions: 12 13 1. The origin of this software must not be misrepresented; you must not 14 claim that you wrote the original software. If you use this software 15 in a product, an acknowledgment in the product documentation would be 16 appreciated but is not required. 17 2. Altered source versions must be plainly marked as such, and must not be 18 misrepresented as being the original software. 19 3. This notice may not be removed or altered from any source distribution. 20*/ 21#include "SDL_internal.h" 22 23#ifdef SDL_CAMERA_DRIVER_COREMEDIA 24 25#include "../SDL_syscamera.h" 26#include "../SDL_camera_c.h" 27#include "../../thread/SDL_systhread.h" 28 29#import <AVFoundation/AVFoundation.h> 30#import <CoreMedia/CoreMedia.h> 31 32#if defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_TVOS) 33#define USE_UIKIT_DEVICE_ROTATION 34#endif 35 36#ifdef USE_UIKIT_DEVICE_ROTATION 37#import <UIKit/UIKit.h> 38#endif 39 40/* 41 * Need to link with:: CoreMedia CoreVideo 42 * 43 * Add in pInfo.list: 44 * <key>NSCameraUsageDescription</key> <string>Access camera</string> 45 * 46 * 47 * MACOSX: 48 * Add to the Code Sign Entitlement file: 49 * <key>com.apple.security.device.camera</key> <true/> 50 */ 51 52static void CoreMediaFormatToSDL(FourCharCode fmt, SDL_PixelFormat *pixel_format, SDL_Colorspace *colorspace) 53{ 54 switch (fmt) { 55 #define CASE(x, y, z) case x: *pixel_format = y; *colorspace = z; return 56 // the 16LE ones should use 16BE if we're on a Bigendian system like PowerPC, 57 // but at current time there is no bigendian Apple platform that has CoreMedia. 58 CASE(kCMPixelFormat_16LE555, SDL_PIXELFORMAT_XRGB1555, SDL_COLORSPACE_SRGB); 59 CASE(kCMPixelFormat_16LE5551, SDL_PIXELFORMAT_RGBA5551, SDL_COLORSPACE_SRGB); 60 CASE(kCMPixelFormat_16LE565, SDL_PIXELFORMAT_RGB565, SDL_COLORSPACE_SRGB); 61 CASE(kCMPixelFormat_24RGB, SDL_PIXELFORMAT_RGB24, SDL_COLORSPACE_SRGB); 62 CASE(kCMPixelFormat_32ARGB, SDL_PIXELFORMAT_ARGB32, SDL_COLORSPACE_SRGB); 63 CASE(kCMPixelFormat_32BGRA, SDL_PIXELFORMAT_BGRA32, SDL_COLORSPACE_SRGB); 64 CASE(kCMPixelFormat_422YpCbCr8, SDL_PIXELFORMAT_UYVY, SDL_COLORSPACE_BT709_LIMITED); 65 CASE(kCMPixelFormat_422YpCbCr8_yuvs, SDL_PIXELFORMAT_YUY2, SDL_COLORSPACE_BT709_LIMITED); 66 CASE(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, SDL_PIXELFORMAT_NV12, SDL_COLORSPACE_BT709_LIMITED); 67 CASE(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, SDL_PIXELFORMAT_NV12, SDL_COLORSPACE_BT709_FULL); 68 CASE(kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange, SDL_PIXELFORMAT_P010, SDL_COLORSPACE_BT2020_LIMITED); 69 CASE(kCVPixelFormatType_420YpCbCr10BiPlanarFullRange, SDL_PIXELFORMAT_P010, SDL_COLORSPACE_BT2020_FULL); 70 #undef CASE 71 default: 72 #if DEBUG_CAMERA 73 SDL_Log("CAMERA: Unknown format FourCharCode '%d'", (int) fmt); 74 #endif 75 break; 76 } 77 *pixel_format = SDL_PIXELFORMAT_UNKNOWN; 78 *colorspace = SDL_COLORSPACE_UNKNOWN; 79} 80 81@class SDLCaptureVideoDataOutputSampleBufferDelegate; 82 83// just a simple wrapper to help ARC manage memory... 84@interface SDLPrivateCameraData : NSObject 85@property(nonatomic, retain) AVCaptureSession *session; 86@property(nonatomic, retain) SDLCaptureVideoDataOutputSampleBufferDelegate *delegate; 87@property(nonatomic, assign) CMSampleBufferRef current_sample; 88#ifdef USE_UIKIT_DEVICE_ROTATION 89@property(nonatomic, assign) UIDeviceOrientation last_device_orientation; 90#endif 91@end 92 93@implementation SDLPrivateCameraData 94@end 95 96 97static bool CheckCameraPermissions(SDL_Camera *device) 98{ 99 if (device->permission == SDL_CAMERA_PERMISSION_STATE_PENDING) { // still expecting a permission result. 100 if (@available(macOS 14, *)) { 101 const AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; 102 if (status != AVAuthorizationStatusNotDetermined) { // NotDetermined == still waiting for an answer from the user. 103 SDL_CameraPermissionOutcome(device, (status == AVAuthorizationStatusAuthorized) ? true : false); 104 } 105 } else { 106 SDL_CameraPermissionOutcome(device, true); // always allowed (or just unqueryable...?) on older macOS. 107 } 108 } 109 110 return (device->permission > SDL_CAMERA_PERMISSION_STATE_PENDING); 111} 112 113// this delegate just receives new video frames on a Grand Central Dispatch queue, and fires off the 114// main device thread iterate function directly to consume it. 115@interface SDLCaptureVideoDataOutputSampleBufferDelegate : NSObject<AVCaptureVideoDataOutputSampleBufferDelegate> 116 @property SDL_Camera *device; 117 -(id) init:(SDL_Camera *) dev; 118 -(void) captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection; 119@end 120 121@implementation SDLCaptureVideoDataOutputSampleBufferDelegate 122 123 -(id) init:(SDL_Camera *) dev { 124 if ( self = [super init] ) { 125 _device = dev; 126 } 127 return self; 128 } 129 130 - (void) captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection 131 { 132 SDL_Camera *device = self.device; 133 if (!device || !device->hidden) { 134 return; // oh well. 135 } 136 137 if (!CheckCameraPermissions(device)) { 138 return; // nothing to do right now, dump what is probably a completely black frame. 139 } 140 141 SDLPrivateCameraData *hidden = (__bridge SDLPrivateCameraData *) device->hidden; 142 hidden.current_sample = sampleBuffer; 143 SDL_CameraThreadIterate(device); 144 hidden.current_sample = NULL; 145 } 146 147 - (void)captureOutput:(AVCaptureOutput *)output didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection 148 { 149 #if DEBUG_CAMERA 150 SDL_Log("CAMERA: Drop frame."); 151 #endif 152 } 153@end 154 155static bool COREMEDIA_WaitDevice(SDL_Camera *device) 156{ 157 return true; // this isn't used atm, since we run our own thread out of Grand Central Dispatch. 158} 159 160static SDL_CameraFrameResult COREMEDIA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, float *rotation) 161{ 162 SDL_CameraFrameResult result = SDL_CAMERA_FRAME_READY; 163 SDLPrivateCameraData *hidden = (__bridge SDLPrivateCameraData *) device->hidden; 164 CMSampleBufferRef sample_buffer = hidden.current_sample; 165 hidden.current_sample = NULL; 166 SDL_assert(sample_buffer != NULL); // should only have been called from our delegate with a new frame. 167 168 CMSampleTimingInfo timinginfo; 169 if (CMSampleBufferGetSampleTimingInfo(sample_buffer, 0, &timinginfo) == noErr) { 170 *timestampNS = (Uint64) (CMTimeGetSeconds(timinginfo.presentationTimeStamp) * ((Float64) SDL_NS_PER_SECOND)); 171 } else { 172 SDL_assert(!"this shouldn't happen, I think."); 173 *timestampNS = 0; 174 } 175 176 CVImageBufferRef image = CMSampleBufferGetImageBuffer(sample_buffer); // does not retain `image` (and we don't want it to). 177 const int numPlanes = (int) CVPixelBufferGetPlaneCount(image); 178 const int planar = (int) CVPixelBufferIsPlanar(image); 179 180 #if DEBUG_CAMERA 181 const int w = (int) CVPixelBufferGetWidth(image); 182 const int h = (int) CVPixelBufferGetHeight(image); 183 const int sz = (int) CVPixelBufferGetDataSize(image); 184 const int pitch = (int) CVPixelBufferGetBytesPerRow(image); 185 SDL_Log("CAMERA: buffer planar=%d numPlanes=%d %d x %d sz=%d pitch=%d", planar, numPlanes, w, h, sz, pitch); 186 #endif 187 188 // !!! FIXME: this currently copies the data to the surface (see FIXME about non-contiguous planar surfaces, but in theory we could just keep this locked until ReleaseFrame... 189 CVPixelBufferLockBaseAddress(image, 0); 190 191 frame->w = (int)CVPixelBufferGetWidth(image); 192 frame->h = (int)CVPixelBufferGetHeight(image); 193 194 if ((planar == 0) && (numPlanes == 0)) { 195 const int pitch = (int) CVPixelBufferGetBytesPerRow(image); 196 const size_t buflen = pitch * frame->h; 197 frame->pixels = SDL_aligned_alloc(SDL_GetSIMDAlignment(), buflen); 198 if (frame->pixels == NULL) { 199 result = SDL_CAMERA_FRAME_ERROR; 200 } else { 201 frame->pitch = pitch; 202 SDL_memcpy(frame->pixels, CVPixelBufferGetBaseAddress(image), buflen); 203 } 204 } else { 205 // !!! FIXME: we have an open issue in SDL3 to allow SDL_Surface to support non-contiguous planar data, but we don't have it yet. 206 size_t buflen = 0; 207 for (int i = 0; i < numPlanes; i++) { 208 size_t plane_height = CVPixelBufferGetHeightOfPlane(image, i); 209 size_t plane_pitch = CVPixelBufferGetBytesPerRowOfPlane(image, i); 210 size_t plane_size = (plane_pitch * plane_height); 211 buflen += plane_size; 212 } 213 214 frame->pitch = (int)CVPixelBufferGetBytesPerRowOfPlane(image, 0); // this is what SDL3 currently expects 215 frame->pixels = SDL_aligned_alloc(SDL_GetSIMDAlignment(), buflen); 216 if (frame->pixels == NULL) { 217 result = SDL_CAMERA_FRAME_ERROR; 218 } else { 219 Uint8 *dst = frame->pixels; 220 for (int i = 0; i < numPlanes; i++) { 221 const void *src = CVPixelBufferGetBaseAddressOfPlane(image, i); 222 size_t plane_height = CVPixelBufferGetHeightOfPlane(image, i); 223 size_t plane_pitch = CVPixelBufferGetBytesPerRowOfPlane(image, i); 224 size_t plane_size = (plane_pitch * plane_height); 225 SDL_memcpy(dst, src, plane_size); 226 dst += plane_size; 227 } 228 } 229 } 230 231 CVPixelBufferUnlockBaseAddress(image, 0); 232 233 #ifdef USE_UIKIT_DEVICE_ROTATION 234 UIDeviceOrientation device_orientation = [[UIDevice currentDevice] orientation]; 235 if (!UIDeviceOrientationIsValidInterfaceOrientation(device_orientation)) { 236 device_orientation = hidden.last_device_orientation; // possible the phone is laying flat or something went wrong, just stay with the last known-good orientation. 237 } else { 238 hidden.last_device_orientation = device_orientation; // update the last known-good orientation for later. 239 } 240 241#pragma clang diagnostic push 242#pragma clang diagnostic ignored "-Wdeprecated-declarations" 243 const UIInterfaceOrientation ui_orientation = [UIApplication sharedApplication].statusBarOrientation; 244#pragma clang diagnostic pop 245 246 // there is probably math for this, but this is easy to slap into a table. 247 // rotation = rotations[uiorientation-1][devorientation-1]; 248 if (device->position == SDL_CAMERA_POSITION_BACK_FACING) { 249 static const Uint16 back_rotations[4][4] = { 250 { 90, 90, 90, 90 }, // ui portrait 251 { 270, 270, 270, 270 }, // ui portrait upside down 252 { 0, 0, 0, 0 }, // ui landscape left 253 { 180, 180, 180, 180 } // ui landscape right 254 }; 255 *rotation = (float) back_rotations[ui_orientation - 1][device_orientation - 1]; 256 } else { 257 static const Uint16 front_rotations[4][4] = { 258 { 90, 90, 270, 270 }, // ui portrait 259 { 270, 270, 90, 90 }, // ui portrait upside down 260 { 0, 0, 180, 180 }, // ui landscape left 261 { 180, 180, 0, 0 } // ui landscape right 262 }; 263 *rotation = (float) front_rotations[ui_orientation - 1][device_orientation - 1]; 264 } 265 #endif 266 267 return result; 268} 269 270static void COREMEDIA_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame) 271{ 272 // !!! FIXME: this currently copies the data to the surface, but in theory we could just keep this locked until ReleaseFrame... 273 SDL_aligned_free(frame->pixels); 274} 275 276static void COREMEDIA_CloseDevice(SDL_Camera *device) 277{ 278 if (device && device->hidden) { 279 #ifdef USE_UIKIT_DEVICE_ROTATION 280 [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; 281 #endif 282 283 SDLPrivateCameraData *hidden = (SDLPrivateCameraData *) CFBridgingRelease(device->hidden); 284 device->hidden = NULL; 285 286 AVCaptureSession *session = hidden.session; 287 if (session) { 288 hidden.session = nil; 289 [session stopRunning]; 290 [session removeInput:[session.inputs objectAtIndex:0]]; 291 [session removeOutput:(AVCaptureVideoDataOutput *)[session.outputs objectAtIndex:0]]; 292 session = nil; 293 } 294 295 hidden.delegate = NULL; 296 hidden.current_sample = NULL; 297 } 298} 299 300static bool COREMEDIA_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec) 301{ 302 AVCaptureDevice *avdevice = (__bridge AVCaptureDevice *) device->handle; 303 304 // Pick format that matches the spec 305 const int w = spec->width; 306 const int h = spec->height; 307 const float rate = (float)spec->framerate_numerator / spec->framerate_denominator; 308 AVCaptureDeviceFormat *spec_format = nil; 309 NSArray<AVCaptureDeviceFormat *> *formats = [avdevice formats]; 310 for (AVCaptureDeviceFormat *format in formats) { 311 CMFormatDescriptionRef formatDescription = [format formatDescription]; 312 SDL_PixelFormat device_format = SDL_PIXELFORMAT_UNKNOWN; 313 SDL_Colorspace device_colorspace = SDL_COLORSPACE_UNKNOWN; 314 CoreMediaFormatToSDL(CMFormatDescriptionGetMediaSubType(formatDescription), &device_format, &device_colorspace); 315 if (device_format != spec->format || device_colorspace != spec->colorspace) { 316 continue; 317 } 318 319 const CMVideoDimensions dim = CMVideoFormatDescriptionGetDimensions(formatDescription); 320 if ((int)dim.width != w || (int)dim.height != h) { 321 continue; 322 } 323 324 const float FRAMERATE_EPSILON = 0.01f; 325 for (AVFrameRateRange *framerate in format.videoSupportedFrameRateRanges) { 326 // Check if the requested rate is within the supported range 327 if (rate >= (framerate.minFrameRate - FRAMERATE_EPSILON) && 328 rate <= (framerate.maxFrameRate + FRAMERATE_EPSILON)) { 329 330 // Prefer formats with narrower frame rate ranges that are closer to our target 331 // This helps avoid formats that support a wide range (like 10-60 FPS) 332 // when we want a specific rate (like 30 FPS) 333 bool should_select = false; 334 if (spec_format == nil) { 335 should_select = true; 336 } else { 337 AVFrameRateRange *current_range = spec_format.videoSupportedFrameRateRanges.firstObject; 338 float current_range_width = current_range.maxFrameRate - current_range.minFrameRate; 339 float new_range_width = framerate.maxFrameRate - framerate.minFrameRate; 340 341 // Prefer formats with narrower ranges, or if ranges are similar, prefer closer to target 342 if (new_range_width < current_range_width) { 343 should_select = true; 344 } else if (SDL_fabsf(new_range_width - current_range_width) < 0.1f) { 345 // Similar range width, prefer the one closer to our target rate 346 float current_distance = SDL_fabsf(rate - current_range.minFrameRate); 347 float new_distance = SDL_fabsf(rate - framerate.minFrameRate); 348 if (new_distance < current_distance) { 349 should_select = true; 350 } 351 } 352 } 353 354 if (should_select) { 355 spec_format = format; 356 } 357 } 358 } 359 360 if (spec_format != nil) { 361 break; 362 } 363 } 364 365 if (spec_format == nil) { 366 return SDL_SetError("camera spec format not available"); 367 } else if (![avdevice lockForConfiguration:NULL]) { 368 return SDL_SetError("Cannot lockForConfiguration"); 369 } 370 371 avdevice.activeFormat = spec_format; 372 373 // Try to set the frame duration to enforce the requested frame rate 374 const CMTime frameDuration = CMTimeMake(spec->framerate_denominator, spec->framerate_numerator); 375 376 // Check if the device supports setting frame duration 377 if ([avdevice respondsToSelector:@selector(setActiveVideoMinFrameDuration:)] && 378 [avdevice respondsToSelector:@selector(setActiveVideoMaxFrameDuration:)]) { 379 @try { 380 avdevice.activeVideoMinFrameDuration = frameDuration; 381 avdevice.activeVideoMaxFrameDuration = frameDuration; 382 } @catch (NSException *exception) { 383 // Some devices don't support setting frame duration, that's okay 384 } 385 } 386 387 [avdevice unlockForConfiguration]; 388 389 AVCaptureSession *session = [[AVCaptureSession alloc] init]; 390 if (session == nil) { 391 return SDL_SetError("Failed to allocate/init AVCaptureSession"); 392 } 393 394 session.sessionPreset = AVCaptureSessionPresetHigh; 395#if defined(SDL_PLATFORM_IOS) 396 if (@available(iOS 10.0, tvOS 17.0, *)) { 397 session.automaticallyConfiguresCaptureDeviceForWideColor = NO; 398 } 399#endif 400 401 NSError *error = nil; 402 AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:avdevice error:&error]; 403 if (!input) { 404 return SDL_SetError("Cannot create AVCaptureDeviceInput"); 405 } 406 407 AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init]; 408 if (!output) { 409 return SDL_SetError("Cannot create AVCaptureVideoDataOutput"); 410 } 411 412 output.videoSettings = @{ 413 (id)kCVPixelBufferWidthKey : @(spec->width), 414 (id)kCVPixelBufferHeightKey : @(spec->height), 415 (id)kCVPixelBufferPixelFormatTypeKey : @(CMFormatDescriptionGetMediaSubType([spec_format formatDescription])) 416 }; 417 418 char threadname[64]; 419 SDL_GetCameraThreadName(device, threadname, sizeof (threadname)); 420 dispatch_queue_t queue = dispatch_queue_create(threadname, NULL); 421 //dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 422 if (!queue) { 423 return SDL_SetError("dispatch_queue_create() failed"); 424 } 425 426 SDLCaptureVideoDataOutputSampleBufferDelegate *delegate = [[SDLCaptureVideoDataOutputSampleBufferDelegate alloc] init:device]; 427 if (delegate == nil) { 428 return SDL_SetError("Cannot create SDLCaptureVideoDataOutputSampleBufferDelegate"); 429 } 430 [output setSampleBufferDelegate:delegate queue:queue]; 431 432 if (![session canAddInput:input]) { 433 return SDL_SetError("Cannot add AVCaptureDeviceInput"); 434 } 435 [session addInput:input]; 436 437 if (![session canAddOutput:output]) { 438 return SDL_SetError("Cannot add AVCaptureVideoDataOutput"); 439 } 440 [session addOutput:output]; 441 442 // Try to set the frame rate on the device (preferred modern approach) 443 if ([avdevice lockForConfiguration:nil]) { 444 @try { 445 avdevice.activeVideoMinFrameDuration = frameDuration; 446 avdevice.activeVideoMaxFrameDuration = frameDuration; 447 } @catch (NSException *exception) { 448 // Some devices don't support setting frame duration, that's okay 449 } 450 [avdevice unlockForConfiguration]; 451 } 452 453 [session commitConfiguration]; 454 455 SDLPrivateCameraData *hidden = [[SDLPrivateCameraData alloc] init]; 456 if (hidden == nil) { 457 return SDL_SetError("Cannot create SDLPrivateCameraData"); 458 } 459 460 hidden.session = session; 461 hidden.delegate = delegate; 462 hidden.current_sample = NULL; 463 464 #ifdef USE_UIKIT_DEVICE_ROTATION 465 // When using a camera, we turn on device orientation tracking. The docs note that this turns on 466 // the device's accelerometer, so I assume this burns power, so we don't leave this running all 467 // the time. These calls nest, so we just need to call the matching `end` message when we close. 468 // You _can_ get an actual events through this mechanism, but we just want to be able to call 469 // -[UIDevice orientation], which will update with real info while notifications are enabled. 470 UIDevice *uidevice = [UIDevice currentDevice]; 471 [uidevice beginGeneratingDeviceOrientationNotifications]; 472 hidden.last_device_orientation = uidevice.orientation; 473 if (!UIDeviceOrientationIsValidInterfaceOrientation(hidden.last_device_orientation)) { 474 // accelerometer isn't ready yet or the phone is laying flat or something. Just try to guess from how the UI is oriented at the moment. 475#pragma clang diagnostic push 476#pragma clang diagnostic ignored "-Wdeprecated-declarations" 477 switch ([UIApplication sharedApplication].statusBarOrientation) { 478#pragma clang diagnostic pop 479 case UIInterfaceOrientationPortrait: hidden.last_device_orientation = UIDeviceOrientationPortrait; break; 480 case UIInterfaceOrientationPortraitUpsideDown: hidden.last_device_orientation = UIDeviceOrientationPortraitUpsideDown; break; 481 case UIInterfaceOrientationLandscapeLeft: hidden.last_device_orientation = UIDeviceOrientationLandscapeRight; break; // Apple docs say UI and device orientations are reversed in landscape. 482 case UIInterfaceOrientationLandscapeRight: hidden.last_device_orientation = UIDeviceOrientationLandscapeLeft; break; 483 default: hidden.last_device_orientation = UIDeviceOrientationPortrait; break; // oh well. 484 } 485 } 486 #endif 487 488 device->hidden = (struct SDL_PrivateCameraData *)CFBridgingRetain(hidden); 489 490 [session startRunning]; // !!! FIXME: docs say this can block while camera warms up and shouldn't be done on main thread. Maybe push through `queue`? 491 492 CheckCameraPermissions(device); // check right away, in case the process is already granted permission. 493 494 return true; 495} 496 497static void COREMEDIA_FreeDeviceHandle(SDL_Camera *device) 498{ 499 if (device && device->handle) { 500 CFBridgingRelease(device->handle); 501 } 502} 503 504static void GatherCameraSpecs(AVCaptureDevice *device, CameraFormatAddData *add_data) 505{ 506 SDL_zerop(add_data); 507 508 for (AVCaptureDeviceFormat *fmt in device.formats) { 509 if (CMFormatDescriptionGetMediaType(fmt.formatDescription) != kCMMediaType_Video) { 510 continue; 511 } 512 513//NSLog(@"Available camera format: %@\n", fmt); 514 SDL_PixelFormat device_format = SDL_PIXELFORMAT_UNKNOWN; 515 SDL_Colorspace device_colorspace = SDL_COLORSPACE_UNKNOWN; 516 CoreMediaFormatToSDL(CMFormatDescriptionGetMediaSubType(fmt.formatDescription), &device_format, &device_colorspace); 517 if (device_format == SDL_PIXELFORMAT_UNKNOWN) { 518 continue; 519 } 520 521 const CMVideoDimensions dims = CMVideoFormatDescriptionGetDimensions(fmt.formatDescription); 522 const int w = (int) dims.width; 523 const int h = (int) dims.height; 524 for (AVFrameRateRange *framerate in fmt.videoSupportedFrameRateRanges) { 525 int min_numerator = 0, min_denominator = 1; 526 int max_numerator = 0, max_denominator = 1; 527 528 SDL_CalculateFraction(framerate.minFrameRate, &min_numerator, &min_denominator); 529 SDL_AddCameraFormat(add_data, device_format, device_colorspace, w, h, min_numerator, min_denominator); 530 SDL_CalculateFraction(framerate.maxFrameRate, &max_numerator, &max_denominator); 531 if (max_numerator != min_numerator || max_denominator != min_denominator) { 532 SDL_AddCameraFormat(add_data, device_format, device_colorspace, w, h, max_numerator, max_denominator); 533 } 534 } 535 } 536} 537 538static bool FindCoreMediaCameraByUniqueID(SDL_Camera *device, void *userdata) 539{ 540 NSString *uniqueid = (__bridge NSString *) userdata; 541 AVCaptureDevice *avdev = (__bridge AVCaptureDevice *) device->handle; 542 return ([uniqueid isEqualToString:avdev.uniqueID]) ? true : false; 543} 544 545static void MaybeAddDevice(AVCaptureDevice *avdevice) 546{ 547 if (!avdevice.connected) { 548 return; // not connected. 549 } else if (![avdevice hasMediaType:AVMediaTypeVideo]) { 550 return; // not a camera. 551 } else if (SDL_FindPhysicalCameraByCallback(FindCoreMediaCameraByUniqueID, (__bridge void *) avdevice.uniqueID)) { 552 return; // already have this one. 553 } 554 555 CameraFormatAddData add_data; 556 GatherCameraSpecs(avdevice, &add_data); 557 if (add_data.num_specs > 0) { 558 SDL_CameraPosition position = SDL_CAMERA_POSITION_UNKNOWN; 559 if (avdevice.position == AVCaptureDevicePositionFront) { 560 position = SDL_CAMERA_POSITION_FRONT_FACING; 561 } else if (avdevice.position == AVCaptureDevicePositionBack) { 562 position = SDL_CAMERA_POSITION_BACK_FACING; 563 } 564 SDL_AddCamera(avdevice.localizedName.UTF8String, position, add_data.num_specs, add_data.specs, (void *) CFBridgingRetain(avdevice)); 565 } 566 567 SDL_free(add_data.specs); 568} 569 570static void COREMEDIA_DetectDevices(void) 571{ 572 NSArray<AVCaptureDevice *> *devices = nil; 573 574 if (@available(macOS 10.15, iOS 13, *)) { 575 // kind of annoying that there isn't a "give me anything that looks like a camera" option, 576 // so this list will need to be updated when Apple decides to add 577 // AVCaptureDeviceTypeBuiltInQuadrupleCamera some day. 578 NSArray *device_types = @[ 579 #ifdef SDL_PLATFORM_IOS 580 AVCaptureDeviceTypeBuiltInTelephotoCamera, 581 AVCaptureDeviceTypeBuiltInDualCamera, 582 AVCaptureDeviceTypeBuiltInDualWideCamera, 583 AVCaptureDeviceTypeBuiltInTripleCamera, 584 AVCaptureDeviceTypeBuiltInUltraWideCamera, 585 #else 586 AVCaptureDeviceTypeExternalUnknown, 587 #endif 588 AVCaptureDeviceTypeBuiltInWideAngleCamera 589 ]; 590 591 AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession 592 discoverySessionWithDeviceTypes:device_types 593 mediaType:AVMediaTypeVideo 594 position:AVCaptureDevicePositionUnspecified]; 595 596 devices = discoverySession.devices; 597 // !!! FIXME: this can use Key Value Observation to get hotplug events. 598 } else { 599#pragma clang diagnostic push 600#pragma clang diagnostic ignored "-Wdeprecated-declarations" 601 602 // this is deprecated but works back to macOS 10.7; 10.15 added AVCaptureDeviceDiscoverySession as a replacement. 603 devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; 604 // !!! FIXME: this can use AVCaptureDeviceWasConnectedNotification and AVCaptureDeviceWasDisconnectedNotification with NSNotificationCenter to get hotplug events. 605#pragma clang diagnostic pop 606 } 607 608 for (AVCaptureDevice *device in devices) { 609 MaybeAddDevice(device); 610 } 611} 612 613static void COREMEDIA_Deinitialize(void) 614{ 615 // !!! FIXME: disable hotplug. 616} 617 618static bool COREMEDIA_Init(SDL_CameraDriverImpl *impl) 619{ 620 impl->DetectDevices = COREMEDIA_DetectDevices; 621 impl->OpenDevice = COREMEDIA_OpenDevice; 622 impl->CloseDevice = COREMEDIA_CloseDevice; 623 impl->WaitDevice = COREMEDIA_WaitDevice; 624 impl->AcquireFrame = COREMEDIA_AcquireFrame; 625 impl->ReleaseFrame = COREMEDIA_ReleaseFrame; 626 impl->FreeDeviceHandle = COREMEDIA_FreeDeviceHandle; 627 impl->Deinitialize = COREMEDIA_Deinitialize; 628 629 impl->ProvidesOwnCallbackThread = true; 630 631 return true; 632} 633 634CameraBootStrap COREMEDIA_bootstrap = { 635 "coremedia", "SDL Apple CoreMedia camera driver", COREMEDIA_Init, false 636}; 637 638#endif // SDL_CAMERA_DRIVER_COREMEDIA 639 640[FILE END](C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.