OBSAVCapture.m 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376
  1. //
  2. // OBSAVCapture.m
  3. // mac-avcapture
  4. //
  5. // Created by Patrick Heyer on 2023-03-07.
  6. //
  7. #import "OBSAVCapture.h"
  8. @implementation OBSAVCapture
  9. - (instancetype)init
  10. {
  11. return [self initWithCaptureInfo:nil];
  12. }
  13. - (instancetype)initWithCaptureInfo:(OBSAVCaptureInfo *)capture_info
  14. {
  15. self = [super init];
  16. if (self) {
  17. CMIOObjectPropertyAddress propertyAddress = {kCMIOHardwarePropertyAllowScreenCaptureDevices,
  18. kCMIOObjectPropertyScopeGlobal, kCMIOObjectPropertyElementMaster};
  19. UInt32 allow = 1;
  20. CMIOObjectSetPropertyData(kCMIOObjectSystemObject, &propertyAddress, 0, NULL, sizeof(allow), &allow);
  21. _errorDomain = @"com.obsproject.obs-studio.av-capture";
  22. _presetList = @{
  23. AVCaptureSessionPresetLow: @"Low",
  24. AVCaptureSessionPresetMedium: @"Medium",
  25. AVCaptureSessionPresetHigh: @"High",
  26. AVCaptureSessionPreset320x240: @"320x240",
  27. AVCaptureSessionPreset352x288: @"352x288",
  28. AVCaptureSessionPreset640x480: @"640x480",
  29. AVCaptureSessionPreset960x540: @"960x540",
  30. AVCaptureSessionPreset1280x720: @"1280x720",
  31. AVCaptureSessionPreset1920x1080: @"1920x1080",
  32. AVCaptureSessionPreset3840x2160: @"3840x2160",
  33. };
  34. _sessionQueue = dispatch_queue_create("session queue", DISPATCH_QUEUE_SERIAL);
  35. OBSAVCaptureVideoInfo newInfo = {0};
  36. _videoInfo = newInfo;
  37. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceDisconnected:)
  38. name:AVCaptureDeviceWasDisconnectedNotification
  39. object:nil];
  40. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceConnected:)
  41. name:AVCaptureDeviceWasConnectedNotification
  42. object:nil];
  43. if (capture_info) {
  44. _captureInfo = capture_info;
  45. NSString *UUID = [OBSAVCapture stringFromSettings:_captureInfo->settings withSetting:@"device"];
  46. NSString *presetName = [OBSAVCapture stringFromSettings:_captureInfo->settings withSetting:@"preset"];
  47. BOOL isPresetEnabled = obs_data_get_bool(_captureInfo->settings, "use_preset");
  48. if (capture_info->isFastPath) {
  49. _isFastPath = YES;
  50. _isPresetBased = NO;
  51. } else {
  52. BOOL isBufferingEnabled = obs_data_get_bool(_captureInfo->settings, "buffering");
  53. obs_source_set_async_unbuffered(_captureInfo->source, !isBufferingEnabled);
  54. }
  55. __weak OBSAVCapture *weakSelf = self;
  56. dispatch_async(_sessionQueue, ^{
  57. NSError *error = nil;
  58. OBSAVCapture *instance = weakSelf;
  59. if ([instance createSession:&error]) {
  60. if ([instance switchCaptureDevice:UUID withError:nil]) {
  61. BOOL isSessionConfigured = NO;
  62. if (isPresetEnabled) {
  63. isSessionConfigured = [instance configureSessionWithPreset:presetName withError:nil];
  64. } else {
  65. isSessionConfigured = [instance configureSession:nil];
  66. }
  67. if (isSessionConfigured) {
  68. [instance startCaptureSession];
  69. }
  70. }
  71. } else {
  72. [instance AVCaptureLog:LOG_ERROR withFormat:error.localizedDescription];
  73. }
  74. });
  75. }
  76. }
  77. return self;
  78. }
  79. #pragma mark - Capture Session Handling
  80. - (BOOL)createSession:(NSError *__autoreleasing *)error
  81. {
  82. AVCaptureSession *session = [[AVCaptureSession alloc] init];
  83. [session beginConfiguration];
  84. if (!session) {
  85. if (error) {
  86. NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to create AVCaptureSession"};
  87. *error = [NSError errorWithDomain:self.errorDomain code:-101 userInfo:userInfo];
  88. }
  89. return NO;
  90. }
  91. AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
  92. if (!videoOutput) {
  93. if (error) {
  94. NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to create AVCaptureVideoDataOutput"};
  95. *error = [NSError errorWithDomain:self.errorDomain code:-102 userInfo:userInfo];
  96. }
  97. return NO;
  98. }
  99. AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init];
  100. if (!audioOutput) {
  101. if (error) {
  102. NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to create AVCaptureAudioDataOutput"};
  103. *error = [NSError errorWithDomain:self.errorDomain code:-103 userInfo:userInfo];
  104. }
  105. return NO;
  106. }
  107. dispatch_queue_t videoQueue = dispatch_queue_create(nil, nil);
  108. if (!videoQueue) {
  109. if (error) {
  110. NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to create video dispatch queue"};
  111. *error = [NSError errorWithDomain:self.errorDomain code:-104 userInfo:userInfo];
  112. }
  113. return NO;
  114. }
  115. dispatch_queue_t audioQueue = dispatch_queue_create(nil, nil);
  116. if (!audioQueue) {
  117. if (error) {
  118. NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to create audio dispatch queue"};
  119. *error = [NSError errorWithDomain:self.errorDomain code:-105 userInfo:userInfo];
  120. }
  121. return NO;
  122. }
  123. if ([session canAddOutput:videoOutput]) {
  124. [session addOutput:videoOutput];
  125. [videoOutput setSampleBufferDelegate:self queue:videoQueue];
  126. }
  127. if ([session canAddOutput:audioOutput]) {
  128. [session addOutput:audioOutput];
  129. [audioOutput setSampleBufferDelegate:self queue:audioQueue];
  130. }
  131. [session commitConfiguration];
  132. self.session = session;
  133. self.videoOutput = videoOutput;
  134. self.videoQueue = videoQueue;
  135. self.audioOutput = audioOutput;
  136. self.audioQueue = audioQueue;
  137. return YES;
  138. }
  139. - (BOOL)switchCaptureDevice:(NSString *)uuid withError:(NSError *__autoreleasing *)error
  140. {
  141. AVCaptureDevice *device = [AVCaptureDevice deviceWithUniqueID:uuid];
  142. if (self.deviceInput.device || !device) {
  143. [self stopCaptureSession];
  144. [self.session removeInput:self.deviceInput];
  145. [self.deviceInput.device unlockForConfiguration];
  146. self.deviceInput = nil;
  147. self.isDeviceLocked = NO;
  148. }
  149. if (!device) {
  150. if (uuid.length < 1) {
  151. [self AVCaptureLog:LOG_INFO withFormat:@"No device selected"];
  152. self.deviceUUID = uuid;
  153. return NO;
  154. } else {
  155. [self AVCaptureLog:LOG_WARNING withFormat:@"Unable to initialize device with unique ID '%@'", uuid];
  156. return NO;
  157. }
  158. }
  159. const char *deviceName = device.localizedName.UTF8String;
  160. obs_data_set_string(self.captureInfo->settings, "device_name", deviceName);
  161. obs_data_set_string(self.captureInfo->settings, "device", device.uniqueID.UTF8String);
  162. [self AVCaptureLog:LOG_INFO withFormat:@"Selected device '%@'", device.localizedName];
  163. self.deviceUUID = device.uniqueID;
  164. BOOL isAudioSupported = [device hasMediaType:AVMediaTypeAudio] || [device hasMediaType:AVMediaTypeMuxed];
  165. obs_source_set_audio_active(self.captureInfo->source, isAudioSupported);
  166. AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:error];
  167. if (!deviceInput) {
  168. return NO;
  169. }
  170. if (@available(macOS 12.0, *)) {
  171. if (device.portraitEffectActive) {
  172. [self AVCaptureLog:LOG_WARNING withFormat:@"Portrait effect is active on selected device"];
  173. }
  174. }
  175. if (@available(macOS 12.3, *)) {
  176. if (device.centerStageActive) {
  177. [self AVCaptureLog:LOG_WARNING withFormat:@"Center Stage effect is active on selected device"];
  178. }
  179. }
  180. if (@available(macOS 13.0, *)) {
  181. if (device.studioLightActive) {
  182. [self AVCaptureLog:LOG_WARNING withFormat:@"Studio Light effect is active on selected device"];
  183. }
  184. }
  185. [self.session beginConfiguration];
  186. if ([self.session canAddInput:deviceInput]) {
  187. [self.session addInput:deviceInput];
  188. self.deviceInput = deviceInput;
  189. } else {
  190. if (error) {
  191. NSDictionary *userInfo = @{
  192. NSLocalizedDescriptionKey: [NSString
  193. stringWithFormat:@"Unable to add device '%@' as deviceInput to capture session", self.deviceUUID]
  194. };
  195. *error = [NSError errorWithDomain:self.errorDomain code:-107 userInfo:userInfo];
  196. }
  197. [self.session commitConfiguration];
  198. return NO;
  199. }
  200. AVCaptureDeviceFormat *deviceFormat = device.activeFormat;
  201. CMMediaType mediaType = CMFormatDescriptionGetMediaType(deviceFormat.formatDescription);
  202. if (mediaType != kCMMediaType_Video && mediaType != kCMMediaType_Muxed) {
  203. if (error) {
  204. NSDictionary *userInfo = @{
  205. NSLocalizedDescriptionKey: [NSString stringWithFormat:@"CMMediaType '%@' is not supported",
  206. [OBSAVCapture stringFromFourCharCode:mediaType]]
  207. };
  208. *error = [NSError errorWithDomain:self.errorDomain code:-108 userInfo:userInfo];
  209. }
  210. [self.session removeInput:deviceInput];
  211. [self.session commitConfiguration];
  212. return NO;
  213. }
  214. if (self.isFastPath) {
  215. self.videoOutput.videoSettings = nil;
  216. NSMutableDictionary *videoSettings =
  217. [NSMutableDictionary dictionaryWithDictionary:self.videoOutput.videoSettings];
  218. FourCharCode targetPixelFormatType = kCVPixelFormatType_32BGRA;
  219. [videoSettings setObject:@(targetPixelFormatType)
  220. forKey:(__bridge NSString *) kCVPixelBufferPixelFormatTypeKey];
  221. self.videoOutput.videoSettings = videoSettings;
  222. } else {
  223. self.videoOutput.videoSettings = nil;
  224. FourCharCode subType = [[self.videoOutput.videoSettings
  225. objectForKey:(__bridge NSString *) kCVPixelBufferPixelFormatTypeKey] unsignedIntValue];
  226. if ([OBSAVCapture formatFromSubtype:subType] != VIDEO_FORMAT_NONE) {
  227. [self AVCaptureLog:LOG_DEBUG
  228. withFormat:@"Using native fourcc '%@'", [OBSAVCapture stringFromFourCharCode:subType]];
  229. } else {
  230. [self AVCaptureLog:LOG_DEBUG withFormat:@"Using fallback fourcc '%@' ('%@', 0x%08x unsupported)",
  231. [OBSAVCapture stringFromFourCharCode:kCVPixelFormatType_32BGRA],
  232. [OBSAVCapture stringFromFourCharCode:subType], subType];
  233. NSMutableDictionary *videoSettings =
  234. [NSMutableDictionary dictionaryWithDictionary:self.videoOutput.videoSettings];
  235. [videoSettings setObject:@(kCVPixelFormatType_32BGRA)
  236. forKey:(__bridge NSString *) kCVPixelBufferPixelFormatTypeKey];
  237. self.videoOutput.videoSettings = videoSettings;
  238. }
  239. }
  240. [self.session commitConfiguration];
  241. return YES;
  242. }
  243. - (void)startCaptureSession
  244. {
  245. if (!self.session.running) {
  246. [self.session startRunning];
  247. }
  248. }
  249. - (void)stopCaptureSession
  250. {
  251. if (self.session.running) {
  252. [self.session stopRunning];
  253. }
  254. if (self.captureInfo->isFastPath) {
  255. if (self.captureInfo->texture) {
  256. obs_enter_graphics();
  257. gs_texture_destroy(self.captureInfo->texture);
  258. obs_leave_graphics();
  259. self.captureInfo->texture = NULL;
  260. }
  261. if (self.captureInfo->currentSurface) {
  262. IOSurfaceDecrementUseCount(self.captureInfo->currentSurface);
  263. CFRelease(self.captureInfo->currentSurface);
  264. self.captureInfo->currentSurface = NULL;
  265. }
  266. if (self.captureInfo->previousSurface) {
  267. IOSurfaceDecrementUseCount(self.captureInfo->previousSurface);
  268. CFRelease(self.captureInfo->previousSurface);
  269. self.captureInfo->previousSurface = NULL;
  270. }
  271. } else {
  272. obs_source_output_video(self.captureInfo->source, NULL);
  273. }
  274. }
  275. - (BOOL)configureSessionWithPreset:(AVCaptureSessionPreset)preset withError:(NSError *__autoreleasing *)error
  276. {
  277. if (!self.deviceInput.device) {
  278. if (error) {
  279. NSDictionary *userInfo =
  280. @{NSLocalizedDescriptionKey: @"Unable to set session preset without capture device"};
  281. *error = [NSError errorWithDomain:self.errorDomain code:-108 userInfo:userInfo];
  282. }
  283. return NO;
  284. }
  285. if (![self.deviceInput.device supportsAVCaptureSessionPreset:preset]) {
  286. if (error) {
  287. NSDictionary *userInfo = @{
  288. NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Preset %@ not supported by device %@",
  289. [OBSAVCapture stringFromCapturePreset:preset],
  290. self.deviceInput.device.localizedName]
  291. };
  292. *error = [NSError errorWithDomain:self.errorDomain code:-201 userInfo:userInfo];
  293. }
  294. return NO;
  295. }
  296. if ([self.session canSetSessionPreset:preset]) {
  297. if (self.isDeviceLocked) {
  298. if ([preset isEqualToString:self.session.sessionPreset]) {
  299. if (self.deviceInput.device.activeFormat) {
  300. self.deviceInput.device.activeFormat = self.presetFormat.activeFormat;
  301. self.deviceInput.device.activeVideoMinFrameDuration = self.presetFormat.minFrameRate;
  302. self.deviceInput.device.activeVideoMaxFrameDuration = self.presetFormat.maxFrameRate;
  303. }
  304. self.presetFormat = nil;
  305. }
  306. [self.deviceInput.device unlockForConfiguration];
  307. self.isDeviceLocked = NO;
  308. }
  309. if ([self.session canSetSessionPreset:preset]) {
  310. self.session.sessionPreset = preset;
  311. }
  312. } else {
  313. if (error) {
  314. NSDictionary *userInfo = @{
  315. NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Preset %@ not supported by capture session",
  316. [OBSAVCapture stringFromCapturePreset:preset]]
  317. };
  318. *error = [NSError errorWithDomain:self.errorDomain code:-202 userInfo:userInfo];
  319. }
  320. return NO;
  321. }
  322. self.isPresetBased = YES;
  323. return YES;
  324. }
  325. - (BOOL)configureSession:(NSError *__autoreleasing *)error
  326. {
  327. int videoRange;
  328. int colorSpace;
  329. FourCharCode inputFourCC;
  330. if (!self.isFastPath) {
  331. videoRange = (int) obs_data_get_int(self.captureInfo->settings, "video_range");
  332. if (![OBSAVCapture isValidVideoRange:videoRange]) {
  333. [self AVCaptureLog:LOG_WARNING withFormat:@"Unsupported video range: %d", videoRange];
  334. return NO;
  335. }
  336. int inputFormat;
  337. inputFormat = (int) obs_data_get_int(self.captureInfo->settings, "input_format");
  338. inputFourCC = [OBSAVCapture fourCharCodeFromFormat:inputFormat withRange:videoRange];
  339. colorSpace = (int) obs_data_get_int(self.captureInfo->settings, "color_space");
  340. if (![OBSAVCapture isValidColorspace:colorSpace]) {
  341. [self AVCaptureLog:LOG_DEBUG withFormat:@"Unsupported color space: %d", colorSpace];
  342. return NO;
  343. }
  344. } else {
  345. int inputFormat;
  346. CMFormatDescriptionRef formatDescription = self.deviceInput.device.activeFormat.formatDescription;
  347. inputFormat = (int) obs_data_get_int(self.captureInfo->settings, "input_format");
  348. inputFourCC = [OBSAVCapture fourCharCodeFromFormat:inputFormat withRange:VIDEO_RANGE_DEFAULT];
  349. colorSpace = [OBSAVCapture colorspaceFromDescription:formatDescription];
  350. videoRange = ([OBSAVCapture isFullRangeFormat:inputFourCC]) ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL;
  351. }
  352. CMVideoDimensions dimensions = [OBSAVCapture dimensionsFromSettings:self.captureInfo->settings];
  353. if (dimensions.width == 0 || dimensions.height == 0) {
  354. [self AVCaptureLog:LOG_DEBUG withFormat:@"No valid resolution found in settings"];
  355. return NO;
  356. }
  357. struct media_frames_per_second fps;
  358. if (!obs_data_get_frames_per_second(self.captureInfo->settings, "frame_rate", &fps, NULL)) {
  359. [self AVCaptureLog:LOG_DEBUG withFormat:@"No valid framerate found in settings"];
  360. return NO;
  361. }
  362. CMTime time = {.value = fps.denominator, .timescale = fps.numerator, .flags = 1};
  363. AVCaptureDeviceFormat *format = nil;
  364. for (AVCaptureDeviceFormat *formatCandidate in [self.deviceInput.device.formats reverseObjectEnumerator]) {
  365. CMVideoDimensions formatDimensions = CMVideoFormatDescriptionGetDimensions(formatCandidate.formatDescription);
  366. if (!(formatDimensions.width == dimensions.width) || !(formatDimensions.height == dimensions.height)) {
  367. continue;
  368. }
  369. for (AVFrameRateRange *range in formatCandidate.videoSupportedFrameRateRanges) {
  370. if (CMTimeCompare(range.maxFrameDuration, time) >= 0 && CMTimeCompare(range.minFrameDuration, time) <= 0) {
  371. CMFormatDescriptionRef formatDescription = formatCandidate.formatDescription;
  372. FourCharCode formatFourCC = CMFormatDescriptionGetMediaSubType(formatDescription);
  373. if (inputFourCC == formatFourCC) {
  374. format = formatCandidate;
  375. inputFourCC = formatFourCC;
  376. break;
  377. }
  378. }
  379. }
  380. if (format) {
  381. break;
  382. }
  383. }
  384. if (!format) {
  385. [self AVCaptureLog:LOG_WARNING withFormat:@"Frame rate is not supported: %g FPS (%u/%u)",
  386. media_frames_per_second_to_fps(fps), fps.numerator, fps.denominator];
  387. return NO;
  388. }
  389. [self.session beginConfiguration];
  390. self.isDeviceLocked = [self.deviceInput.device lockForConfiguration:error];
  391. if (!self.isDeviceLocked) {
  392. [self AVCaptureLog:LOG_WARNING withFormat:@"Could not lock devie for configuration"];
  393. return NO;
  394. }
  395. [self AVCaptureLog:LOG_INFO
  396. withFormat:@"Capturing '%@' (%@):\n"
  397. " Resolution : %ux%u\n"
  398. " FPS : %g (%u/%u)\n"
  399. " Frame Interval : %g\u00a0s\n"
  400. " Input Format : %@\n"
  401. " Requested Color Space : %@ (%d)\n"
  402. " Requested Video Range : %@ (%d)\n"
  403. " Using Format : %@",
  404. self.deviceInput.device.localizedName, self.deviceInput.device.uniqueID, dimensions.width,
  405. dimensions.height, media_frames_per_second_to_fps(fps), fps.numerator, fps.denominator,
  406. media_frames_per_second_to_frame_interval(fps), [OBSAVCapture stringFromSubType:inputFourCC],
  407. [OBSAVCapture stringFromColorspace:colorSpace], colorSpace,
  408. [OBSAVCapture stringFromVideoRange:videoRange], videoRange, format.description];
  409. OBSAVCaptureVideoInfo newInfo = {.colorSpace = _videoInfo.colorSpace,
  410. .videoRange = _videoInfo.videoRange,
  411. .isValid = false};
  412. self.videoInfo = newInfo;
  413. self.isPresetBased = NO;
  414. if (!self.presetFormat) {
  415. OBSAVCapturePresetInfo *presetInfo = [[OBSAVCapturePresetInfo alloc] init];
  416. presetInfo.activeFormat = self.deviceInput.device.activeFormat;
  417. presetInfo.minFrameRate = self.deviceInput.device.activeVideoMinFrameDuration;
  418. presetInfo.maxFrameRate = self.deviceInput.device.activeVideoMaxFrameDuration;
  419. self.presetFormat = presetInfo;
  420. }
  421. self.deviceInput.device.activeFormat = format;
  422. self.deviceInput.device.activeVideoMinFrameDuration = time;
  423. self.deviceInput.device.activeVideoMaxFrameDuration = time;
  424. [self.session commitConfiguration];
  425. return YES;
  426. }
  427. - (BOOL)updateSessionwithError:(NSError *__autoreleasing *)error
  428. {
  429. switch (self.captureInfo->lastError) {
  430. case OBSAVCaptureError_SampleBufferFormat:
  431. if (self.captureInfo->sampleBufferDescription) {
  432. FourCharCode mediaSubType =
  433. CMFormatDescriptionGetMediaSubType(self.captureInfo->sampleBufferDescription);
  434. [self AVCaptureLog:LOG_ERROR
  435. withFormat:@"Incompatible sample buffer format received for sync AVCapture source: %@ (0x%x)",
  436. [OBSAVCapture stringFromFourCharCode:mediaSubType], mediaSubType];
  437. }
  438. break;
  439. case OBSAVCaptureError_ColorSpace: {
  440. if (self.captureInfo->sampleBufferDescription) {
  441. FourCharCode mediaSubType =
  442. CMFormatDescriptionGetMediaSubType(self.captureInfo->sampleBufferDescription);
  443. BOOL isSampleBufferFullRange = [OBSAVCapture isFullRangeFormat:mediaSubType];
  444. OBSAVCaptureColorSpace sampleBufferColorSpace =
  445. [OBSAVCapture colorspaceFromDescription:self.captureInfo->sampleBufferDescription];
  446. OBSAVCaptureVideoRange sampleBufferRangeType = isSampleBufferFullRange ? VIDEO_RANGE_FULL
  447. : VIDEO_RANGE_PARTIAL;
  448. [self AVCaptureLog:LOG_ERROR
  449. withFormat:@"Failed to get colorspace parameters for colorspace %u and range %u",
  450. sampleBufferColorSpace, sampleBufferRangeType];
  451. }
  452. break;
  453. default:
  454. self.captureInfo->lastError = OBSAVCaptureError_NoError;
  455. self.captureInfo->sampleBufferDescription = NULL;
  456. break;
  457. }
  458. }
  459. switch (self.captureInfo->lastAudioError) {
  460. case OBSAVCaptureError_AudioBuffer: {
  461. [OBSAVCapture AVCaptureLog:LOG_ERROR
  462. withFormat:@"Unable to retrieve required AudioBufferList size from sample buffer."];
  463. break;
  464. }
  465. default:
  466. self.captureInfo->lastAudioError = OBSAVCaptureError_NoError;
  467. break;
  468. }
  469. NSString *newDeviceUUID = [OBSAVCapture stringFromSettings:self.captureInfo->settings withSetting:@"device"];
  470. NSString *presetName = [OBSAVCapture stringFromSettings:self.captureInfo->settings withSetting:@"preset"];
  471. BOOL isPresetEnabled = obs_data_get_bool(self.captureInfo->settings, "use_preset");
  472. BOOL updateSession = YES;
  473. if (![self.deviceUUID isEqualToString:newDeviceUUID]) {
  474. if (![self switchCaptureDevice:newDeviceUUID withError:error]) {
  475. obs_source_update_properties(self.captureInfo->source);
  476. return NO;
  477. }
  478. } else if (self.isPresetBased && isPresetEnabled && [presetName isEqualToString:self.session.sessionPreset]) {
  479. updateSession = NO;
  480. }
  481. if (updateSession) {
  482. if (isPresetEnabled) {
  483. [self configureSessionWithPreset:presetName withError:error];
  484. } else {
  485. if (![self configureSession:error]) {
  486. obs_source_update_properties(self.captureInfo->source);
  487. return NO;
  488. }
  489. }
  490. __weak OBSAVCapture *weakSelf = self;
  491. dispatch_async(self.sessionQueue, ^{
  492. [weakSelf startCaptureSession];
  493. });
  494. }
  495. BOOL isAudioAvailable = [self.deviceInput.device hasMediaType:AVMediaTypeAudio] ||
  496. [self.deviceInput.device hasMediaType:AVMediaTypeMuxed];
  497. obs_source_set_audio_active(self.captureInfo->source, isAudioAvailable);
  498. if (!self.isFastPath) {
  499. BOOL isBufferingEnabled = obs_data_get_bool(self.captureInfo->settings, "buffering");
  500. obs_source_set_async_unbuffered(self.captureInfo->source, !isBufferingEnabled);
  501. }
  502. return YES;
  503. }
  504. #pragma mark - OBS Settings Helpers
  505. + (CMVideoDimensions)dimensionsFromSettings:(void *)settings
  506. {
  507. CMVideoDimensions zero = {0};
  508. NSString *jsonString = [OBSAVCapture stringFromSettings:settings withSetting:@"resolution"];
  509. NSDictionary *data = [NSJSONSerialization JSONObjectWithData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]
  510. options:0
  511. error:nil];
  512. if (data.count == 0) {
  513. return zero;
  514. }
  515. NSInteger width = [[data objectForKey:@"width"] intValue];
  516. NSInteger height = [[data objectForKey:@"height"] intValue];
  517. if (!width || !height) {
  518. return zero;
  519. }
  520. CMVideoDimensions dimensions = {.width = (int32_t) clamp_Uint(width, 0, UINT32_MAX),
  521. .height = (int32_t) clamp_Uint(height, 0, UINT32_MAX)};
  522. return dimensions;
  523. }
  524. + (NSString *)stringFromSettings:(void *)settings withSetting:(NSString *)setting
  525. {
  526. return [OBSAVCapture stringFromSettings:settings withSetting:setting withDefault:@""];
  527. }
  528. + (NSString *)stringFromSettings:(void *)settings withSetting:(NSString *)setting withDefault:(NSString *)defaultValue
  529. {
  530. NSString *result;
  531. if (settings) {
  532. const char *setting_value = obs_data_get_string(settings, setting.UTF8String);
  533. if (!setting_value) {
  534. result = [NSString stringWithString:defaultValue];
  535. } else {
  536. result = @(setting_value);
  537. }
  538. } else {
  539. result = [NSString stringWithString:defaultValue];
  540. }
  541. return result;
  542. }
  543. #pragma mark - Format Conversion Helpers
  544. + (NSString *)stringFromSubType:(FourCharCode)subtype
  545. {
  546. switch (subtype) {
  547. case kCVPixelFormatType_422YpCbCr8:
  548. return @"UYVY - 422YpCbCr8";
  549. case kCVPixelFormatType_422YpCbCr8_yuvs:
  550. return @"YUY2 - 422YpCbCr8_yuvs";
  551. case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
  552. case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
  553. return @"NV12 - 420YpCbCr8BiPlanar";
  554. case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
  555. case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
  556. return @"P010 - 420YpCbCr10BiPlanar";
  557. case kCVPixelFormatType_32ARGB:
  558. return @"ARGB - 32ARGB";
  559. case kCVPixelFormatType_32BGRA:
  560. return @"BGRA - 32BGRA";
  561. case kCMVideoCodecType_Animation:
  562. return @"Apple Animation";
  563. case kCMVideoCodecType_Cinepak:
  564. return @"Cinepak";
  565. case kCMVideoCodecType_JPEG:
  566. return @"JPEG";
  567. case kCMVideoCodecType_JPEG_OpenDML:
  568. return @"MJPEG - JPEG OpenDML";
  569. case kCMVideoCodecType_SorensonVideo:
  570. return @"Sorenson Video";
  571. case kCMVideoCodecType_SorensonVideo3:
  572. return @"Sorenson Video 3";
  573. case kCMVideoCodecType_H263:
  574. return @"H.263";
  575. case kCMVideoCodecType_H264:
  576. return @"H.264";
  577. case kCMVideoCodecType_MPEG4Video:
  578. return @"MPEG-4";
  579. case kCMVideoCodecType_MPEG2Video:
  580. return @"MPEG-2";
  581. case kCMVideoCodecType_MPEG1Video:
  582. return @"MPEG-1";
  583. case kCMVideoCodecType_DVCNTSC:
  584. return @"DV NTSC";
  585. case kCMVideoCodecType_DVCPAL:
  586. return @"DV PAL";
  587. case kCMVideoCodecType_DVCProPAL:
  588. return @"Panasonic DVCPro Pal";
  589. case kCMVideoCodecType_DVCPro50NTSC:
  590. return @"Panasonic DVCPro-50 NTSC";
  591. case kCMVideoCodecType_DVCPro50PAL:
  592. return @"Panasonic DVCPro-50 PAL";
  593. case kCMVideoCodecType_DVCPROHD720p60:
  594. return @"Panasonic DVCPro-HD 720p60";
  595. case kCMVideoCodecType_DVCPROHD720p50:
  596. return @"Panasonic DVCPro-HD 720p50";
  597. case kCMVideoCodecType_DVCPROHD1080i60:
  598. return @"Panasonic DVCPro-HD 1080i60";
  599. case kCMVideoCodecType_DVCPROHD1080i50:
  600. return @"Panasonic DVCPro-HD 1080i50";
  601. case kCMVideoCodecType_DVCPROHD1080p30:
  602. return @"Panasonic DVCPro-HD 1080p30";
  603. case kCMVideoCodecType_DVCPROHD1080p25:
  604. return @"Panasonic DVCPro-HD 1080p25";
  605. case kCMVideoCodecType_AppleProRes4444:
  606. return @"Apple ProRes 4444";
  607. case kCMVideoCodecType_AppleProRes422HQ:
  608. return @"Apple ProRes 422 HQ";
  609. case kCMVideoCodecType_AppleProRes422:
  610. return @"Apple ProRes 422";
  611. case kCMVideoCodecType_AppleProRes422LT:
  612. return @"Apple ProRes 422 LT";
  613. case kCMVideoCodecType_AppleProRes422Proxy:
  614. return @"Apple ProRes 422 Proxy";
  615. default:
  616. return @"Unknown";
  617. }
  618. }
  619. + (NSString *)stringFromColorspace:(enum video_colorspace)colorspace
  620. {
  621. switch (colorspace) {
  622. case VIDEO_CS_DEFAULT:
  623. return @"Default";
  624. case VIDEO_CS_601:
  625. return @"CS 601";
  626. case VIDEO_CS_709:
  627. return @"CS 709";
  628. case VIDEO_CS_SRGB:
  629. return @"sRGB";
  630. case VIDEO_CS_2100_PQ:
  631. return @"CS 2100 (PQ)";
  632. case VIDEO_CS_2100_HLG:
  633. return @"CS 2100 (HLG)";
  634. default:
  635. return @"Unknown";
  636. }
  637. }
  638. + (NSString *)stringFromVideoRange:(enum video_range_type)videoRange
  639. {
  640. switch (videoRange) {
  641. case VIDEO_RANGE_FULL:
  642. return @"Full";
  643. case VIDEO_RANGE_PARTIAL:
  644. return @"Partial";
  645. case VIDEO_RANGE_DEFAULT:
  646. return @"Default";
  647. }
  648. }
  649. + (NSString *)stringFromCapturePreset:(AVCaptureSessionPreset)preset
  650. {
  651. NSDictionary *presetDescriptions = @{
  652. AVCaptureSessionPresetLow: @"Low",
  653. AVCaptureSessionPresetMedium: @"Medium",
  654. AVCaptureSessionPresetHigh: @"High",
  655. AVCaptureSessionPreset320x240: @"320x240",
  656. AVCaptureSessionPreset352x288: @"352x288",
  657. AVCaptureSessionPreset640x480: @"640x480",
  658. AVCaptureSessionPreset960x540: @"960x460",
  659. AVCaptureSessionPreset1280x720: @"1280x720",
  660. AVCaptureSessionPreset1920x1080: @"1920x1080",
  661. AVCaptureSessionPreset3840x2160: @"3840x2160",
  662. };
  663. NSString *presetDescription = [presetDescriptions objectForKey:preset];
  664. if (!presetDescription) {
  665. return [NSString stringWithFormat:@"Unknown (%@)", preset];
  666. } else {
  667. return presetDescription;
  668. }
  669. }
  670. + (NSString *)stringFromFourCharCode:(OSType)fourCharCode
  671. {
  672. char cString[5] = {(fourCharCode >> 24) & 0xFF, (fourCharCode >> 16) & 0xFF, (fourCharCode >> 8) & 0xFF,
  673. fourCharCode & 0xFF, 0};
  674. NSString *codeString = @(cString);
  675. return codeString;
  676. }
  677. + (FourCharCode)fourCharCodeFromString:(NSString *)codeString
  678. {
  679. FourCharCode fourCharCode;
  680. const char *cString = codeString.UTF8String;
  681. fourCharCode = (cString[0] << 24) | (cString[1] << 16) | (cString[2] << 8) | cString[3];
  682. return fourCharCode;
  683. }
  684. + (BOOL)isValidColorspace:(enum video_colorspace)colorspace
  685. {
  686. switch (colorspace) {
  687. case VIDEO_CS_DEFAULT:
  688. case VIDEO_CS_601:
  689. case VIDEO_CS_709:
  690. return YES;
  691. default:
  692. return NO;
  693. }
  694. }
  695. + (BOOL)isValidVideoRange:(enum video_range_type)videoRange
  696. {
  697. switch (videoRange) {
  698. case VIDEO_RANGE_DEFAULT:
  699. case VIDEO_RANGE_PARTIAL:
  700. case VIDEO_RANGE_FULL:
  701. return YES;
  702. default:
  703. return NO;
  704. }
  705. }
  706. + (BOOL)isFullRangeFormat:(FourCharCode)pixelFormat
  707. {
  708. switch (pixelFormat) {
  709. case kCVPixelFormatType_420YpCbCr8PlanarFullRange:
  710. case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
  711. case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
  712. case kCVPixelFormatType_422YpCbCr8FullRange:
  713. return YES;
  714. default:
  715. return NO;
  716. }
  717. }
  718. + (OBSAVCaptureVideoFormat)formatFromSubtype:(FourCharCode)subtype
  719. {
  720. switch (subtype) {
  721. case kCVPixelFormatType_422YpCbCr8:
  722. return VIDEO_FORMAT_UYVY;
  723. case kCVPixelFormatType_422YpCbCr8_yuvs:
  724. return VIDEO_FORMAT_YUY2;
  725. case kCVPixelFormatType_32BGRA:
  726. return VIDEO_FORMAT_BGRA;
  727. case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
  728. case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
  729. return VIDEO_FORMAT_NV12;
  730. case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
  731. case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
  732. return VIDEO_FORMAT_P010;
  733. default:
  734. return VIDEO_FORMAT_NONE;
  735. }
  736. }
  737. + (FourCharCode)fourCharCodeFromFormat:(OBSAVCaptureVideoFormat)format withRange:(enum video_range_type)videoRange
  738. {
  739. switch (format) {
  740. case VIDEO_FORMAT_UYVY:
  741. return kCVPixelFormatType_422YpCbCr8;
  742. case VIDEO_FORMAT_YUY2:
  743. return kCVPixelFormatType_422YpCbCr8_yuvs;
  744. case VIDEO_FORMAT_NV12:
  745. if (videoRange == VIDEO_RANGE_FULL) {
  746. return kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;
  747. } else {
  748. return kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
  749. }
  750. case VIDEO_FORMAT_P010:
  751. if (videoRange == VIDEO_RANGE_FULL) {
  752. return kCVPixelFormatType_420YpCbCr10BiPlanarFullRange;
  753. } else {
  754. return kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange;
  755. }
  756. case VIDEO_FORMAT_BGRA:
  757. return kCVPixelFormatType_32BGRA;
  758. default:
  759. return 0;
  760. }
  761. }
  762. + (FourCharCode)fourCharCodeFromFormat:(OBSAVCaptureVideoFormat)format
  763. {
  764. return [OBSAVCapture fourCharCodeFromFormat:format withRange:VIDEO_RANGE_PARTIAL];
  765. }
  766. + (OBSAVCaptureColorSpace)colorspaceFromDescription:(CMFormatDescriptionRef)description
  767. {
  768. CFPropertyListRef matrix = CMFormatDescriptionGetExtension(description, kCMFormatDescriptionExtension_YCbCrMatrix);
  769. if (!matrix) {
  770. return VIDEO_CS_DEFAULT;
  771. }
  772. CFComparisonResult is601 = CFStringCompare(matrix, kCVImageBufferYCbCrMatrix_ITU_R_601_4, 0);
  773. CFComparisonResult is709 = CFStringCompare(matrix, kCVImageBufferYCbCrMatrix_ITU_R_709_2, 0);
  774. CFComparisonResult is2020 = CFStringCompare(matrix, kCVImageBufferYCbCrMatrix_ITU_R_2020, 0);
  775. if (is601 == kCFCompareEqualTo) {
  776. return VIDEO_CS_601;
  777. } else if (is709 == kCFCompareEqualTo) {
  778. return VIDEO_CS_709;
  779. } else if (is2020 == kCFCompareEqualTo) {
  780. CFPropertyListRef transferFunction =
  781. CMFormatDescriptionGetExtension(description, kCMFormatDescriptionExtension_TransferFunction);
  782. if (!matrix) {
  783. return VIDEO_CS_DEFAULT;
  784. }
  785. CFComparisonResult isPQ = CFStringCompare(transferFunction, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ, 0);
  786. CFComparisonResult isHLG = CFStringCompare(transferFunction, kCVImageBufferTransferFunction_ITU_R_2100_HLG, 0);
  787. if (isPQ == kCFCompareEqualTo) {
  788. return VIDEO_CS_2100_PQ;
  789. } else if (isHLG == kCFCompareEqualTo) {
  790. return VIDEO_CS_2100_HLG;
  791. }
  792. }
  793. return VIDEO_CS_DEFAULT;
  794. }
  795. #pragma mark - Notification Handlers
  796. - (void)deviceConnected:(NSNotification *)notification
  797. {
  798. AVCaptureDevice *device = notification.object;
  799. if (!device) {
  800. return;
  801. }
  802. if (![[device uniqueID] isEqualTo:self.deviceUUID]) {
  803. obs_source_update_properties(self.captureInfo->source);
  804. return;
  805. }
  806. if (self.deviceInput.device) {
  807. [self AVCaptureLog:LOG_INFO withFormat:@"Received connect event with active device '%@' (UUID %@)",
  808. self.deviceInput.device.localizedName, self.deviceInput.device.uniqueID];
  809. obs_source_update_properties(self.captureInfo->source);
  810. return;
  811. }
  812. [self AVCaptureLog:LOG_INFO
  813. withFormat:@"Received connect event for device '%@' (UUID %@)", device.localizedName, device.uniqueID];
  814. NSError *error;
  815. NSString *presetName = [OBSAVCapture stringFromSettings:self.captureInfo->settings withSetting:@"preset"];
  816. BOOL isPresetEnabled = obs_data_get_bool(self.captureInfo->settings, "use_preset");
  817. BOOL isFastPath = self.captureInfo->isFastPath;
  818. if ([self switchCaptureDevice:device.uniqueID withError:&error]) {
  819. BOOL success;
  820. if (isPresetEnabled && !isFastPath) {
  821. success = [self configureSessionWithPreset:presetName withError:&error];
  822. } else {
  823. success = [self configureSession:&error];
  824. }
  825. if (success) {
  826. dispatch_async(self.sessionQueue, ^{
  827. [self startCaptureSession];
  828. });
  829. } else {
  830. [self AVCaptureLog:LOG_ERROR withFormat:error.localizedDescription];
  831. }
  832. } else {
  833. [self AVCaptureLog:LOG_ERROR withFormat:error.localizedDescription];
  834. }
  835. obs_source_update_properties(self.captureInfo->source);
  836. }
  837. - (void)deviceDisconnected:(NSNotification *)notification
  838. {
  839. AVCaptureDevice *device = notification.object;
  840. if (!device) {
  841. return;
  842. }
  843. if (![[device uniqueID] isEqualTo:self.deviceUUID]) {
  844. obs_source_update_properties(self.captureInfo->source);
  845. return;
  846. }
  847. if (!self.deviceInput.device) {
  848. [self AVCaptureLog:LOG_ERROR withFormat:@"Received disconnect event for inactive device '%@' (UUID %@)",
  849. device.localizedName, device.uniqueID];
  850. obs_source_update_properties(self.captureInfo->source);
  851. return;
  852. }
  853. [self AVCaptureLog:LOG_INFO
  854. withFormat:@"Received disconnect event for device '%@' (UUID %@)", device.localizedName, device.uniqueID];
  855. __weak OBSAVCapture *weakSelf = self;
  856. dispatch_async(self.sessionQueue, ^{
  857. OBSAVCapture *instance = weakSelf;
  858. [instance stopCaptureSession];
  859. [instance.session removeInput:instance.deviceInput];
  860. instance.deviceInput = nil;
  861. instance = nil;
  862. });
  863. obs_source_update_properties(self.captureInfo->source);
  864. }
  865. #pragma mark - AVCapture Delegate Methods
  866. - (void)captureOutput:(AVCaptureOutput *)output
  867. didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
  868. fromConnection:(AVCaptureConnection *)connection
  869. {
  870. return;
  871. }
  872. - (void)captureOutput:(AVCaptureOutput *)output
  873. didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
  874. fromConnection:(AVCaptureConnection *)connection
  875. {
  876. CMItemCount sampleCount = CMSampleBufferGetNumSamples(sampleBuffer);
  877. if (!_captureInfo || sampleCount < 1) {
  878. return;
  879. }
  880. CMTime presentationTimeStamp = CMSampleBufferGetOutputPresentationTimeStamp(sampleBuffer);
  881. CMTime presentationNanoTimeStamp = CMTimeConvertScale(presentationTimeStamp, 1E9, kCMTimeRoundingMethod_Default);
  882. CMFormatDescriptionRef description = CMSampleBufferGetFormatDescription(sampleBuffer);
  883. CMMediaType mediaType = CMFormatDescriptionGetMediaType(description);
  884. switch (mediaType) {
  885. case kCMMediaType_Video: {
  886. CMVideoDimensions sampleBufferDimensions = CMVideoFormatDescriptionGetDimensions(description);
  887. CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
  888. FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(description);
  889. OBSAVCaptureVideoInfo newInfo = {.videoRange = _videoInfo.videoRange,
  890. .colorSpace = _videoInfo.colorSpace,
  891. .isValid = false};
  892. BOOL usePreset = obs_data_get_bool(_captureInfo->settings, "use_preset");
  893. if (_isFastPath) {
  894. if (mediaSubType != kCVPixelFormatType_32BGRA &&
  895. mediaSubType != kCVPixelFormatType_ARGB2101010LEPacked) {
  896. _captureInfo->lastError = OBSAVCaptureError_SampleBufferFormat;
  897. CMFormatDescriptionCreate(kCFAllocatorDefault, mediaType, mediaSubType, NULL,
  898. &_captureInfo->sampleBufferDescription);
  899. obs_source_update_properties(_captureInfo->source);
  900. break;
  901. } else {
  902. _captureInfo->lastError = OBSAVCaptureError_NoError;
  903. _captureInfo->sampleBufferDescription = NULL;
  904. }
  905. CVPixelBufferLockBaseAddress(imageBuffer, 0);
  906. IOSurfaceRef frameSurface = CVPixelBufferGetIOSurface(imageBuffer);
  907. CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
  908. IOSurfaceRef previousSurface = NULL;
  909. if (frameSurface && !pthread_mutex_lock(&_captureInfo->mutex)) {
  910. NSRect frameSize = _captureInfo->frameSize;
  911. if (frameSize.size.width != sampleBufferDimensions.width ||
  912. frameSize.size.height != sampleBufferDimensions.height) {
  913. frameSize = CGRectMake(0, 0, sampleBufferDimensions.width, sampleBufferDimensions.height);
  914. }
  915. previousSurface = _captureInfo->currentSurface;
  916. _captureInfo->currentSurface = frameSurface;
  917. CFRetain(_captureInfo->currentSurface);
  918. IOSurfaceIncrementUseCount(_captureInfo->currentSurface);
  919. pthread_mutex_unlock(&_captureInfo->mutex);
  920. newInfo.isValid = true;
  921. if (_videoInfo.isValid != newInfo.isValid) {
  922. obs_source_update_properties(_captureInfo->source);
  923. }
  924. _captureInfo->frameSize = frameSize;
  925. _videoInfo = newInfo;
  926. }
  927. if (previousSurface) {
  928. IOSurfaceDecrementUseCount(previousSurface);
  929. CFRelease(previousSurface);
  930. }
  931. break;
  932. } else {
  933. OBSAVCaptureVideoFrame *frame = _captureInfo->videoFrame;
  934. frame->timestamp = presentationNanoTimeStamp.value;
  935. enum video_format videoFormat = [OBSAVCapture formatFromSubtype:mediaSubType];
  936. if (videoFormat == VIDEO_FORMAT_NONE) {
  937. _captureInfo->lastError = OBSAVCaptureError_SampleBufferFormat;
  938. CMFormatDescriptionCreate(kCFAllocatorDefault, mediaType, mediaSubType, NULL,
  939. &_captureInfo->sampleBufferDescription);
  940. } else {
  941. _captureInfo->lastError = OBSAVCaptureError_NoError;
  942. _captureInfo->sampleBufferDescription = NULL;
  943. #ifdef DEBUG
  944. if (frame->format != VIDEO_FORMAT_NONE && frame->format != videoFormat) {
  945. [self AVCaptureLog:LOG_DEBUG
  946. withFormat:@"Switching fourcc: '%@' (0x%x) -> '%@' (0x%x)",
  947. [OBSAVCapture stringFromFourCharCode:frame->format], frame -> format,
  948. [OBSAVCapture stringFromFourCharCode:mediaSubType], mediaSubType];
  949. }
  950. #endif
  951. bool isFrameYuv = format_is_yuv(frame->format);
  952. bool isSampleBufferYuv = format_is_yuv(videoFormat);
  953. frame->format = videoFormat;
  954. frame->width = sampleBufferDimensions.width;
  955. frame->height = sampleBufferDimensions.height;
  956. BOOL isSampleBufferFullRange = [OBSAVCapture isFullRangeFormat:mediaSubType];
  957. if (isSampleBufferYuv) {
  958. OBSAVCaptureColorSpace sampleBufferColorSpace =
  959. [OBSAVCapture colorspaceFromDescription:description];
  960. OBSAVCaptureVideoRange sampleBufferRangeType = isSampleBufferFullRange ? VIDEO_RANGE_FULL
  961. : VIDEO_RANGE_PARTIAL;
  962. BOOL isColorSpaceMatching = NO;
  963. SInt64 configuredColorSpace = obs_data_get_int(_captureInfo->settings, "color_space");
  964. if (usePreset) {
  965. isColorSpaceMatching = sampleBufferColorSpace == _videoInfo.colorSpace;
  966. } else {
  967. isColorSpaceMatching = configuredColorSpace == _videoInfo.colorSpace;
  968. }
  969. BOOL isVideoRangeMatching = NO;
  970. SInt64 configuredVideoRangeType = obs_data_get_int(_captureInfo->settings, "video_range");
  971. if (usePreset) {
  972. isVideoRangeMatching = sampleBufferRangeType == _videoInfo.videoRange;
  973. } else {
  974. isVideoRangeMatching = configuredVideoRangeType == _videoInfo.videoRange;
  975. isSampleBufferFullRange = configuredVideoRangeType == VIDEO_RANGE_FULL;
  976. }
  977. if (isColorSpaceMatching && isVideoRangeMatching) {
  978. newInfo.isValid = true;
  979. } else {
  980. frame->full_range = isSampleBufferFullRange;
  981. bool success = video_format_get_parameters_for_format(
  982. sampleBufferColorSpace, sampleBufferRangeType, frame->format, frame->color_matrix,
  983. frame->color_range_min, frame->color_range_max);
  984. if (!success) {
  985. _captureInfo->lastError = OBSAVCaptureError_ColorSpace;
  986. CMFormatDescriptionCreate(kCFAllocatorDefault, mediaType, mediaSubType, NULL,
  987. &_captureInfo->sampleBufferDescription);
  988. newInfo.isValid = false;
  989. } else {
  990. newInfo.colorSpace = sampleBufferColorSpace;
  991. newInfo.videoRange = sampleBufferRangeType;
  992. newInfo.isValid = true;
  993. }
  994. }
  995. } else if (!isFrameYuv && !isSampleBufferYuv) {
  996. newInfo.isValid = true;
  997. }
  998. }
  999. if (newInfo.isValid != _videoInfo.isValid) {
  1000. obs_source_update_properties(_captureInfo->source);
  1001. }
  1002. _videoInfo = newInfo;
  1003. if (newInfo.isValid) {
  1004. CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
  1005. if (!CVPixelBufferIsPlanar(imageBuffer)) {
  1006. frame->linesize[0] = (UInt32) CVPixelBufferGetBytesPerRow(imageBuffer);
  1007. frame->data[0] = CVPixelBufferGetBaseAddress(imageBuffer);
  1008. } else {
  1009. size_t planeCount = CVPixelBufferGetPlaneCount(imageBuffer);
  1010. for (size_t i = 0; i < planeCount; i++) {
  1011. frame->linesize[i] = (UInt32) CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, i);
  1012. frame->data[i] = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, i);
  1013. }
  1014. }
  1015. obs_source_output_video(_captureInfo->source, frame);
  1016. CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
  1017. } else {
  1018. obs_source_output_video(_captureInfo->source, NULL);
  1019. }
  1020. break;
  1021. }
  1022. }
  1023. case kCMMediaType_Audio: {
  1024. size_t requiredBufferListSize;
  1025. OSStatus status = noErr;
  1026. status = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
  1027. sampleBuffer, &requiredBufferListSize, NULL, 0, NULL, NULL,
  1028. kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, NULL);
  1029. if (status != noErr) {
  1030. _captureInfo->lastAudioError = OBSAVCaptureError_AudioBuffer;
  1031. obs_source_update_properties(_captureInfo->source);
  1032. break;
  1033. }
  1034. AudioBufferList *bufferList = (AudioBufferList *) malloc(requiredBufferListSize);
  1035. CMBlockBufferRef blockBuffer = NULL;
  1036. OSStatus error = noErr;
  1037. error = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
  1038. sampleBuffer, NULL, bufferList, requiredBufferListSize, kCFAllocatorSystemDefault,
  1039. kCFAllocatorSystemDefault, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer);
  1040. if (error == noErr) {
  1041. _captureInfo->lastAudioError = OBSAVCaptureError_NoError;
  1042. OBSAVCaptureAudioFrame *audio = _captureInfo->audioFrame;
  1043. for (size_t i = 0; i < bufferList->mNumberBuffers; i++) {
  1044. audio->data[i] = bufferList->mBuffers[i].mData;
  1045. }
  1046. audio->timestamp = presentationNanoTimeStamp.value;
  1047. audio->frames = (uint32_t) CMSampleBufferGetNumSamples(sampleBuffer);
  1048. const AudioStreamBasicDescription *basicDescription =
  1049. CMAudioFormatDescriptionGetStreamBasicDescription(description);
  1050. audio->samples_per_sec = (uint32_t) basicDescription->mSampleRate;
  1051. audio->speakers = (enum speaker_layout) basicDescription->mChannelsPerFrame;
  1052. switch (basicDescription->mBitsPerChannel) {
  1053. case 8:
  1054. audio->format = AUDIO_FORMAT_U8BIT;
  1055. break;
  1056. case 16:
  1057. audio->format = AUDIO_FORMAT_16BIT;
  1058. break;
  1059. case 32:
  1060. audio->format = AUDIO_FORMAT_32BIT;
  1061. break;
  1062. default:
  1063. audio->format = AUDIO_FORMAT_UNKNOWN;
  1064. break;
  1065. }
  1066. obs_source_output_audio(_captureInfo->source, audio);
  1067. } else {
  1068. _captureInfo->lastAudioError = OBSAVCaptureError_AudioBuffer;
  1069. obs_source_output_audio(_captureInfo->source, NULL);
  1070. }
  1071. if (blockBuffer != NULL) {
  1072. CFRelease(blockBuffer);
  1073. }
  1074. if (bufferList != NULL) {
  1075. free(bufferList);
  1076. bufferList = NULL;
  1077. }
  1078. break;
  1079. }
  1080. default:
  1081. break;
  1082. }
  1083. }
  1084. #pragma mark - Log Helpers
  1085. - (void)AVCaptureLog:(int)logLevel withFormat:(NSString *)format, ...
  1086. {
  1087. va_list args;
  1088. va_start(args, format);
  1089. NSString *logMessage = [[NSString alloc] initWithFormat:format arguments:args];
  1090. va_end(args);
  1091. const char *name_value = obs_source_get_name(self.captureInfo->source);
  1092. NSString *sourceName = @((name_value) ? name_value : "");
  1093. blog(logLevel, "%s: %s", sourceName.UTF8String, logMessage.UTF8String);
  1094. }
  1095. + (void)AVCaptureLog:(int)logLevel withFormat:(NSString *)format, ...
  1096. {
  1097. va_list args;
  1098. va_start(args, format);
  1099. NSString *logMessage = [[NSString alloc] initWithFormat:format arguments:args];
  1100. va_end(args);
  1101. blog(logLevel, "%s", logMessage.UTF8String);
  1102. }
  1103. @end