| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515 |
- //
- // OBSAVCapture.m
- // mac-avcapture
- //
- // Created by Patrick Heyer on 2023-03-07.
- //
- #import "OBSAVCapture.h"
- #import "AVCaptureDeviceFormat+OBSListable.h"
- /// Tthe maximum number of frame rate ranges to show complete information for before providing a more generic description of the supported frame rates inside of a device format description.
- static const UInt32 kMaxFrameRateRangesInDescription = 10;
- @implementation OBSAVCapture
- - (instancetype)init
- {
- return [self initWithCaptureInfo:nil];
- }
- - (instancetype)initWithCaptureInfo:(OBSAVCaptureInfo *)capture_info
- {
- self = [super init];
- if (self) {
- CMIOObjectPropertyAddress propertyAddress = {kCMIOHardwarePropertyAllowScreenCaptureDevices,
- kCMIOObjectPropertyScopeGlobal, kCMIOObjectPropertyElementMain};
- UInt32 allow = 1;
- CMIOObjectSetPropertyData(kCMIOObjectSystemObject, &propertyAddress, 0, NULL, sizeof(allow), &allow);
- _errorDomain = @"com.obsproject.obs-studio.av-capture";
- _presetList = @{
- AVCaptureSessionPresetLow: @"Low",
- AVCaptureSessionPresetMedium: @"Medium",
- AVCaptureSessionPresetHigh: @"High",
- AVCaptureSessionPreset320x240: @"320x240",
- AVCaptureSessionPreset352x288: @"352x288",
- AVCaptureSessionPreset640x480: @"640x480",
- AVCaptureSessionPreset960x540: @"960x540",
- AVCaptureSessionPreset1280x720: @"1280x720",
- AVCaptureSessionPreset1920x1080: @"1920x1080",
- AVCaptureSessionPreset3840x2160: @"3840x2160",
- };
- _sessionQueue = dispatch_queue_create("session queue", DISPATCH_QUEUE_SERIAL);
- OBSAVCaptureVideoInfo newInfo = {0};
- _videoInfo = newInfo;
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceDisconnected:)
- name:AVCaptureDeviceWasDisconnectedNotification
- object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceConnected:)
- name:AVCaptureDeviceWasConnectedNotification
- object:nil];
- if (capture_info) {
- _captureInfo = capture_info;
- NSString *UUID = [OBSAVCapture stringFromSettings:_captureInfo->settings withSetting:@"device"];
- NSString *presetName = [OBSAVCapture stringFromSettings:_captureInfo->settings withSetting:@"preset"];
- BOOL isPresetEnabled = obs_data_get_bool(_captureInfo->settings, "use_preset");
- if (capture_info->isFastPath) {
- _isFastPath = YES;
- _isPresetBased = NO;
- } else {
- BOOL isBufferingEnabled = obs_data_get_bool(_captureInfo->settings, "buffering");
- obs_source_set_async_unbuffered(_captureInfo->source, !isBufferingEnabled);
- }
- __weak OBSAVCapture *weakSelf = self;
- dispatch_async(_sessionQueue, ^{
- NSError *error = nil;
- OBSAVCapture *instance = weakSelf;
- if ([instance createSession:&error]) {
- if ([instance switchCaptureDevice:UUID withError:nil]) {
- BOOL isSessionConfigured = NO;
- if (isPresetEnabled) {
- isSessionConfigured = [instance configureSessionWithPreset:presetName withError:nil];
- } else {
- isSessionConfigured = [instance configureSession:nil];
- }
- if (isSessionConfigured) {
- [instance startCaptureSession];
- }
- }
- } else {
- [instance AVCaptureLog:LOG_ERROR withFormat:error.localizedDescription];
- }
- });
- }
- }
- return self;
- }
- #pragma mark - Capture Session Handling
- - (BOOL)createSession:(NSError *__autoreleasing *)error
- {
- AVCaptureSession *session = [[AVCaptureSession alloc] init];
- [session beginConfiguration];
- if (!session) {
- if (error) {
- NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to create AVCaptureSession"};
- *error = [NSError errorWithDomain:self.errorDomain code:-101 userInfo:userInfo];
- }
- return NO;
- }
- AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
- if (!videoOutput) {
- if (error) {
- NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to create AVCaptureVideoDataOutput"};
- *error = [NSError errorWithDomain:self.errorDomain code:-102 userInfo:userInfo];
- }
- return NO;
- }
- AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init];
- if (!audioOutput) {
- if (error) {
- NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to create AVCaptureAudioDataOutput"};
- *error = [NSError errorWithDomain:self.errorDomain code:-103 userInfo:userInfo];
- }
- return NO;
- }
- dispatch_queue_t videoQueue = dispatch_queue_create(nil, nil);
- if (!videoQueue) {
- if (error) {
- NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to create video dispatch queue"};
- *error = [NSError errorWithDomain:self.errorDomain code:-104 userInfo:userInfo];
- }
- return NO;
- }
- dispatch_queue_t audioQueue = dispatch_queue_create(nil, nil);
- if (!audioQueue) {
- if (error) {
- NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to create audio dispatch queue"};
- *error = [NSError errorWithDomain:self.errorDomain code:-105 userInfo:userInfo];
- }
- return NO;
- }
- if ([session canAddOutput:videoOutput]) {
- [session addOutput:videoOutput];
- [videoOutput setSampleBufferDelegate:self queue:videoQueue];
- }
- if ([session canAddOutput:audioOutput]) {
- [session addOutput:audioOutput];
- [audioOutput setSampleBufferDelegate:self queue:audioQueue];
- }
- [session commitConfiguration];
- self.session = session;
- self.videoOutput = videoOutput;
- self.videoQueue = videoQueue;
- self.audioOutput = audioOutput;
- self.audioQueue = audioQueue;
- return YES;
- }
- - (BOOL)switchCaptureDevice:(NSString *)uuid withError:(NSError *__autoreleasing *)error
- {
- AVCaptureDevice *device = [AVCaptureDevice deviceWithUniqueID:uuid];
- if (self.deviceInput.device || !device) {
- [self stopCaptureSession];
- [self.session removeInput:self.deviceInput];
- [self.deviceInput.device unlockForConfiguration];
- self.deviceInput = nil;
- self.isDeviceLocked = NO;
- self.presetFormat = nil;
- }
- if (!device) {
- if (uuid.length < 1) {
- [self AVCaptureLog:LOG_INFO withFormat:@"No device selected"];
- self.deviceUUID = uuid;
- return NO;
- } else {
- [self AVCaptureLog:LOG_WARNING withFormat:@"Unable to initialize device with unique ID '%@'", uuid];
- return NO;
- }
- }
- const char *deviceName = device.localizedName.UTF8String;
- obs_data_set_string(self.captureInfo->settings, "device_name", deviceName);
- obs_data_set_string(self.captureInfo->settings, "device", device.uniqueID.UTF8String);
- [self AVCaptureLog:LOG_INFO withFormat:@"Selected device '%@'", device.localizedName];
- self.deviceUUID = device.uniqueID;
- BOOL isAudioSupported = [device hasMediaType:AVMediaTypeAudio] || [device hasMediaType:AVMediaTypeMuxed];
- obs_source_set_audio_active(self.captureInfo->source, isAudioSupported);
- AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:error];
- if (!deviceInput) {
- return NO;
- }
- [self.session beginConfiguration];
- if ([self.session canAddInput:deviceInput]) {
- [self.session addInput:deviceInput];
- self.deviceInput = deviceInput;
- } else {
- if (error) {
- NSDictionary *userInfo = @{
- NSLocalizedDescriptionKey: [NSString
- stringWithFormat:@"Unable to add device '%@' as deviceInput to capture session", self.deviceUUID]
- };
- *error = [NSError errorWithDomain:self.errorDomain code:-107 userInfo:userInfo];
- }
- [self.session commitConfiguration];
- return NO;
- }
- AVCaptureDeviceFormat *deviceFormat = device.activeFormat;
- CMMediaType mediaType = CMFormatDescriptionGetMediaType(deviceFormat.formatDescription);
- if (mediaType != kCMMediaType_Video && mediaType != kCMMediaType_Muxed) {
- if (error) {
- NSDictionary *userInfo = @{
- NSLocalizedDescriptionKey: [NSString stringWithFormat:@"CMMediaType '%@' is not supported",
- [OBSAVCapture stringFromFourCharCode:mediaType]]
- };
- *error = [NSError errorWithDomain:self.errorDomain code:-108 userInfo:userInfo];
- }
- [self.session removeInput:deviceInput];
- [self.session commitConfiguration];
- return NO;
- }
- if (self.isFastPath) {
- self.videoOutput.videoSettings = nil;
- NSMutableDictionary *videoSettings =
- [NSMutableDictionary dictionaryWithDictionary:self.videoOutput.videoSettings];
- FourCharCode targetPixelFormatType = kCVPixelFormatType_32BGRA;
- [videoSettings setObject:@(targetPixelFormatType)
- forKey:(__bridge NSString *) kCVPixelBufferPixelFormatTypeKey];
- self.videoOutput.videoSettings = videoSettings;
- } else {
- self.videoOutput.videoSettings = nil;
- FourCharCode subType = [[self.videoOutput.videoSettings
- objectForKey:(__bridge NSString *) kCVPixelBufferPixelFormatTypeKey] unsignedIntValue];
- if ([OBSAVCapture formatFromSubtype:subType] != VIDEO_FORMAT_NONE) {
- [self AVCaptureLog:LOG_DEBUG
- withFormat:@"Using native fourcc '%@'", [OBSAVCapture stringFromFourCharCode:subType]];
- } else {
- [self AVCaptureLog:LOG_DEBUG withFormat:@"Using fallback fourcc '%@' ('%@', 0x%08x unsupported)",
- [OBSAVCapture stringFromFourCharCode:kCVPixelFormatType_32BGRA],
- [OBSAVCapture stringFromFourCharCode:subType], subType];
- NSMutableDictionary *videoSettings =
- [NSMutableDictionary dictionaryWithDictionary:self.videoOutput.videoSettings];
- [videoSettings setObject:@(kCVPixelFormatType_32BGRA)
- forKey:(__bridge NSString *) kCVPixelBufferPixelFormatTypeKey];
- self.videoOutput.videoSettings = videoSettings;
- }
- }
- [self.session commitConfiguration];
- return YES;
- }
- - (void)startCaptureSession
- {
- if (!self.session.running) {
- [self.session startRunning];
- }
- }
- - (void)stopCaptureSession
- {
- if (self.session.running) {
- [self.session stopRunning];
- }
- if (self.captureInfo->isFastPath) {
- if (self.captureInfo->texture) {
- obs_enter_graphics();
- gs_texture_destroy(self.captureInfo->texture);
- obs_leave_graphics();
- self.captureInfo->texture = NULL;
- }
- if (self.captureInfo->currentSurface) {
- IOSurfaceDecrementUseCount(self.captureInfo->currentSurface);
- CFRelease(self.captureInfo->currentSurface);
- self.captureInfo->currentSurface = NULL;
- }
- if (self.captureInfo->previousSurface) {
- IOSurfaceDecrementUseCount(self.captureInfo->previousSurface);
- CFRelease(self.captureInfo->previousSurface);
- self.captureInfo->previousSurface = NULL;
- }
- } else {
- if (self.captureInfo->source) {
- obs_source_output_video(self.captureInfo->source, NULL);
- }
- }
- }
- - (BOOL)configureSessionWithPreset:(AVCaptureSessionPreset)preset withError:(NSError *__autoreleasing *)error
- {
- if (!self.deviceInput.device) {
- if (error) {
- NSDictionary *userInfo =
- @{NSLocalizedDescriptionKey: @"Unable to set session preset without capture device"};
- *error = [NSError errorWithDomain:self.errorDomain code:-108 userInfo:userInfo];
- }
- return NO;
- }
- if (![self.deviceInput.device supportsAVCaptureSessionPreset:preset]) {
- if (error) {
- NSDictionary *userInfo = @{
- NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Preset %@ not supported by device %@",
- [OBSAVCapture stringFromCapturePreset:preset],
- self.deviceInput.device.localizedName]
- };
- *error = [NSError errorWithDomain:self.errorDomain code:-201 userInfo:userInfo];
- }
- return NO;
- }
- if ([self.session canSetSessionPreset:preset]) {
- if (self.isDeviceLocked) {
- if ([preset isEqualToString:self.session.sessionPreset]) {
- if (self.deviceInput.device.activeFormat) {
- self.deviceInput.device.activeFormat = self.presetFormat.activeFormat;
- self.deviceInput.device.activeVideoMinFrameDuration = self.presetFormat.minFrameRate;
- self.deviceInput.device.activeVideoMaxFrameDuration = self.presetFormat.maxFrameRate;
- }
- self.presetFormat = nil;
- }
- [self.deviceInput.device unlockForConfiguration];
- self.isDeviceLocked = NO;
- }
- if ([self.session canSetSessionPreset:preset]) {
- self.session.sessionPreset = preset;
- }
- } else {
- if (error) {
- NSDictionary *userInfo = @{
- NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Preset %@ not supported by capture session",
- [OBSAVCapture stringFromCapturePreset:preset]]
- };
- *error = [NSError errorWithDomain:self.errorDomain code:-202 userInfo:userInfo];
- }
- return NO;
- }
- self.isPresetBased = YES;
- return YES;
- }
- - (BOOL)configureSession:(NSError *__autoreleasing *)error
- {
- OBSAVCaptureMediaFPS fps;
- if (!obs_data_get_frames_per_second(self.captureInfo->settings, "frame_rate", &fps, NULL)) {
- [self AVCaptureLog:LOG_DEBUG withFormat:@"No valid framerate found in settings"];
- return NO;
- }
- CMTime time = {.value = fps.denominator, .timescale = fps.numerator, .flags = 1};
- const char *selectedFormat = obs_data_get_string(self.captureInfo->settings, "supported_format");
- NSString *selectedFormatNSString = selectedFormat != NULL ? @(selectedFormat) : @"";
- AVCaptureDeviceFormat *format = nil;
- FourCharCode subtype;
- OBSAVCaptureColorSpace colorSpace;
- bool fpsSupported = false;
- if (![selectedFormatNSString isEqualToString:@""]) {
- for (AVCaptureDeviceFormat *formatCandidate in [self.deviceInput.device.formats reverseObjectEnumerator]) {
- if ([selectedFormatNSString isEqualToString:formatCandidate.obsPropertyListInternalRepresentation]) {
- CMFormatDescriptionRef formatDescription = formatCandidate.formatDescription;
- FourCharCode formatFourCC = CMFormatDescriptionGetMediaSubType(formatDescription);
- format = formatCandidate;
- subtype = formatFourCC;
- colorSpace = [OBSAVCapture colorspaceFromDescription:formatDescription];
- break;
- }
- }
- } else {
- //try to migrate from the legacy suite of properties
- int legacyVideoRange = (int) obs_data_get_int(self.captureInfo->settings, "video_range");
- int legacyInputFormat = (int) obs_data_get_int(self.captureInfo->settings, "input_format");
- int legacyColorSpace = (int) obs_data_get_int(self.captureInfo->settings, "color_space");
- CMVideoDimensions legacyDimensions = [OBSAVCapture legacyDimensionsFromSettings:self.captureInfo->settings];
- for (AVCaptureDeviceFormat *formatCandidate in [self.deviceInput.device.formats reverseObjectEnumerator]) {
- CMFormatDescriptionRef formatDescription = formatCandidate.formatDescription;
- CMVideoDimensions formatDimensions = CMVideoFormatDescriptionGetDimensions(formatDescription);
- int formatColorSpace = [OBSAVCapture colorspaceFromDescription:formatDescription];
- int formatInputFormat =
- [OBSAVCapture formatFromSubtype:CMFormatDescriptionGetMediaSubType(formatDescription)];
- int formatVideoRange = [OBSAVCapture isFullRangeFormat:formatInputFormat] ? VIDEO_RANGE_FULL
- : VIDEO_RANGE_PARTIAL;
- bool foundFormat = legacyVideoRange == formatVideoRange && legacyInputFormat == formatInputFormat &&
- legacyColorSpace == formatColorSpace &&
- legacyDimensions.width == formatDimensions.width &&
- legacyDimensions.height == formatDimensions.height;
- if (foundFormat) {
- format = formatCandidate;
- subtype = formatInputFormat;
- colorSpace = formatColorSpace;
- break;
- }
- }
- }
- if (!format) {
- [self AVCaptureLog:LOG_WARNING withFormat:@"Configured format not found on device"];
- return NO;
- }
- for (AVFrameRateRange *range in format.videoSupportedFrameRateRanges) {
- if (CMTimeCompare(range.maxFrameDuration, time) >= 0 && CMTimeCompare(range.minFrameDuration, time) <= 0) {
- fpsSupported = true;
- break;
- }
- }
- if (!fpsSupported) {
- OBSAVCaptureMediaFPS fallbackFPS = [OBSAVCapture fallbackFrameRateForFormat:format];
- if (fallbackFPS.denominator > 0 && fallbackFPS.numerator > 0) {
- [self AVCaptureLog:LOG_WARNING withFormat:@"Frame rate is not supported: %g FPS (%u/%u), \n"
- " falling back to value supported by device: %G FPS (%u/%u)",
- media_frames_per_second_to_fps(fps), fps.numerator,
- fps.denominator, media_frames_per_second_to_fps(fallbackFPS),
- fallbackFPS.numerator, fallbackFPS.denominator];
- obs_data_set_frames_per_second(self.captureInfo->settings, "frame_rate", fallbackFPS, NULL);
- time.value = fallbackFPS.denominator;
- time.timescale = fallbackFPS.numerator;
- } else {
- [self AVCaptureLog:LOG_WARNING
- withFormat:@"Frame rate is not supported: %g FPS (%u/%u), \n"
- " no supported fallback FPS found",
- media_frames_per_second_to_fps(fps), fps.numerator, fps.denominator];
- return NO;
- }
- }
- [self.session beginConfiguration];
- self.isDeviceLocked = [self.deviceInput.device lockForConfiguration:error];
- if (!self.isDeviceLocked) {
- [self AVCaptureLog:LOG_WARNING withFormat:@"Could not lock device for configuration"];
- return NO;
- }
- [self AVCaptureLog:LOG_INFO
- withFormat:@"Capturing '%@' (%@):\n"
- " Using Format : %@ \n"
- " FPS : %g (%u/%u)\n"
- " Frame Interval : %g\u00a0s\n",
- self.deviceInput.device.localizedName, self.deviceInput.device.uniqueID,
- format.obsPropertyListDescription, media_frames_per_second_to_fps(fps), fps.numerator,
- fps.denominator, media_frames_per_second_to_frame_interval(fps)];
- OBSAVCaptureVideoInfo newInfo = {.colorSpace = _videoInfo.colorSpace,
- .fourCC = _videoInfo.fourCC,
- .isValid = false};
- self.videoInfo = newInfo;
- self.captureInfo->configuredColorSpace = colorSpace;
- self.captureInfo->configuredFourCC = subtype;
- self.isPresetBased = NO;
- if (!self.presetFormat) {
- OBSAVCapturePresetInfo *presetInfo = [[OBSAVCapturePresetInfo alloc] init];
- presetInfo.activeFormat = self.deviceInput.device.activeFormat;
- presetInfo.minFrameRate = self.deviceInput.device.activeVideoMinFrameDuration;
- presetInfo.maxFrameRate = self.deviceInput.device.activeVideoMaxFrameDuration;
- self.presetFormat = presetInfo;
- }
- self.deviceInput.device.activeFormat = format;
- self.deviceInput.device.activeVideoMinFrameDuration = time;
- self.deviceInput.device.activeVideoMaxFrameDuration = time;
- [self.session commitConfiguration];
- return YES;
- }
- - (BOOL)updateSessionwithError:(NSError *__autoreleasing *)error
- {
- switch (self.captureInfo->lastError) {
- case OBSAVCaptureError_SampleBufferFormat:
- if (self.captureInfo->sampleBufferDescription) {
- FourCharCode mediaSubType =
- CMFormatDescriptionGetMediaSubType(self.captureInfo->sampleBufferDescription);
- [self AVCaptureLog:LOG_ERROR
- withFormat:@"Incompatible sample buffer format received for sync AVCapture source: %@ (0x%x)",
- [OBSAVCapture stringFromFourCharCode:mediaSubType], mediaSubType];
- }
- break;
- case OBSAVCaptureError_ColorSpace: {
- if (self.captureInfo->sampleBufferDescription) {
- FourCharCode mediaSubType =
- CMFormatDescriptionGetMediaSubType(self.captureInfo->sampleBufferDescription);
- BOOL isSampleBufferFullRange = [OBSAVCapture isFullRangeFormat:mediaSubType];
- OBSAVCaptureColorSpace sampleBufferColorSpace =
- [OBSAVCapture colorspaceFromDescription:self.captureInfo->sampleBufferDescription];
- OBSAVCaptureVideoRange sampleBufferRangeType = isSampleBufferFullRange ? VIDEO_RANGE_FULL
- : VIDEO_RANGE_PARTIAL;
- [self AVCaptureLog:LOG_ERROR
- withFormat:@"Failed to get colorspace parameters for colorspace %u and range %u",
- sampleBufferColorSpace, sampleBufferRangeType];
- }
- break;
- default:
- self.captureInfo->lastError = OBSAVCaptureError_NoError;
- self.captureInfo->sampleBufferDescription = NULL;
- break;
- }
- }
- switch (self.captureInfo->lastAudioError) {
- case OBSAVCaptureError_AudioBuffer: {
- [OBSAVCapture AVCaptureLog:LOG_ERROR
- withFormat:@"Unable to retrieve required AudioBufferList size from sample buffer."];
- break;
- }
- default:
- self.captureInfo->lastAudioError = OBSAVCaptureError_NoError;
- break;
- }
- NSString *newDeviceUUID = [OBSAVCapture stringFromSettings:self.captureInfo->settings withSetting:@"device"];
- NSString *presetName = [OBSAVCapture stringFromSettings:self.captureInfo->settings withSetting:@"preset"];
- BOOL isPresetEnabled = obs_data_get_bool(self.captureInfo->settings, "use_preset");
- BOOL updateSession = YES;
- if (![self.deviceUUID isEqualToString:newDeviceUUID]) {
- if (![self switchCaptureDevice:newDeviceUUID withError:error]) {
- obs_source_update_properties(self.captureInfo->source);
- return NO;
- }
- } else if (self.isPresetBased && isPresetEnabled && [presetName isEqualToString:self.session.sessionPreset]) {
- updateSession = NO;
- }
- if (updateSession) {
- if (isPresetEnabled) {
- [self configureSessionWithPreset:presetName withError:error];
- } else {
- if (![self configureSession:error]) {
- obs_source_update_properties(self.captureInfo->source);
- return NO;
- }
- }
- __weak OBSAVCapture *weakSelf = self;
- dispatch_async(self.sessionQueue, ^{
- [weakSelf startCaptureSession];
- });
- }
- BOOL isAudioAvailable = [self.deviceInput.device hasMediaType:AVMediaTypeAudio] ||
- [self.deviceInput.device hasMediaType:AVMediaTypeMuxed];
- obs_source_set_audio_active(self.captureInfo->source, isAudioAvailable);
- if (!self.isFastPath) {
- BOOL isBufferingEnabled = obs_data_get_bool(self.captureInfo->settings, "buffering");
- obs_source_set_async_unbuffered(self.captureInfo->source, !isBufferingEnabled);
- }
- return YES;
- }
- #pragma mark - OBS Settings Helpers
- + (CMVideoDimensions)legacyDimensionsFromSettings:(void *)settings
- {
- CMVideoDimensions zero = {0};
- NSString *jsonString = [OBSAVCapture stringFromSettings:settings withSetting:@"resolution"];
- NSDictionary *data = [NSJSONSerialization JSONObjectWithData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]
- options:0
- error:nil];
- if (data.count == 0) {
- return zero;
- }
- NSInteger width = [[data objectForKey:@"width"] intValue];
- NSInteger height = [[data objectForKey:@"height"] intValue];
- if (!width || !height) {
- return zero;
- }
- CMVideoDimensions dimensions = {.width = (int32_t) clamp_Uint(width, 0, UINT32_MAX),
- .height = (int32_t) clamp_Uint(height, 0, UINT32_MAX)};
- return dimensions;
- }
- + (OBSAVCaptureMediaFPS)fallbackFrameRateForFormat:(AVCaptureDeviceFormat *)format
- {
- struct obs_video_info video_info;
- bool result = obs_get_video_info(&video_info);
- double outputFPS = result ? ((double) video_info.fps_num / (double) video_info.fps_den) : 0;
- double closestUpTo = 0;
- double closestAbove = DBL_MAX;
- OBSAVCaptureMediaFPS closestUpToMFPS = {};
- OBSAVCaptureMediaFPS closestAboveMFPS = {};
- for (AVFrameRateRange *range in format.videoSupportedFrameRateRanges) {
- if (range.maxFrameRate > closestUpTo && range.maxFrameRate <= outputFPS) {
- closestUpTo = range.maxFrameRate;
- closestUpToMFPS.numerator = (uint32_t) clamp_Uint(range.minFrameDuration.timescale, 0, UINT32_MAX);
- closestUpToMFPS.denominator = (uint32_t) clamp_Uint(range.minFrameDuration.value, 0, UINT32_MAX);
- }
- if (range.minFrameRate > outputFPS && range.minFrameRate < closestAbove) {
- closestAbove = range.minFrameRate;
- closestAboveMFPS.numerator = (uint32_t) clamp_Uint(range.maxFrameDuration.timescale, 0, UINT32_MAX);
- closestAboveMFPS.denominator = (uint32_t) clamp_Uint(range.maxFrameDuration.value, 0, UINT32_MAX);
- }
- }
- if (closestUpTo > 0) {
- return closestUpToMFPS;
- } else {
- return closestAboveMFPS;
- }
- }
- + (NSString *)aspectRatioStringFromDimensions:(CMVideoDimensions)dimensions
- {
- if (dimensions.width <= 0 || dimensions.height <= 0) {
- return @"";
- }
- double divisor = (double) gcd(dimensions.width, dimensions.height);
- if (divisor <= 50) {
- if (dimensions.width > dimensions.height) {
- double x = (double) dimensions.width / (double) dimensions.height;
- return [NSString stringWithFormat:@"%.2f:1", x];
- } else {
- double y = (double) dimensions.height / (double) dimensions.width;
- return [NSString stringWithFormat:@"1:%.2f", y];
- }
- } else {
- SInt32 x = dimensions.width / (SInt32) divisor;
- SInt32 y = dimensions.height / (SInt32) divisor;
- if (x == 8 && y == 5) {
- x = 16;
- y = 10;
- }
- return [NSString stringWithFormat:@"%i:%i", x, y];
- }
- }
- + (NSString *)stringFromSettings:(void *)settings withSetting:(NSString *)setting
- {
- return [OBSAVCapture stringFromSettings:settings withSetting:setting withDefault:@""];
- }
- + (NSString *)stringFromSettings:(void *)settings withSetting:(NSString *)setting withDefault:(NSString *)defaultValue
- {
- NSString *result;
- if (settings) {
- const char *setting_value = obs_data_get_string(settings, setting.UTF8String);
- if (!setting_value) {
- result = [NSString stringWithString:defaultValue];
- } else {
- result = @(setting_value);
- }
- } else {
- result = [NSString stringWithString:defaultValue];
- }
- return result;
- }
- + (NSString *)effectsWarningForDevice:(AVCaptureDevice *)device
- {
- int effectsCount = 0;
- NSString *effectWarning = nil;
- if (@available(macOS 12.0, *)) {
- if (device.portraitEffectActive) {
- effectWarning = @"Warning.Effect.Portrait";
- effectsCount++;
- }
- }
- if (@available(macOS 12.3, *)) {
- if (device.centerStageActive) {
- effectWarning = @"Warning.Effect.CenterStage";
- effectsCount++;
- }
- }
- if (@available(macOS 13.0, *)) {
- if (device.studioLightActive) {
- effectWarning = @"Warning.Effect.StudioLight";
- effectsCount++;
- }
- }
- if (@available(macOS 14.0, *)) {
- /// Reaction effects do not follow the same paradigm as other effects in terms of checking whether they are active. According to Apple, this is because a device instance property `reactionEffectsActive` would have been ambiguous (conflicting with whether a reaction is currently rendering).
- ///
- /// Instead, Apple exposes the `AVCaptureDevice.reactionEffectGesturesEnabled` class property (an equivalent exists for all other effects, but is hidden/private) to tell us whether the effect is enabled application-wide, as well as the `device.canPerformReactionEffects` instance property to tell us whether the device's active format currently supports the effect.
- ///
- /// The logical conjunction of these two properties tells us whether the effect is 'active'; i.e. whether putting our thumbs inside the video frame will make fireworks appear. The device instance properties for other effects are a convenience 'shorthand' for this private class/instance property combination.
- if (device.canPerformReactionEffects && AVCaptureDevice.reactionEffectGesturesEnabled) {
- effectWarning = @"Warning.Effect.Reactions";
- effectsCount++;
- }
- }
- #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 150000
- if (@available(macOS 15.0, *)) {
- if (device.backgroundReplacementActive) {
- effectWarning = @"Warning.Effect.BackgroundReplacement";
- effectsCount++;
- }
- }
- #endif
- if (effectsCount > 1) {
- effectWarning = @"Warning.Effect.Multiple";
- }
- return effectWarning;
- }
- #pragma mark - Format Conversion Helpers
- + (NSString *)stringFromSubType:(FourCharCode)subtype
- {
- switch (subtype) {
- case kCVPixelFormatType_422YpCbCr8:
- return @"UYVY (2vuy)";
- case kCVPixelFormatType_422YpCbCr8_yuvs:
- return @"YUY2 (yuvs)";
- case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
- return @"NV12 (420v)";
- case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
- return @"NV12 (420f)";
- case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
- return @"P010 (xf20)";
- case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
- return @"P010 (x420)";
- case kCVPixelFormatType_32ARGB:
- return @"ARGB - 32ARGB";
- case kCVPixelFormatType_32BGRA:
- return @"BGRA - 32BGRA";
- case kCMVideoCodecType_Animation:
- return @"Apple Animation";
- case kCMVideoCodecType_Cinepak:
- return @"Cinepak";
- case kCMVideoCodecType_JPEG:
- return @"JPEG";
- case kCMVideoCodecType_JPEG_OpenDML:
- return @"MJPEG - JPEG OpenDML";
- case kCMVideoCodecType_SorensonVideo:
- return @"Sorenson Video";
- case kCMVideoCodecType_SorensonVideo3:
- return @"Sorenson Video 3";
- case kCMVideoCodecType_H263:
- return @"H.263";
- case kCMVideoCodecType_H264:
- return @"H.264";
- case kCMVideoCodecType_MPEG4Video:
- return @"MPEG-4";
- case kCMVideoCodecType_MPEG2Video:
- return @"MPEG-2";
- case kCMVideoCodecType_MPEG1Video:
- return @"MPEG-1";
- case kCMVideoCodecType_DVCNTSC:
- return @"DV NTSC";
- case kCMVideoCodecType_DVCPAL:
- return @"DV PAL";
- case kCMVideoCodecType_DVCProPAL:
- return @"Panasonic DVCPro Pal";
- case kCMVideoCodecType_DVCPro50NTSC:
- return @"Panasonic DVCPro-50 NTSC";
- case kCMVideoCodecType_DVCPro50PAL:
- return @"Panasonic DVCPro-50 PAL";
- case kCMVideoCodecType_DVCPROHD720p60:
- return @"Panasonic DVCPro-HD 720p60";
- case kCMVideoCodecType_DVCPROHD720p50:
- return @"Panasonic DVCPro-HD 720p50";
- case kCMVideoCodecType_DVCPROHD1080i60:
- return @"Panasonic DVCPro-HD 1080i60";
- case kCMVideoCodecType_DVCPROHD1080i50:
- return @"Panasonic DVCPro-HD 1080i50";
- case kCMVideoCodecType_DVCPROHD1080p30:
- return @"Panasonic DVCPro-HD 1080p30";
- case kCMVideoCodecType_DVCPROHD1080p25:
- return @"Panasonic DVCPro-HD 1080p25";
- case kCMVideoCodecType_AppleProRes4444:
- return @"Apple ProRes 4444";
- case kCMVideoCodecType_AppleProRes422HQ:
- return @"Apple ProRes 422 HQ";
- case kCMVideoCodecType_AppleProRes422:
- return @"Apple ProRes 422";
- case kCMVideoCodecType_AppleProRes422LT:
- return @"Apple ProRes 422 LT";
- case kCMVideoCodecType_AppleProRes422Proxy:
- return @"Apple ProRes 422 Proxy";
- default:
- return @"Unknown";
- }
- }
- + (NSString *)stringFromColorspace:(enum video_colorspace)colorspace
- {
- switch (colorspace) {
- case VIDEO_CS_DEFAULT:
- return @"Default";
- case VIDEO_CS_601:
- return @"CS 601";
- case VIDEO_CS_709:
- return @"CS 709";
- case VIDEO_CS_SRGB:
- return @"sRGB";
- case VIDEO_CS_2100_PQ:
- return @"CS 2100 (PQ)";
- case VIDEO_CS_2100_HLG:
- return @"CS 2100 (HLG)";
- default:
- return @"Unknown";
- }
- }
- + (NSString *)stringFromVideoRange:(enum video_range_type)videoRange
- {
- switch (videoRange) {
- case VIDEO_RANGE_FULL:
- return @"Full";
- case VIDEO_RANGE_PARTIAL:
- return @"Partial";
- case VIDEO_RANGE_DEFAULT:
- return @"Default";
- }
- }
- + (NSString *)stringFromCapturePreset:(AVCaptureSessionPreset)preset
- {
- NSDictionary *presetDescriptions = @{
- AVCaptureSessionPresetLow: @"Low",
- AVCaptureSessionPresetMedium: @"Medium",
- AVCaptureSessionPresetHigh: @"High",
- AVCaptureSessionPreset320x240: @"320x240",
- AVCaptureSessionPreset352x288: @"352x288",
- AVCaptureSessionPreset640x480: @"640x480",
- AVCaptureSessionPreset960x540: @"960x460",
- AVCaptureSessionPreset1280x720: @"1280x720",
- AVCaptureSessionPreset1920x1080: @"1920x1080",
- AVCaptureSessionPreset3840x2160: @"3840x2160",
- };
- NSString *presetDescription = [presetDescriptions objectForKey:preset];
- if (!presetDescription) {
- return [NSString stringWithFormat:@"Unknown (%@)", preset];
- } else {
- return presetDescription;
- }
- }
- + (NSString *)stringFromFourCharCode:(OSType)fourCharCode
- {
- char cString[5] = {(fourCharCode >> 24) & 0xFF, (fourCharCode >> 16) & 0xFF, (fourCharCode >> 8) & 0xFF,
- fourCharCode & 0xFF, 0};
- NSString *codeString = @(cString);
- return codeString;
- }
- + (FourCharCode)fourCharCodeFromString:(NSString *)codeString
- {
- FourCharCode fourCharCode;
- const char *cString = codeString.UTF8String;
- fourCharCode = (cString[0] << 24) | (cString[1] << 16) | (cString[2] << 8) | cString[3];
- return fourCharCode;
- }
- + (BOOL)isValidColorspace:(enum video_colorspace)colorspace
- {
- switch (colorspace) {
- case VIDEO_CS_DEFAULT:
- case VIDEO_CS_601:
- case VIDEO_CS_709:
- return YES;
- default:
- return NO;
- }
- }
- + (BOOL)isValidVideoRange:(enum video_range_type)videoRange
- {
- switch (videoRange) {
- case VIDEO_RANGE_DEFAULT:
- case VIDEO_RANGE_PARTIAL:
- case VIDEO_RANGE_FULL:
- return YES;
- default:
- return NO;
- }
- }
- + (BOOL)isFullRangeFormat:(FourCharCode)pixelFormat
- {
- switch (pixelFormat) {
- case kCVPixelFormatType_420YpCbCr8PlanarFullRange:
- case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
- case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
- case kCVPixelFormatType_422YpCbCr8FullRange:
- return YES;
- default:
- return NO;
- }
- }
- + (OBSAVCaptureVideoFormat)formatFromSubtype:(FourCharCode)subtype
- {
- switch (subtype) {
- case kCVPixelFormatType_422YpCbCr8:
- return VIDEO_FORMAT_UYVY;
- case kCVPixelFormatType_422YpCbCr8_yuvs:
- return VIDEO_FORMAT_YUY2;
- case kCVPixelFormatType_32BGRA:
- return VIDEO_FORMAT_BGRA;
- case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
- case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
- return VIDEO_FORMAT_NV12;
- case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
- case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
- return VIDEO_FORMAT_P010;
- default:
- return VIDEO_FORMAT_NONE;
- }
- }
- + (FourCharCode)fourCharCodeFromFormat:(OBSAVCaptureVideoFormat)format withRange:(enum video_range_type)videoRange
- {
- switch (format) {
- case VIDEO_FORMAT_UYVY:
- return kCVPixelFormatType_422YpCbCr8;
- case VIDEO_FORMAT_YUY2:
- return kCVPixelFormatType_422YpCbCr8_yuvs;
- case VIDEO_FORMAT_NV12:
- if (videoRange == VIDEO_RANGE_FULL) {
- return kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;
- } else {
- return kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
- }
- case VIDEO_FORMAT_P010:
- if (videoRange == VIDEO_RANGE_FULL) {
- return kCVPixelFormatType_420YpCbCr10BiPlanarFullRange;
- } else {
- return kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange;
- }
- case VIDEO_FORMAT_BGRA:
- return kCVPixelFormatType_32BGRA;
- default:
- return 0;
- }
- }
- + (FourCharCode)fourCharCodeFromFormat:(OBSAVCaptureVideoFormat)format
- {
- return [OBSAVCapture fourCharCodeFromFormat:format withRange:VIDEO_RANGE_PARTIAL];
- }
- + (NSString *)frameRateDescription:(NSArray<AVFrameRateRange *> *)ranges
- {
- // The videoSupportedFrameRateRanges property seems to provide frame rate ranges in this order, but since that
- // ordering does not seem to be guaranteed, ensure they are sorted anyway.
- NSArray<AVFrameRateRange *> *sortedRangesDescending = [ranges
- sortedArrayUsingComparator:^NSComparisonResult(AVFrameRateRange *_Nonnull lhs, AVFrameRateRange *_Nonnull rhs) {
- if (lhs.maxFrameRate > rhs.maxFrameRate) {
- return NSOrderedAscending;
- } else if (lhs.maxFrameRate < rhs.maxFrameRate) {
- return NSOrderedDescending;
- }
- if (lhs.minFrameRate > rhs.minFrameRate) {
- return NSOrderedAscending;
- } else if (lhs.minFrameRate < rhs.minFrameRate) {
- return NSOrderedDescending;
- }
- return NSOrderedSame;
- }];
- NSString *frameRateDescription;
- NSMutableArray *frameRateDescriptions = [[NSMutableArray alloc] initWithCapacity:ranges.count];
- for (AVFrameRateRange *range in [sortedRangesDescending reverseObjectEnumerator]) {
- double minFrameRate = round(range.minFrameRate * 100) / 100;
- double maxFrameRate = round(range.maxFrameRate * 100) / 100;
- if (minFrameRate == maxFrameRate) {
- if (fmod(minFrameRate, 1.0) == 0 && fmod(maxFrameRate, 1.0) == 0) {
- [frameRateDescriptions addObject:[NSString stringWithFormat:@"%.0f", maxFrameRate]];
- } else {
- [frameRateDescriptions addObject:[NSString stringWithFormat:@"%.2f", maxFrameRate]];
- }
- } else {
- if (fmod(minFrameRate, 1.0) == 0 && fmod(maxFrameRate, 1.0) == 0) {
- [frameRateDescriptions addObject:[NSString stringWithFormat:@"%.0f-%.0f", minFrameRate, maxFrameRate]];
- } else {
- [frameRateDescriptions addObject:[NSString stringWithFormat:@"%.2f-%.2f", minFrameRate, maxFrameRate]];
- }
- }
- }
- if (frameRateDescriptions.count > 0 && frameRateDescriptions.count <= kMaxFrameRateRangesInDescription) {
- frameRateDescription = [frameRateDescriptions componentsJoinedByString:@", "];
- frameRateDescription = [frameRateDescription stringByAppendingString:@" FPS"];
- } else if (frameRateDescriptions.count > kMaxFrameRateRangesInDescription) {
- frameRateDescription =
- [NSString stringWithFormat:@"%.0f-%.0f FPS (%lu values)", sortedRangesDescending.lastObject.minFrameRate,
- sortedRangesDescending.firstObject.maxFrameRate, sortedRangesDescending.count];
- }
- return frameRateDescription;
- }
- + (OBSAVCaptureColorSpace)colorspaceFromDescription:(CMFormatDescriptionRef)description
- {
- CFPropertyListRef matrix = CMFormatDescriptionGetExtension(description, kCMFormatDescriptionExtension_YCbCrMatrix);
- if (!matrix) {
- return VIDEO_CS_DEFAULT;
- }
- CFComparisonResult is601 = CFStringCompare(matrix, kCVImageBufferYCbCrMatrix_ITU_R_601_4, 0);
- CFComparisonResult is709 = CFStringCompare(matrix, kCVImageBufferYCbCrMatrix_ITU_R_709_2, 0);
- CFComparisonResult is2020 = CFStringCompare(matrix, kCVImageBufferYCbCrMatrix_ITU_R_2020, 0);
- if (is601 == kCFCompareEqualTo) {
- return VIDEO_CS_601;
- } else if (is709 == kCFCompareEqualTo) {
- return VIDEO_CS_709;
- } else if (is2020 == kCFCompareEqualTo) {
- CFPropertyListRef transferFunction =
- CMFormatDescriptionGetExtension(description, kCMFormatDescriptionExtension_TransferFunction);
- if (!matrix) {
- return VIDEO_CS_DEFAULT;
- }
- CFComparisonResult isPQ = CFStringCompare(transferFunction, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ, 0);
- CFComparisonResult isHLG = CFStringCompare(transferFunction, kCVImageBufferTransferFunction_ITU_R_2100_HLG, 0);
- if (isPQ == kCFCompareEqualTo) {
- return VIDEO_CS_2100_PQ;
- } else if (isHLG == kCFCompareEqualTo) {
- return VIDEO_CS_2100_HLG;
- }
- }
- return VIDEO_CS_DEFAULT;
- }
- #pragma mark - Notification Handlers
- - (void)deviceConnected:(NSNotification *)notification
- {
- AVCaptureDevice *device = notification.object;
- if (!device) {
- return;
- }
- if (![[device uniqueID] isEqualTo:self.deviceUUID]) {
- obs_source_update_properties(self.captureInfo->source);
- return;
- }
- if (self.deviceInput.device) {
- [self AVCaptureLog:LOG_INFO withFormat:@"Received connect event with active device '%@' (UUID %@)",
- self.deviceInput.device.localizedName, self.deviceInput.device.uniqueID];
- obs_source_update_properties(self.captureInfo->source);
- return;
- }
- [self AVCaptureLog:LOG_INFO
- withFormat:@"Received connect event for device '%@' (UUID %@)", device.localizedName, device.uniqueID];
- NSError *error;
- NSString *presetName = [OBSAVCapture stringFromSettings:self.captureInfo->settings withSetting:@"preset"];
- BOOL isPresetEnabled = obs_data_get_bool(self.captureInfo->settings, "use_preset");
- BOOL isFastPath = self.captureInfo->isFastPath;
- if ([self switchCaptureDevice:device.uniqueID withError:&error]) {
- BOOL success;
- if (isPresetEnabled && !isFastPath) {
- success = [self configureSessionWithPreset:presetName withError:&error];
- } else {
- success = [self configureSession:&error];
- }
- if (success) {
- dispatch_async(self.sessionQueue, ^{
- [self startCaptureSession];
- });
- } else {
- [self AVCaptureLog:LOG_ERROR withFormat:error.localizedDescription];
- }
- } else {
- [self AVCaptureLog:LOG_ERROR withFormat:error.localizedDescription];
- }
- obs_source_update_properties(self.captureInfo->source);
- }
- - (void)deviceDisconnected:(NSNotification *)notification
- {
- AVCaptureDevice *device = notification.object;
- if (!device) {
- return;
- }
- if (![[device uniqueID] isEqualTo:self.deviceUUID]) {
- obs_source_update_properties(self.captureInfo->source);
- return;
- }
- if (!self.deviceInput.device) {
- [self AVCaptureLog:LOG_ERROR withFormat:@"Received disconnect event for inactive device '%@' (UUID %@)",
- device.localizedName, device.uniqueID];
- obs_source_update_properties(self.captureInfo->source);
- return;
- }
- [self AVCaptureLog:LOG_INFO
- withFormat:@"Received disconnect event for device '%@' (UUID %@)", device.localizedName, device.uniqueID];
- __weak OBSAVCapture *weakSelf = self;
- dispatch_async(self.sessionQueue, ^{
- OBSAVCapture *instance = weakSelf;
- [instance stopCaptureSession];
- [instance.session removeInput:instance.deviceInput];
- instance.deviceInput = nil;
- instance = nil;
- });
- obs_source_update_properties(self.captureInfo->source);
- }
- #pragma mark - AVCapture Delegate Methods
- - (void)captureOutput:(AVCaptureOutput *)output
- didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
- fromConnection:(AVCaptureConnection *)connection
- {
- return;
- }
- - (void)captureOutput:(AVCaptureOutput *)output
- didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
- fromConnection:(AVCaptureConnection *)connection
- {
- CMItemCount sampleCount = CMSampleBufferGetNumSamples(sampleBuffer);
- if (!_captureInfo || sampleCount < 1) {
- return;
- }
- CMTime presentationTimeStamp = CMSampleBufferGetOutputPresentationTimeStamp(sampleBuffer);
- CMTime presentationNanoTimeStamp = CMTimeConvertScale(presentationTimeStamp, 1E9, kCMTimeRoundingMethod_Default);
- CMFormatDescriptionRef description = CMSampleBufferGetFormatDescription(sampleBuffer);
- CMMediaType mediaType = CMFormatDescriptionGetMediaType(description);
- switch (mediaType) {
- case kCMMediaType_Video: {
- CMVideoDimensions sampleBufferDimensions = CMVideoFormatDescriptionGetDimensions(description);
- CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
- FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(description);
- OBSAVCaptureVideoInfo newInfo = {.fourCC = _videoInfo.fourCC,
- .colorSpace = _videoInfo.colorSpace,
- .isValid = false};
- BOOL usePreset = obs_data_get_bool(_captureInfo->settings, "use_preset");
- if (_isFastPath) {
- if (mediaSubType != kCVPixelFormatType_32BGRA &&
- mediaSubType != kCVPixelFormatType_ARGB2101010LEPacked) {
- _captureInfo->lastError = OBSAVCaptureError_SampleBufferFormat;
- CMFormatDescriptionCreate(kCFAllocatorDefault, mediaType, mediaSubType, NULL,
- &_captureInfo->sampleBufferDescription);
- obs_source_update_properties(_captureInfo->source);
- break;
- } else {
- _captureInfo->lastError = OBSAVCaptureError_NoError;
- _captureInfo->sampleBufferDescription = NULL;
- }
- CVPixelBufferLockBaseAddress(imageBuffer, 0);
- IOSurfaceRef frameSurface = CVPixelBufferGetIOSurface(imageBuffer);
- CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
- IOSurfaceRef previousSurface = NULL;
- if (frameSurface && !pthread_mutex_lock(&_captureInfo->mutex)) {
- NSRect frameSize = _captureInfo->frameSize;
- if (frameSize.size.width != sampleBufferDimensions.width ||
- frameSize.size.height != sampleBufferDimensions.height) {
- frameSize = CGRectMake(0, 0, sampleBufferDimensions.width, sampleBufferDimensions.height);
- }
- previousSurface = _captureInfo->currentSurface;
- _captureInfo->currentSurface = frameSurface;
- CFRetain(_captureInfo->currentSurface);
- IOSurfaceIncrementUseCount(_captureInfo->currentSurface);
- pthread_mutex_unlock(&_captureInfo->mutex);
- newInfo.isValid = true;
- if (_videoInfo.isValid != newInfo.isValid) {
- obs_source_update_properties(_captureInfo->source);
- }
- _captureInfo->frameSize = frameSize;
- _videoInfo = newInfo;
- }
- if (previousSurface) {
- IOSurfaceDecrementUseCount(previousSurface);
- CFRelease(previousSurface);
- }
- break;
- } else {
- OBSAVCaptureVideoFrame *frame = _captureInfo->videoFrame;
- frame->timestamp = presentationNanoTimeStamp.value;
- enum video_format videoFormat = [OBSAVCapture formatFromSubtype:mediaSubType];
- if (videoFormat == VIDEO_FORMAT_NONE) {
- _captureInfo->lastError = OBSAVCaptureError_SampleBufferFormat;
- CMFormatDescriptionCreate(kCFAllocatorDefault, mediaType, mediaSubType, NULL,
- &_captureInfo->sampleBufferDescription);
- } else {
- _captureInfo->lastError = OBSAVCaptureError_NoError;
- _captureInfo->sampleBufferDescription = NULL;
- #ifdef DEBUG
- if (frame->format != VIDEO_FORMAT_NONE && frame->format != videoFormat) {
- [self AVCaptureLog:LOG_DEBUG
- withFormat:@"Switching fourcc: '%@' (0x%x) -> '%@' (0x%x)",
- [OBSAVCapture stringFromFourCharCode:frame->format], frame -> format,
- [OBSAVCapture stringFromFourCharCode:mediaSubType], mediaSubType];
- }
- #endif
- bool isFrameYuv = format_is_yuv(frame->format);
- bool isSampleBufferYuv = format_is_yuv(videoFormat);
- frame->format = videoFormat;
- frame->width = sampleBufferDimensions.width;
- frame->height = sampleBufferDimensions.height;
- BOOL isSampleBufferFullRange = [OBSAVCapture isFullRangeFormat:mediaSubType];
- if (isSampleBufferYuv) {
- OBSAVCaptureColorSpace sampleBufferColorSpace =
- [OBSAVCapture colorspaceFromDescription:description];
- OBSAVCaptureVideoRange sampleBufferRangeType = isSampleBufferFullRange ? VIDEO_RANGE_FULL
- : VIDEO_RANGE_PARTIAL;
- BOOL isColorSpaceMatching = NO;
- SInt64 configuredColorSpace = _captureInfo->configuredColorSpace;
- if (usePreset) {
- isColorSpaceMatching = sampleBufferColorSpace == _videoInfo.colorSpace;
- } else {
- isColorSpaceMatching = configuredColorSpace == _videoInfo.colorSpace;
- }
- BOOL isFourCCMatching = NO;
- SInt64 configuredFourCC = _captureInfo->configuredFourCC;
- if (usePreset) {
- isFourCCMatching = mediaSubType == _videoInfo.fourCC;
- } else {
- isFourCCMatching = configuredFourCC == _videoInfo.fourCC;
- }
- if (isColorSpaceMatching && isFourCCMatching) {
- newInfo.isValid = true;
- } else {
- frame->full_range = isSampleBufferFullRange;
- bool success = video_format_get_parameters_for_format(
- sampleBufferColorSpace, sampleBufferRangeType, frame->format, frame->color_matrix,
- frame->color_range_min, frame->color_range_max);
- if (!success) {
- _captureInfo->lastError = OBSAVCaptureError_ColorSpace;
- CMFormatDescriptionCreate(kCFAllocatorDefault, mediaType, mediaSubType, NULL,
- &_captureInfo->sampleBufferDescription);
- newInfo.isValid = false;
- } else {
- newInfo.colorSpace = sampleBufferColorSpace;
- newInfo.fourCC = mediaSubType;
- newInfo.isValid = true;
- }
- }
- } else if (!isFrameYuv && !isSampleBufferYuv) {
- newInfo.isValid = true;
- }
- }
- if (newInfo.isValid != _videoInfo.isValid) {
- obs_source_update_properties(_captureInfo->source);
- }
- _videoInfo = newInfo;
- if (newInfo.isValid) {
- CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
- if (!CVPixelBufferIsPlanar(imageBuffer)) {
- frame->linesize[0] = (UInt32) CVPixelBufferGetBytesPerRow(imageBuffer);
- frame->data[0] = CVPixelBufferGetBaseAddress(imageBuffer);
- } else {
- size_t planeCount = CVPixelBufferGetPlaneCount(imageBuffer);
- for (size_t i = 0; i < planeCount; i++) {
- frame->linesize[i] = (UInt32) CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, i);
- frame->data[i] = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, i);
- }
- }
- obs_source_output_video(_captureInfo->source, frame);
- CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
- } else {
- obs_source_output_video(_captureInfo->source, NULL);
- }
- break;
- }
- }
- case kCMMediaType_Audio: {
- size_t requiredBufferListSize;
- OSStatus status = noErr;
- status = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
- sampleBuffer, &requiredBufferListSize, NULL, 0, NULL, NULL,
- kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, NULL);
- if (status != noErr) {
- _captureInfo->lastAudioError = OBSAVCaptureError_AudioBuffer;
- obs_source_update_properties(_captureInfo->source);
- break;
- }
- AudioBufferList *bufferList = (AudioBufferList *) malloc(requiredBufferListSize);
- CMBlockBufferRef blockBuffer = NULL;
- OSStatus error = noErr;
- error = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
- sampleBuffer, NULL, bufferList, requiredBufferListSize, kCFAllocatorSystemDefault,
- kCFAllocatorSystemDefault, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer);
- if (error == noErr) {
- _captureInfo->lastAudioError = OBSAVCaptureError_NoError;
- OBSAVCaptureAudioFrame *audio = _captureInfo->audioFrame;
- for (size_t i = 0; i < bufferList->mNumberBuffers; i++) {
- audio->data[i] = bufferList->mBuffers[i].mData;
- }
- audio->timestamp = presentationNanoTimeStamp.value;
- audio->frames = (uint32_t) CMSampleBufferGetNumSamples(sampleBuffer);
- const AudioStreamBasicDescription *basicDescription =
- CMAudioFormatDescriptionGetStreamBasicDescription(description);
- audio->samples_per_sec = (uint32_t) basicDescription->mSampleRate;
- audio->speakers = (enum speaker_layout) basicDescription->mChannelsPerFrame;
- switch (basicDescription->mBitsPerChannel) {
- case 8:
- audio->format = AUDIO_FORMAT_U8BIT;
- break;
- case 16:
- audio->format = AUDIO_FORMAT_16BIT;
- break;
- case 32:
- audio->format = AUDIO_FORMAT_32BIT;
- break;
- default:
- audio->format = AUDIO_FORMAT_UNKNOWN;
- break;
- }
- obs_source_output_audio(_captureInfo->source, audio);
- } else {
- _captureInfo->lastAudioError = OBSAVCaptureError_AudioBuffer;
- obs_source_output_audio(_captureInfo->source, NULL);
- }
- if (blockBuffer != NULL) {
- CFRelease(blockBuffer);
- }
- if (bufferList != NULL) {
- free(bufferList);
- bufferList = NULL;
- }
- break;
- }
- default:
- break;
- }
- }
- #pragma mark - Log Helpers
- - (void)AVCaptureLog:(int)logLevel withFormat:(NSString *)format, ...
- {
- va_list args;
- va_start(args, format);
- NSString *logMessage = [[NSString alloc] initWithFormat:format arguments:args];
- va_end(args);
- const char *name_value = obs_source_get_name(self.captureInfo->source);
- NSString *sourceName = @((name_value) ? name_value : "");
- blog(logLevel, "%s: %s", sourceName.UTF8String, logMessage.UTF8String);
- }
- + (void)AVCaptureLog:(int)logLevel withFormat:(NSString *)format, ...
- {
- va_list args;
- va_start(args, format);
- NSString *logMessage = [[NSString alloc] initWithFormat:format arguments:args];
- va_end(args);
- blog(logLevel, "%s", logMessage.UTF8String);
- }
- @end
|