371 lines
11 KiB
C++
371 lines
11 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "AvfMediaCaptureHelper.h"
|
|
|
|
// Steps to try if permissions are not working correctly in macOS
|
|
|
|
// 1) macOS resetting permissions, also useful for testing:
|
|
// tccutil reset Camera
|
|
// tccutil reset Microphone
|
|
//
|
|
// 2) System integrity protection has to be enabled for permissions to work correctly, check using:
|
|
// csrutil status
|
|
//
|
|
// 3) delete permissions database then reboot mac:
|
|
// ~/Library/Application\\ Support/com.apple.TCC
|
|
|
|
@interface AvfMediaCaptureHelper()<AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate>
|
|
|
|
@property (nonatomic, assign) AVMediaType mediaType;
|
|
@property (nonatomic, assign) AVCaptureSession* captureSession;
|
|
@property (nonatomic, assign) AVCaptureDevice* captureDevice;
|
|
@property (nonatomic, assign) AVCaptureDeviceInput* deviceInput;
|
|
@property (nonatomic, assign) AVCaptureOutput* captureOutput;
|
|
@property (nonatomic, assign) dispatch_queue_t callBackQueue;
|
|
|
|
@property (nonatomic, assign) void (^sampleBufferCallback)(CMSampleBufferRef);
|
|
@property (nonatomic, assign) void (^notificationCallback)(NSNotification* const Notification);
|
|
|
|
@end
|
|
|
|
@implementation AvfMediaCaptureHelper
|
|
|
|
- (instancetype)init:(AVMediaType)mediaType
|
|
{
|
|
self = [super init];
|
|
if (self != nil)
|
|
{
|
|
self.mediaType = mediaType;
|
|
self.captureSession = nil;
|
|
self.captureDevice = nil;
|
|
self.deviceInput = nil;
|
|
self.captureOutput = nil;
|
|
self.sampleBufferCallback = nil;
|
|
self.notificationCallback = nil;
|
|
self.callBackQueue = dispatch_queue_create("AvfCaptureSessionSampleCallbackQueue", DISPATCH_QUEUE_SERIAL);
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[self reset];
|
|
|
|
if (self.callBackQueue != nil)
|
|
{
|
|
dispatch_release(self.callBackQueue);
|
|
self.callBackQueue = nil;
|
|
}
|
|
|
|
[super dealloc];
|
|
}
|
|
|
|
- (void)reset
|
|
{
|
|
if (self.captureSession.inputs.count > 0 && self.captureSession.outputs.count > 0)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
if (self.captureSession.isRunning)
|
|
{
|
|
[self.captureSession stopRunning];
|
|
}
|
|
|
|
if (self.captureOutput != nil)
|
|
{
|
|
[self.captureSession removeOutput:self.captureOutput];
|
|
[self.captureOutput release];
|
|
self.captureOutput = nil;
|
|
}
|
|
|
|
if (self.deviceInput != nil)
|
|
{
|
|
[self.captureSession removeInput:self.deviceInput];
|
|
[self.deviceInput release];
|
|
self.deviceInput = nil;
|
|
}
|
|
|
|
if (self.captureSession != nil)
|
|
{
|
|
[self.captureSession release];
|
|
self.captureSession = nil;
|
|
}
|
|
|
|
if (self.captureDevice != nil)
|
|
{
|
|
[self.captureDevice release];
|
|
self.captureDevice = nil;
|
|
}
|
|
|
|
if (self.sampleBufferCallback != nil)
|
|
{
|
|
Block_release(self.sampleBufferCallback);
|
|
self.sampleBufferCallback = nil;
|
|
}
|
|
|
|
if (self.notificationCallback != nil)
|
|
{
|
|
Block_release(self.notificationCallback);
|
|
self.notificationCallback = nil;
|
|
}
|
|
}
|
|
|
|
+ (EAvfMediaCaptureAuthStatus)authorizationStatusForMediaType:(AVMediaType)mediaType
|
|
{
|
|
if (mediaType != AVMediaTypeVideo && mediaType != AVMediaTypeAudio)
|
|
{
|
|
return EAvfMediaCaptureAuthStatus::InvalidRequest;
|
|
}
|
|
|
|
NSDictionary<NSString*,id>* infoDictionary = [NSBundle mainBundle].infoDictionary;
|
|
|
|
id entry = nil;
|
|
|
|
if (infoDictionary != nil)
|
|
{
|
|
if (mediaType == AVMediaTypeVideo)
|
|
{
|
|
entry = infoDictionary[@"NSCameraUsageDescription"];
|
|
}
|
|
else if (mediaType == AVMediaTypeAudio)
|
|
{
|
|
entry = infoDictionary[@"NSMicrophoneUsageDescription"];
|
|
}
|
|
}
|
|
|
|
if (entry == nil)
|
|
{
|
|
return EAvfMediaCaptureAuthStatus::MissingInfoPListEntry;
|
|
}
|
|
|
|
return (EAvfMediaCaptureAuthStatus)[AVCaptureDevice authorizationStatusForMediaType:mediaType];
|
|
}
|
|
|
|
+ (EAvfMediaCaptureAuthStatus)requestAccessForMediaType:(AVMediaType)mediaType completionCallback:(void (^)(EAvfMediaCaptureAuthStatus AuthStatus))cbHandler
|
|
{
|
|
// Don't make request if the correct device access key is not in the info.plist otherwise the OS will terminate this app
|
|
EAvfMediaCaptureAuthStatus authStatus = [AvfMediaCaptureHelper authorizationStatusForMediaType:mediaType];
|
|
switch(authStatus)
|
|
{
|
|
case EAvfMediaCaptureAuthStatus::NotDetermined:
|
|
{
|
|
[AVCaptureDevice requestAccessForMediaType:mediaType completionHandler:^(BOOL bGranted)
|
|
{
|
|
cbHandler((EAvfMediaCaptureAuthStatus)[AVCaptureDevice authorizationStatusForMediaType:mediaType]);
|
|
}];
|
|
break;
|
|
}
|
|
case EAvfMediaCaptureAuthStatus::Authorized:
|
|
case EAvfMediaCaptureAuthStatus::Restricted:
|
|
case EAvfMediaCaptureAuthStatus::Denied:
|
|
case EAvfMediaCaptureAuthStatus::MissingInfoPListEntry:
|
|
case EAvfMediaCaptureAuthStatus::InvalidRequest:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return authStatus;
|
|
}
|
|
|
|
- (BOOL)setupCaptureSession:(NSString*)deviceID sampleBufferCallback:(void(^)(CMSampleBufferRef sampleBuffer))sampleCallbackBlock notificationCallback:(void(^)(NSNotification* const notification))notificationCallbackBlock
|
|
{
|
|
[self reset];
|
|
|
|
if (sampleCallbackBlock != nil)
|
|
{
|
|
self.sampleBufferCallback = Block_copy(sampleCallbackBlock);
|
|
}
|
|
if (notificationCallbackBlock != nil)
|
|
{
|
|
self.notificationCallback = Block_copy(notificationCallbackBlock);
|
|
}
|
|
|
|
self.captureDevice = [[AVCaptureDevice deviceWithUniqueID: deviceID] retain];
|
|
|
|
// Check that the chosen device supports the specified media type
|
|
if (![self.captureDevice hasMediaType: self.mediaType])
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
if (self.captureDevice != nil)
|
|
{
|
|
self.captureSession = [[AVCaptureSession alloc] init];
|
|
|
|
// Default to high so it starts with a reasonably good initial setting
|
|
self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;
|
|
|
|
// Add device input
|
|
{
|
|
self.deviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.captureDevice error:nil];
|
|
|
|
if (self.deviceInput != nil && [self.captureSession canAddInput:self.deviceInput])
|
|
{
|
|
[self.captureSession addInput:self.deviceInput];
|
|
}
|
|
}
|
|
|
|
// If we have an input add output capture
|
|
if (self.captureSession.inputs.count > 0)
|
|
{
|
|
// Video or Audio Device - need the correct output
|
|
if (self.mediaType == AVMediaTypeVideo)
|
|
{
|
|
AVCaptureVideoDataOutput* videoOutput = [[AVCaptureVideoDataOutput alloc] init];
|
|
|
|
[videoOutput setSampleBufferDelegate:self queue:self.callBackQueue];
|
|
videoOutput.alwaysDiscardsLateVideoFrames = YES;
|
|
|
|
videoOutput.videoSettings = @{ (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
|
|
(id)kCVPixelBufferMetalCompatibilityKey : @(1)};
|
|
|
|
self.captureOutput = videoOutput;
|
|
}
|
|
else
|
|
{
|
|
AVCaptureAudioDataOutput* audioOutput = [[AVCaptureAudioDataOutput alloc] init];
|
|
|
|
[audioOutput setSampleBufferDelegate:self queue:self.callBackQueue];
|
|
|
|
#define MACOS_AUDIO_CAPTURE_USR_SETTINGS 0
|
|
#if PLATFORM_MAC && MACOS_AUDIO_CAPTURE_USR_SETTINGS != 0
|
|
// AVCaptureAudioDataOutput.audioSettings is available on macOS only
|
|
// Use default device / OS output instead. This is left here for debug and test purposes
|
|
// Changes the output audio format delivered to the sample buffer delete function
|
|
// See AVFAudio/AVAudioSettings.h for available keys / values
|
|
audioOutput.audioSettings = @{
|
|
// LPCM=Linear Quantization Levels
|
|
AVFormatIDKey :@(kAudioFormatLinearPCM),
|
|
// 8/16.24/32=INT, 32=FLOAT
|
|
AVLinearPCMBitDepthKey : @(32),
|
|
// NO=INT, YES=FLOAT, engine perfers float and will convert to float eventually what ever we output
|
|
AVLinearPCMIsFloatKey : @YES,
|
|
// Non Interleaved each channel is sequential one after each other e.g 3 channel samples = 111222333, Interleaved e.g. 123123123, engine wants interleaved
|
|
AVLinearPCMIsNonInterleavedKey : @NO,
|
|
// As Per hardware
|
|
AVNumberOfChannelsKey : @(1)
|
|
};
|
|
#endif
|
|
#undef MACOS_AUDIO_CAPTURE_USR_SETTINGS
|
|
|
|
self.captureOutput = audioOutput;
|
|
}
|
|
|
|
if (self.captureOutput != nil && [self.captureSession canAddOutput:self.captureOutput])
|
|
{
|
|
[self.captureSession addOutput:self.captureOutput];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (self.captureSession.inputs.count > 0 && self.captureSession.outputs.count > 0)
|
|
{
|
|
NSNotificationCenter* sharedCentre = [NSNotificationCenter defaultCenter];
|
|
|
|
[sharedCentre addObserver:self selector:@selector(captureNotification:) name:AVCaptureSessionDidStartRunningNotification object:self.captureSession];
|
|
[sharedCentre addObserver:self selector:@selector(captureNotification:) name:AVCaptureSessionDidStopRunningNotification object:self.captureSession];
|
|
[sharedCentre addObserver:self selector:@selector(captureNotification:) name:AVCaptureSessionWasInterruptedNotification object:self.captureSession];
|
|
[sharedCentre addObserver:self selector:@selector(captureNotification:) name:AVCaptureSessionInterruptionEndedNotification object:self.captureSession];
|
|
[sharedCentre addObserver:self selector:@selector(captureNotification:) name:AVCaptureSessionRuntimeErrorNotification object:self.captureSession];
|
|
|
|
#if PLATFORM_MAC && WITH_EDITOR
|
|
if ([self.captureOutput isKindOfClass:[AVCaptureVideoDataOutput class]])
|
|
{
|
|
[sharedCentre addObserver:self selector:@selector(captureNotification:) name:NSApplicationDidBecomeActiveNotification object:nil];
|
|
[sharedCentre addObserver:self selector:@selector(captureNotification:) name:NSApplicationWillResignActiveNotification object:nil];
|
|
}
|
|
#endif
|
|
|
|
return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (void)stopCaptureSession
|
|
{
|
|
if (self.captureSession.isRunning)
|
|
{
|
|
[self.captureSession stopRunning];
|
|
}
|
|
}
|
|
|
|
- (void)startCaptureSession
|
|
{
|
|
if (!self.captureSession.isRunning)
|
|
{
|
|
[self.captureSession startRunning];
|
|
}
|
|
}
|
|
|
|
- (BOOL)isCaptureRunning
|
|
{
|
|
return self.captureSession.isRunning;
|
|
}
|
|
|
|
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection*)connection
|
|
{
|
|
if (self.sampleBufferCallback != nil && sampleBuffer != NULL)
|
|
{
|
|
self.sampleBufferCallback(sampleBuffer);
|
|
}
|
|
}
|
|
|
|
- (void)captureNotification:(NSNotification*)notification
|
|
{
|
|
if (self.notificationCallback != nil && notification != nil)
|
|
{
|
|
self.notificationCallback(notification);
|
|
}
|
|
}
|
|
|
|
- (AVMediaType)getCaptureDeviceMediaType
|
|
{
|
|
return self.mediaType;
|
|
}
|
|
|
|
- (NSString*)getCaptureDeviceName
|
|
{
|
|
return self.captureDevice.localizedName;
|
|
}
|
|
|
|
- (NSArray<AVCaptureDeviceFormat*>*)getCaptureDeviceAvailableFormats
|
|
{
|
|
AVMediaType helperMediaType = self.mediaType;
|
|
NSArray<AVCaptureDeviceFormat*>* filtered = [self.captureDevice.formats filteredArrayUsingPredicate: [NSPredicate predicateWithBlock:^BOOL(AVCaptureDeviceFormat* format, NSDictionary* bindings)
|
|
{
|
|
return [format.mediaType isEqualToString: helperMediaType];
|
|
}]];
|
|
|
|
return filtered;
|
|
}
|
|
|
|
- (NSInteger)getCaptureDeviceActiveFormatIndex
|
|
{
|
|
NSArray<AVCaptureDeviceFormat*>* formats = [self getCaptureDeviceAvailableFormats];
|
|
for (NSInteger formatIndex = 0; formatIndex < formats.count; ++formatIndex)
|
|
{
|
|
if ([formats[formatIndex] isEqual:self.captureDevice.activeFormat])
|
|
{
|
|
return formatIndex;
|
|
}
|
|
}
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
- (BOOL)setCaptureDeviceActiveFormatIndex:(NSInteger)formatIdx
|
|
{
|
|
NSArray<AVCaptureDeviceFormat*>* formats = [self getCaptureDeviceAvailableFormats];
|
|
if (formatIdx >= 0 && formatIdx < formats.count && [self.captureDevice lockForConfiguration: nil])
|
|
{
|
|
self.captureDevice.activeFormat = formats[formatIdx];
|
|
[self.captureDevice unlockForConfiguration];
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
@end
|