| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513 |
- //
- // 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 (@available(macOS 15.0, *)) {
- if (device.backgroundReplacementActive) {
- effectWarning = @"Warning.Effect.BackgroundReplacement";
- effectsCount++;
- }
- }
- 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
|