16 #include "../vuo.hid/VuoUsbVendor.h"
18 #ifndef NS_RETURNS_INNER_POINTER
19 #define NS_RETURNS_INNER_POINTER
21 #include <IOKit/IOKitLib.h>
22 #include <CoreMediaIO/CMIOHardware.h>
23 #include <AVFoundation/AVFoundation.h>
28 "title" :
"VuoVideoCapture",
33 "VuoVideoInputDevice",
34 "VuoList_VuoVideoInputDevice",
35 "AVFoundation.framework",
36 "CoreMedia.framework",
37 "CoreMediaIO.framework",
38 "CoreVideo.framework",
53 static dispatch_once_t enableOnce = 0;
54 dispatch_once(&enableOnce, ^{
56 CMIOObjectPropertyAddress
property = { kCMIOHardwarePropertyAllowScreenCaptureDevices, kCMIOObjectPropertyScopeGlobal, kCMIOObjectPropertyElementMaster };
58 bool hasProperty = CMIOObjectHasProperty(kCMIOObjectSystemObject, &property);
61 VUserLog(
"Warning: Apple CoreMediaIO took %.1f seconds to initialize its 3rd-party plugins.", t1 - t0);
66 OSStatus ret = CMIOObjectSetPropertyData(kCMIOObjectSystemObject, &property, 0, NULL,
sizeof(yes), &yes);
67 if (ret != kCMIOHardwareNoError)
70 VUserLog(
"Warning: Couldn't enable tethered iOS device support: %s", errorText);
134 - (void) devicesDidChange:(NSNotification*)notif;
143 if (
self = [super
init])
145 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(devicesDidChange:) name:AVCaptureDeviceWasConnectedNotification object:nil];
146 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(devicesDidChange:) name:AVCaptureDeviceWasDisconnectedNotification object:nil];
154 [NSNotificationCenter.defaultCenter removeObserver:self];
169 - (void) devicesDidChange:(NSNotification*)notif
175 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(AVCaptureDevice *)object change:(NSDictionary *)change context:(
void *)p
178 if ([keyPath isEqualToString:
@"connected"]
179 && !
object.connected)
183 [vci->device removeObserver:self forKeyPath:@"connected" context:vci];
185 [vci->session removeInput:vci->input];
203 static dispatch_once_t
init = 0;
204 dispatch_once(&
init, ^{
237 CFMutableDictionaryRef match_dictionary = IOServiceMatching(
"IOFireWireDevice");
238 io_iterator_t entry_iterator;
239 if (IOServiceGetMatchingServices(kIOMasterPortDefault, match_dictionary, &entry_iterator) == kIOReturnSuccess)
241 io_registry_entry_t serviceObject;
242 while ((serviceObject = IOIteratorNext(entry_iterator)))
244 CFMutableDictionaryRef serviceDictionary;
245 if (IORegistryEntryCreateCFProperties(serviceObject, &serviceDictionary, kCFAllocatorDefault, kNilOptions) != kIOReturnSuccess)
247 IOObjectRelease(serviceObject);
251 NSString *guidAsHexString = [NSString stringWithFormat:@"%llx",[(NSNumber *)CFDictionaryGetValue(serviceDictionary, @"GUID") longLongValue]];
252 if ([uniqueID rangeOfString:guidAsHexString].location != NSNotFound)
254 NSString *vendorName = [(NSString *)CFDictionaryGetValue(serviceDictionary, @"FireWire Vendor Name") retain];
255 CFRelease(serviceDictionary);
256 IOObjectRelease(serviceObject);
257 IOObjectRelease(entry_iterator);
261 CFRelease(serviceDictionary);
262 IOObjectRelease(serviceObject);
264 IOObjectRelease(entry_iterator);
267 match_dictionary = IOServiceMatching(
"IOUSBDevice");
268 if (IOServiceGetMatchingServices(kIOMasterPortDefault, match_dictionary, &entry_iterator) == kIOReturnSuccess)
270 io_registry_entry_t serviceObject;
271 while ((serviceObject = IOIteratorNext(entry_iterator)))
273 CFMutableDictionaryRef serviceDictionary;
274 if (IORegistryEntryCreateCFProperties(serviceObject, &serviceDictionary, kCFAllocatorDefault, kNilOptions) != kIOReturnSuccess)
276 IOObjectRelease(serviceObject);
280 NSString *guidAsHexString = [NSString stringWithFormat:@"%llx",[(NSNumber *)CFDictionaryGetValue(serviceDictionary, @"locationID") longLongValue]];
281 if ([uniqueID rangeOfString:guidAsHexString].location != NSNotFound)
283 NSString *vendorName = [(NSString *)CFDictionaryGetValue(serviceDictionary, @"USB Vendor Name") retain];
284 if (!vendorName.length)
286 char *vendor =
VuoUsbVendor_getText(((NSNumber *)CFDictionaryGetValue(serviceDictionary,
@"idVendor")).intValue);
287 vendorName = [[NSString stringWithUTF8String:vendor] retain];
291 CFRelease(serviceDictionary);
292 IOObjectRelease(serviceObject);
293 IOObjectRelease(entry_iterator);
297 CFRelease(serviceDictionary);
298 IOObjectRelease(serviceObject);
300 IOObjectRelease(entry_iterator);
311 NSString *deviceName = dev.localizedName;
315 NSString *modelWithoutVendor = dev.modelID;
316 NSUInteger tail = [modelWithoutVendor rangeOfString:@" Camera VendorID_"].location;
317 if (tail != NSNotFound)
318 modelWithoutVendor = [modelWithoutVendor substringToIndex:tail];
319 if (modelWithoutVendor.length
320 && [dev.manufacturer rangeOfString:modelWithoutVendor].location == NSNotFound)
321 deviceName = [NSString stringWithFormat:@"%@ %@", modelWithoutVendor, deviceName];
323 if (![dev.manufacturer isEqualToString:vendorName]
324 && ![dev.manufacturer isEqualToString:
@"Unknown"])
325 deviceName = [NSString stringWithFormat:@"%@ %@", dev.manufacturer, deviceName];
327 if ([vendorName length])
328 deviceName = [NSString stringWithFormat:@"%@ %@", vendorName, deviceName];
344 [vendorName release];
357 NSArray *inputDevices = [AVCaptureDevice devices];
361 for (AVCaptureDevice *dev in inputDevices)
363 if (![dev hasMediaType:AVMediaTypeVideo]
364 && ![dev hasMediaType:AVMediaTypeMuxed])
397 - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
399 CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
402 VUserLog(
"Warning: AV Foundation gave us a sampleBuffer without a pixelBuffer.");
406 size_t pixelBufferWidth = CVPixelBufferGetWidth(pixelBuffer);
407 size_t pixelBufferHeight = CVPixelBufferGetHeight(pixelBuffer);
410 CMVideoDimensions preferredDim = CMVideoFormatDescriptionGetDimensions(_vci->preferredFormat.formatDescription);
411 if ((preferredDim.width && pixelBufferWidth != preferredDim.width)
412 || (preferredDim.height && pixelBufferHeight != preferredDim.height))
414 const int maxSetSizeAttempts = 10;
415 if (_vci->attemptedSetSizes < maxSetSizeAttempts)
417 ++_vci->attemptedSetSizes;
418 VUserLog(
"Warning: We requested %dx%d, but the camera gave us %zux%zu. Trying again (attempt %d of %d)…",
419 preferredDim.width, preferredDim.height,
420 pixelBufferWidth, pixelBufferHeight,
421 _vci->attemptedSetSizes, maxSetSizeAttempts);
425 else if (_vci->attemptedSetSizes == maxSetSizeAttempts)
427 ++_vci->attemptedSetSizes;
428 VUserLog(
"Warning: The camera didn't get the hint after %d attempts, so we'll just let it do what it wants…", maxSetSizeAttempts);
432 __block CVOpenGLTextureRef texture;
434 CVOpenGLTextureCacheCreateTextureFromImage(NULL, _vci->textureCache, pixelBuffer, NULL, &texture);
437 NSDictionary *pixelAspectRatio = (NSDictionary *)CVBufferGetAttachment(pixelBuffer, CFSTR(
"CVPixelAspectRatio"),
nullptr);
438 unsigned int stretchedWidth = 0;
439 if (pixelAspectRatio)
440 stretchedWidth = pixelBufferWidth * ((NSNumber *)pixelAspectRatio[
@"HorizontalSpacing"]).intValue / ((NSNumber *)pixelAspectRatio[
@"VerticalSpacing"]).intValue;
443 CVOpenGLTextureGetName(texture),
450 if (_vci->firstFrameTime == -INFINITY)
451 _vci->firstFrameTime = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer));
452 if (_vci->receivedFrameTrigger)
454 CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) - _vci->firstFrameTime,
455 CMTimeGetSeconds(CMSampleBufferGetDuration(sampleBuffer))));
457 CVOpenGLTextureRelease(texture);
461 CVOpenGLTextureCacheFlush(_vci->textureCache, 0);
472 vci->
device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
475 vci->
device = [AVCaptureDevice deviceWithUniqueID:[NSString stringWithUTF8String:vci->vdevice.id]];
482 for (AVCaptureDevice *dev in AVCaptureDevice.devices)
484 if (![dev hasMediaType:AVMediaTypeVideo]
485 && ![dev hasMediaType:AVMediaTypeMuxed])
499 [vci->device addObserver:deviceListener forKeyPath:@"connected" options:0 context:vci];
506 vci->
input = [AVCaptureDeviceInput deviceInputWithDevice:vci->device error:&e];
510 [vci->session addInput:vci->input];
512 VUserLog(
"Error: -[AVCaptureSession canAddInput:] said no.");
515 VUserLog(
"Error: Couldn't create AVCaptureDeviceInput: %s", e.localizedDescription.UTF8String);
528 if ([AVCaptureDevice respondsToSelector:
@selector(authorizationStatusForMediaType:)])
530 long status = (long)[AVCaptureDevice performSelector:
@selector(authorizationStatusForMediaType:) withObject:
@"vide"];
532 VUserLog(
"Warning: Video input may be unavailable due to system restrictions. Check System Preferences > Security & Privacy > Privacy > Camera.");
535 VUserLog(
"Error: Video input is unavailable due to system restrictions. Check System Preferences > Security & Privacy > Privacy > Camera.");
543 vci->
queue = dispatch_queue_create(
"org.vuo.VuoVideoCapture", NULL);
546 dispatch_sync(dispatch_get_main_queue(), ^{
552 vci->
session.sessionPreset = AVCaptureSessionPresetPhoto;
569 AVCaptureVideoDataOutput *output = [AVCaptureVideoDataOutput new];
570 [output setAlwaysDiscardsLateVideoFrames:YES];
571 [output setSampleBufferDelegate:vci->delegate queue:vci->queue];
572 [vci->session addOutput:output];
579 __block CVReturn ret;
581 ret = CVOpenGLTextureCacheCreate(NULL, NULL, cgl_ctx, pf, NULL, &
vci->
textureCache);
583 CGLReleasePixelFormat(pf);
585 if (ret != kCVReturnSuccess)
587 VUserLog(
"Error: Couldn't create texture cache: %d", ret);
611 AVCaptureDeviceFormat *bestFormat = nil;
612 AVFrameRateRange *bestFrameRateRange = nil;
613 if (width == 0 && height == 0)
616 for (AVCaptureDeviceFormat *format in
vci->
device.formats)
617 for (AVFrameRateRange *range in format.videoSupportedFrameRateRanges)
618 if (range.maxFrameRate > bestFrameRateRange.maxFrameRate)
621 bestFrameRateRange = range;
625 for (AVCaptureDeviceFormat *format in
vci->
device.formats)
627 CMVideoDimensions dim = CMVideoFormatDescriptionGetDimensions(format.formatDescription);
628 for (AVFrameRateRange *range in format.videoSupportedFrameRateRanges)
630 CMVideoDimensions bestDim = CMVideoFormatDescriptionGetDimensions(bestFormat.formatDescription);
632 && dim.width > bestDim.width
633 && dim.height > bestDim.height)
636 bestFrameRateRange = range;
644 float bestDist = INFINITY;
645 for (AVCaptureDeviceFormat *format in
vci->
device.formats)
647 CMVideoDimensions dim = CMVideoFormatDescriptionGetDimensions(format.formatDescription);
648 float dist = pow(dim.width - width, 2) + pow(dim.height - height, 2);
653 bestFrameRateRange = nil;
654 for (AVFrameRateRange *range in format.videoSupportedFrameRateRanges)
655 if (range.maxFrameRate > bestFrameRateRange.maxFrameRate)
656 bestFrameRateRange = range;
663 if ([
vci->
device lockForConfiguration:NULL] == YES)
667 [vci->preferredFormat release];
669 if (bestFrameRateRange)
671 vci->
device.activeVideoMinFrameDuration = bestFrameRateRange.minFrameDuration;
672 vci->
device.activeVideoMaxFrameDuration = bestFrameRateRange.minFrameDuration;
675 [vci->device unlockForConfiguration];
678 VUserLog(
"Warning: Couldn't lock the video input device for configuration; ignoring the specified resolution.");
682 [vci->preferredFormat release];
684 VUserLog(
"Warning: This device didn't report any supported formats.");
699 dispatch_sync(dispatch_get_main_queue(), ^{
701 [vci->session startRunning];
716 dispatch_sync(dispatch_get_main_queue(), ^{
718 [vci->session stopRunning];
745 [vci->device removeObserver:deviceListener forKeyPath:@"connected" context:vci];
748 dispatch_sync(dispatch_get_main_queue(), ^{
750 [vci->session stopRunning];
754 [vci->session release];
755 [vci->delegate release];