Browse Source

mac-virtualcam: Update obs-plugin to support macOS camera extensions

Co-authored-by: PatTheMav <[email protected]>
gxalpha 2 năm trước cách đây
mục cha
commit
aae2f7e8ff

+ 6 - 0
plugins/mac-virtualcam/src/dal-plugin/CMakeLists.txt

@@ -61,3 +61,9 @@ set_target_properties(
              XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER com.obsproject.obs-mac-virtualcam
              XCODE_ATTRIBUTE_CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION YES
              XCODE_ATTRIBUTE_GCC_WARN_SHADOW YES)
+
+set_target_properties_obs(
+  obs-dal-plugin
+  PROPERTIES OUTPUT_NAME obs-mac-virtualcam
+             FOLDER plugins
+             PREFIX "")

+ 11 - 11
plugins/mac-virtualcam/src/obs-plugin/CMakeLists.txt

@@ -2,23 +2,20 @@ cmake_minimum_required(VERSION 3.22...3.25)
 
 legacy_check()
 
-find_library(APPKIT AppKit)
-find_library(COREVIDEO CoreVideo)
-find_library(IOSURFACE IOSurface)
-mark_as_advanced(APPKIT COREVIDEO IOSURFACE)
-
 add_library(mac-virtualcam MODULE)
 add_library(OBS::virtualcam ALIAS mac-virtualcam)
 
 target_sources(mac-virtualcam PRIVATE Defines.h plugin-main.mm OBSDALMachServer.mm OBSDALMachServer.h)
-target_compile_features(mac-virtualcam PRIVATE cxx_deleted_functions cxx_rvalue_references cxx_std_17)
-if(NOT XCODE)
-  target_compile_options(mac-virtualcam PRIVATE -fobjc-arc -fobjc-weak)
+
+if(XCODE)
+  target_compile_options(mac-virtualcam PRIVATE -fmodules -fcxx-modules)
+else()
+  target_compile_options(mac-virtualcam PRIVATE -fobjc-arc -fobjc-weak -fmodules -fcxx-modules)
 endif()
 
-target_link_libraries(mac-virtualcam PRIVATE OBS::mach-protocol OBS::libobs OBS::frontend-api ${APPKIT} ${COREVIDEO}
-                                             ${IOSURFACE})
+target_link_libraries(mac-virtualcam PRIVATE OBS::mach-protocol OBS::libobs OBS::frontend-api)
 
+# cmake-format: off
 set_target_properties_obs(
   mac-virtualcam
   PROPERTIES FOLDER plugins
@@ -26,4 +23,7 @@ set_target_properties_obs(
              LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/../../"
              XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC YES
              XCODE_ATTRIBUTE_CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION YES
-             XCODE_ATTRIBUTE_GCC_WARN_SHADOW YES)
+             XCODE_ATTRIBUTE_GCC_WARN_SHADOW YES
+             XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES YES
+             XCODE_ATTRIBUTE_CLANG_MODULES_AUTOLINK YES)
+# cmake-format: on

+ 2 - 2
plugins/mac-virtualcam/src/obs-plugin/OBSDALMachServer.h

@@ -5,8 +5,8 @@
 //  Created by John Boiles  on 5/5/20.
 //
 
-#import <Foundation/Foundation.h>
-#import <CoreVideo/CoreVideo.h>
+@import Foundation;
+@import CoreVideo;
 
 NS_ASSUME_NONNULL_BEGIN
 

+ 0 - 1
plugins/mac-virtualcam/src/obs-plugin/OBSDALMachServer.mm

@@ -7,7 +7,6 @@
 
 #import "OBSDALMachServer.h"
 #include <obs-module.h>
-#include <CoreVideo/CoreVideo.h>
 #include "MachProtocol.h"
 #include "Defines.h"
 

+ 3 - 1
plugins/mac-virtualcam/src/obs-plugin/cmake/macos/Info.plist.in

@@ -23,6 +23,8 @@
 	<key>LSMinimumSystemVersion</key>
 	<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
 	<key>NSHumanReadableCopyright</key>
-	<string>(c) 2012-${CURRENT_YEAR} Hugh Bailey</string>
+	<string>(c) 2020-${CURRENT_YEAR} John Boiles, Patrick Heyer, Sebastian Beckmann</string>
+	<key>OBSCameraDeviceUUID</key>
+	<string>${VIRTUALCAM_DEVICE_UUID}</string>
 </dict>
 </plist>

+ 7 - 0
plugins/mac-virtualcam/src/obs-plugin/data/locale/en-US.ini

@@ -1 +1,8 @@
 Plugin_Name="macOS Virtual Webcam"
+Error.SystemExtension.NotInstalled="The virtual camera is not installed.\n\nPlease allow OBS to install system software in System Settings → Privacy & Security → Security.\n\nYou may need to restart OBS if this message still appears afterward."
+Error.SystemExtension.CameraUnavailable="Could not find virtual camera.\n\nPlease try again."
+Error.SystemExtension.CameraNotStarted="Unable to start virtual camera.\n\nPlease try again."
+Error.SystemExtension.InstallationError="An error has occured while installing the virtual camera:"
+Error.SystemExtension.WrongLocation="OBS cannot install the virtual camera if it's not in \"/Applications\". Please move OBS to the \"/Applications\" directory."
+Error.DAL.NotInstalled="The virtual camera could not be installed or updated.\n\nPlease try again and enter your administrator password when prompted."
+Error.DAL.NotUninstalled="Could not remove the legacy virtual camera.\n\nPlease try again and enter your account password when prompted."

+ 399 - 73
plugins/mac-virtualcam/src/obs-plugin/plugin-main.mm

@@ -1,3 +1,6 @@
+@import CoreMediaIO;
+@import SystemExtensions;
+
 #include <obs-module.h>
 #include "OBSDALMachServer.h"
 #include "Defines.h"
@@ -9,33 +12,164 @@ MODULE_EXPORT const char *obs_module_description(void)
 	return "macOS virtual webcam output";
 }
 
+NSString *const OBSDalDestination = @"/Library/CoreMediaIO/Plug-Ins/DAL";
+
+static bool cmio_extension_supported()
+{
+	if (@available(macOS 13.0, *)) {
+		return true;
+	} else {
+		return false;
+	}
+}
+
 struct virtualcam_data {
 	obs_output_t *output;
 	obs_video_info videoInfo;
 	CVPixelBufferPoolRef pool;
+
+	// CMIO Extension (available with macOS 13)
+	CMSimpleQueueRef queue;
+	CMIODeviceID deviceID;
+	CMIOStreamID streamID;
+	CMFormatDescriptionRef formatDescription;
+	id extensionDelegate;
+
+	// Legacy DAL (deprecated since macOS 12.3)
 	OBSDALMachServer *machServer;
 };
 
-static bool check_dal_plugin()
+@interface SystemExtensionActivationDelegate
+	: NSObject <OSSystemExtensionRequestDelegate> {
+@private
+	struct virtualcam_data *_vcam;
+}
+
+@property (getter=isInstalled) BOOL installed;
+@property NSString *lastErrorMessage;
+- (instancetype)init __unavailable;
+@end
+
+@implementation SystemExtensionActivationDelegate
+
+- (id)initWithVcam:(virtualcam_data *)vcam
+{
+	self = [super init];
+
+	if (self) {
+		_vcam = vcam;
+		_installed = NO;
+	}
+
+	return self;
+}
+
+- (OSSystemExtensionReplacementAction)
+			    request:(nonnull OSSystemExtensionRequest *)request
+	actionForReplacingExtension:
+		(nonnull OSSystemExtensionProperties *)existing
+		      withExtension:(nonnull OSSystemExtensionProperties *)ext
+{
+	NSString *extVersion =
+		[NSString stringWithFormat:@"%@.%@", [ext bundleShortVersion],
+					   [ext bundleVersion]];
+	NSString *existingVersion = [NSString
+		stringWithFormat:@"%@.%@", [existing bundleShortVersion],
+				 [existing bundleVersion]];
+
+	if ([extVersion compare:existingVersion
+			options:NSNumericSearch] == NSOrderedDescending) {
+		return OSSystemExtensionReplacementActionReplace;
+	} else {
+		return OSSystemExtensionReplacementActionCancel;
+	}
+}
+
+- (void)request:(nonnull OSSystemExtensionRequest *)request
+	didFailWithError:(nonnull NSError *)error
+{
+	NSString *errorMessage;
+	int severity;
+
+	switch (error.code) {
+	case OSSystemExtensionErrorRequestCanceled:
+		errorMessage =
+			@"macOS Camera Extension installation request cancelled.";
+		severity = LOG_INFO;
+		break;
+	case OSSystemExtensionErrorUnsupportedParentBundleLocation:
+		self.lastErrorMessage = [NSString
+			stringWithUTF8String:
+				obs_module_text(
+					"Error.SystemExtension.WrongLocation")];
+		errorMessage = self.lastErrorMessage;
+		severity = LOG_WARNING;
+		break;
+	default:
+		self.lastErrorMessage = error.localizedDescription;
+		errorMessage = [NSString
+			stringWithFormat:
+				@"OSSystemExtensionErrorCode %ld (\"%s\")",
+				error.code,
+				error.localizedDescription.UTF8String];
+		severity = LOG_ERROR;
+		break;
+	}
+
+	blog(severity, "mac-camera-extension error: %s",
+	     errorMessage.UTF8String);
+}
+
+- (void)request:(nonnull OSSystemExtensionRequest *)request
+	didFinishWithResult:(OSSystemExtensionRequestResult)result
+{
+	self.installed = YES;
+	blog(LOG_INFO, "macOS Camera Extension activated successfully.");
+}
+
+- (void)requestNeedsUserApproval:(nonnull OSSystemExtensionRequest *)request
+{
+	self.installed = NO;
+	blog(LOG_INFO, "macOS Camera Extension user approval required.");
+}
+
+@end
+
+static void install_cmio_system_extension(struct virtualcam_data *vcam)
+{
+	OSSystemExtensionRequest *request = [OSSystemExtensionRequest
+		activationRequestForExtension:
+			@"com.obsproject.obs-studio.mac-camera-extension"
+					queue:dispatch_get_main_queue()];
+	request.delegate = vcam->extensionDelegate;
+
+	[[OSSystemExtensionManager sharedManager] submitRequest:request];
+}
+
+typedef enum {
+	OBSDalPluginNotInstalled,
+	OBSDalPluginInstalled,
+	OBSDalPluginNeedsUpdate
+} dal_plugin_status;
+
+static dal_plugin_status check_dal_plugin()
 {
 	NSFileManager *fileManager = [NSFileManager defaultManager];
 
-	NSString *dalPluginDestinationPath =
-		@"/Library/CoreMediaIO/Plug-Ins/DAL/";
-	NSString *dalPluginFileName =
-		@"/Library/CoreMediaIO/Plug-Ins/DAL/obs-mac-virtualcam.plugin";
+	NSString *dalPluginFileName = [OBSDalDestination
+		stringByAppendingString:@"/obs-mac-virtualcam.plugin"];
 
-	BOOL dalPluginDirExists =
-		[fileManager fileExistsAtPath:dalPluginDestinationPath];
 	BOOL dalPluginInstalled =
 		[fileManager fileExistsAtPath:dalPluginFileName];
-	BOOL dalPluginUpdateNeeded = NO;
 
 	if (dalPluginInstalled) {
 		NSDictionary *dalPluginInfoPlist = [NSDictionary
 			dictionaryWithContentsOfURL:
 				[NSURL fileURLWithPath:
-						@"/Library/CoreMediaIO/Plug-Ins/DAL/obs-mac-virtualcam.plugin/Contents/Info.plist"]];
+						[OBSDalDestination
+							stringByAppendingString:
+								@"/obs-mac-virtualcam.plugin/Contents/Info.plist"]]];
+
 		NSString *dalPluginVersion = [dalPluginInfoPlist
 			valueForKey:@"CFBundleShortVersionString"];
 		NSString *dalPluginBuild =
@@ -45,64 +179,93 @@ static bool check_dal_plugin()
 			objectForKey:@"CFBundleShortVersionString"];
 		NSString *obsBuild = [[[NSBundle mainBundle] infoDictionary]
 			objectForKey:(NSString *)kCFBundleVersionKey];
-		dalPluginUpdateNeeded =
+		BOOL dalPluginUpdateNeeded =
 			!([dalPluginVersion isEqualToString:obsVersion] &&
 			  [dalPluginBuild isEqualToString:obsBuild]);
+
+		return dalPluginUpdateNeeded ? OBSDalPluginNeedsUpdate
+					     : OBSDalPluginInstalled;
 	}
 
-	if (!dalPluginInstalled || dalPluginUpdateNeeded) {
-		NSString *dalPluginSourcePath;
-
-		NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];
-		NSString *pluginPath =
-			@"Contents/Resources/obs-mac-virtualcam.plugin";
-
-		NSURL *pluginUrl =
-			[bundleURL URLByAppendingPathComponent:pluginPath];
-		dalPluginSourcePath = [pluginUrl path];
-
-		NSString *createPluginDirCmd =
-			(!dalPluginDirExists)
-				? [NSString stringWithFormat:
-						    @"mkdir -p '%@' && ",
-						    dalPluginDestinationPath]
-				: @"";
-		NSString *deleteOldPluginCmd =
-			(dalPluginUpdateNeeded)
-				? [NSString stringWithFormat:@"rm -rf '%@' && ",
-							     dalPluginFileName]
-				: @"";
-		NSString *copyPluginCmd =
-			[NSString stringWithFormat:@"cp -R '%@' '%@'",
-						   dalPluginSourcePath,
-						   dalPluginDestinationPath];
-		if ([fileManager fileExistsAtPath:dalPluginSourcePath]) {
-			NSString *copyCmd = [NSString
-				stringWithFormat:
-					@"do shell script \"%@%@%@\" with administrator privileges",
-					createPluginDirCmd, deleteOldPluginCmd,
-					copyPluginCmd];
-
-			NSDictionary *errorDict;
-			NSAppleScript *scriptObject =
-				[[NSAppleScript alloc] initWithSource:copyCmd];
-			[scriptObject executeAndReturnError:&errorDict];
-			if (errorDict != nil) {
-				const char *errorMessage = [[errorDict
-					objectForKey:@"NSAppleScriptErrorMessage"]
-					UTF8String];
-				blog(LOG_INFO,
-				     "[macOS] VirtualCam DAL Plugin Installation status: %s",
-				     errorMessage);
-				return false;
-			}
-		} else {
+	return OBSDalPluginNotInstalled;
+}
+
+static bool install_dal_plugin(bool update)
+{
+	NSFileManager *fileManager = [NSFileManager defaultManager];
+	BOOL dalPluginDirExists =
+		[fileManager fileExistsAtPath:OBSDalDestination];
+
+	NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];
+	NSString *pluginPath = @"Contents/Resources/obs-mac-virtualcam.plugin";
+
+	NSURL *pluginUrl = [bundleURL URLByAppendingPathComponent:pluginPath];
+	NSString *dalPluginSourcePath = [pluginUrl path];
+
+	NSString *createPluginDirCmd =
+		(!dalPluginDirExists)
+			? [NSString stringWithFormat:@"mkdir -p '%@' && ",
+						     OBSDalDestination]
+			: @"";
+	NSString *deleteOldPluginCmd =
+		(update) ? [NSString stringWithFormat:@"rm -rf '%@' && ",
+						      OBSDalDestination]
+			 : @"";
+	NSString *copyPluginCmd = [NSString
+		stringWithFormat:@"cp -R '%@' '%@'", dalPluginSourcePath,
+				 OBSDalDestination];
+
+	if ([fileManager fileExistsAtPath:dalPluginSourcePath]) {
+		NSString *copyCmd = [NSString
+			stringWithFormat:
+				@"do shell script \"%@%@%@\" with administrator privileges",
+				createPluginDirCmd, deleteOldPluginCmd,
+				copyPluginCmd];
+
+		NSDictionary *errorDict;
+		NSAppleScript *scriptObject =
+			[[NSAppleScript alloc] initWithSource:copyCmd];
+		[scriptObject executeAndReturnError:&errorDict];
+		if (errorDict != nil) {
+			const char *errorMessage = [[errorDict
+				objectForKey:@"NSAppleScriptErrorMessage"]
+				UTF8String];
+
 			blog(LOG_INFO,
-			     "[macOS] VirtualCam DAL Plugin not shipped with OBS");
+			     "[macOS] VirtualCam DAL Plugin Installation status: %s",
+			     errorMessage);
 			return false;
+		} else {
+			return true;
 		}
+	} else {
+		blog(LOG_INFO,
+		     "[macOS] VirtualCam DAL Plugin not shipped with OBS");
+		return false;
+	}
+}
+
+static bool uninstall_dal_plugin()
+{
+	NSAppleScript *scriptObject = [[NSAppleScript alloc]
+		initWithSource:
+			[NSString
+				stringWithFormat:
+					@"do shell script \"rm -rf %@/obs-mac-virtualcam.plugin\" with administrator privileges",
+					OBSDalDestination]];
+
+	NSDictionary *errorDict;
+
+	[scriptObject executeAndReturnError:&errorDict];
+	if (errorDict) {
+		blog(LOG_INFO,
+		     "[macOS] VirtualCam DAL Plugin could not be uninstalled: %s",
+		     [[errorDict objectForKey:NSAppleScriptErrorMessage]
+			     UTF8String]);
+		return false;
+	} else {
+		return true;
 	}
-	return true;
 }
 
 FourCharCode convert_video_format_to_mac(enum video_format format,
@@ -148,7 +311,16 @@ static void *virtualcam_output_create(obs_data_t *settings,
 		(struct virtualcam_data *)bzalloc(sizeof(*vcam));
 
 	vcam->output = output;
-	vcam->machServer = [[OBSDALMachServer alloc] init];
+
+	if (cmio_extension_supported()) {
+		vcam->extensionDelegate =
+			[[SystemExtensionActivationDelegate alloc]
+				initWithVcam:vcam];
+		install_cmio_system_extension(vcam);
+	} else {
+		vcam->machServer = [[OBSDALMachServer alloc] init];
+	}
+
 	return vcam;
 }
 
@@ -156,7 +328,12 @@ static void virtualcam_output_destroy(void *data)
 {
 	struct virtualcam_data *vcam = (struct virtualcam_data *)data;
 
-	vcam->machServer = nil;
+	if (cmio_extension_supported()) {
+		vcam->extensionDelegate = nil;
+	} else {
+		vcam->machServer = nil;
+	}
+
 	bfree(vcam);
 }
 
@@ -164,10 +341,57 @@ static bool virtualcam_output_start(void *data)
 {
 	struct virtualcam_data *vcam = (struct virtualcam_data *)data;
 
-	bool hasDalPlugin = check_dal_plugin();
+	dal_plugin_status dal_status = check_dal_plugin();
 
-	if (!hasDalPlugin) {
-		return false;
+	if (cmio_extension_supported()) {
+		if (dal_status != OBSDalPluginNotInstalled) {
+			if (!uninstall_dal_plugin()) {
+				obs_output_set_last_error(
+					vcam->output,
+					obs_module_text(
+						"Error.DAL.NotUninstalled"));
+				return false;
+			}
+		}
+
+		SystemExtensionActivationDelegate *delegate =
+			vcam->extensionDelegate;
+
+		if (!delegate.installed) {
+			if (delegate.lastErrorMessage) {
+				obs_output_set_last_error(
+					vcam->output,
+					[NSString
+						stringWithFormat:
+							@"%s\n\n%@",
+							obs_module_text(
+								"Error.SystemExtension.InstallationError"),
+							delegate.lastErrorMessage]
+						.UTF8String);
+			} else {
+				obs_output_set_last_error(
+					vcam->output,
+					obs_module_text(
+						"Error.SystemExtension.NotInstalled"));
+			}
+
+			return false;
+		}
+	} else {
+		bool success = false;
+		if (dal_status == OBSDalPluginNotInstalled) {
+			success = install_dal_plugin(false);
+		} else if (dal_status == OBSDalPluginNeedsUpdate) {
+			success = install_dal_plugin(true);
+		} else {
+			success = true;
+		}
+
+		if (!success) {
+			obs_output_set_last_error(vcam->output,
+						  "Error.DAL.NotInstalled");
+			return false;
+		}
 	}
 
 	obs_get_video_info(&vcam->videoInfo);
@@ -205,13 +429,99 @@ static bool virtualcam_output_start(void *data)
 	CVReturn status = CVPixelBufferPoolCreate(
 		kCFAllocatorDefault, (__bridge CFDictionaryRef)pAttr,
 		(__bridge CFDictionaryRef)pbAttr, &vcam->pool);
+
 	if (status != kCVReturnSuccess) {
 		blog(LOG_ERROR,
 		     "unable to allocate pixel buffer pool (error %d)", status);
 		return false;
 	}
 
-	[vcam->machServer run];
+	if (cmio_extension_supported()) {
+		UInt32 size;
+		UInt32 used;
+
+		CMIOObjectPropertyAddress address{
+			.mSelector = kCMIOHardwarePropertyDevices,
+			.mScope = kCMIOObjectPropertyScopeGlobal,
+			.mElement = kCMIOObjectPropertyElementMain};
+		CMIOObjectGetPropertyDataSize(kCMIOObjectSystemObject, &address,
+					      0, NULL, &size);
+		size_t num_devices = size / sizeof(CMIOObjectID);
+		CMIOObjectID cmio_devices[num_devices];
+		CMIOObjectGetPropertyData(kCMIOObjectSystemObject, &address, 0,
+					  NULL, size, &used, &cmio_devices);
+
+		vcam->deviceID = 0;
+
+		NSString *OBSVirtualCamUUID = [[NSBundle
+			bundleWithIdentifier:@"com.obsproject.mac-virtualcam"]
+			objectForInfoDictionaryKey:@"OBSCameraDeviceUUID"];
+		for (size_t i = 0; i < num_devices; i++) {
+			CMIOObjectID cmio_device = cmio_devices[i];
+			address.mSelector = kCMIODevicePropertyDeviceUID;
+
+			UInt32 device_name_size;
+			CMIOObjectGetPropertyDataSize(cmio_device, &address, 0,
+						      NULL, &device_name_size);
+
+			CFStringRef uid;
+			CMIOObjectGetPropertyData(cmio_device, &address, 0,
+						  NULL, device_name_size, &used,
+						  &uid);
+
+			const char *uid_string = CFStringGetCStringPtr(
+				uid, kCFStringEncodingUTF8);
+			if (uid_string &&
+			    strcmp(uid_string, OBSVirtualCamUUID.UTF8String) ==
+				    0) {
+				vcam->deviceID = cmio_device;
+				CFRelease(uid);
+				break;
+			} else {
+				CFRelease(uid);
+			}
+		}
+
+		if (!vcam->deviceID) {
+			obs_output_set_last_error(
+				vcam->output,
+				obs_module_text(
+					"Error.SystemExtension.CameraUnavailable"));
+			return false;
+		}
+
+		address.mSelector = kCMIODevicePropertyStreams;
+		CMIOObjectGetPropertyDataSize(vcam->deviceID, &address, 0, NULL,
+					      &size);
+		CMIOStreamID stream_ids[(size / sizeof(CMIOStreamID))];
+
+		CMIOObjectGetPropertyData(vcam->deviceID, &address, 0, NULL,
+					  size, &used, &stream_ids);
+
+		vcam->streamID = stream_ids[1];
+
+		CMIOStreamCopyBufferQueue(
+			vcam->streamID, [](CMIOStreamID, void *, void *) {},
+			NULL, &vcam->queue);
+		CMVideoFormatDescriptionCreate(kCFAllocatorDefault,
+					       video_format,
+					       vcam->videoInfo.output_width,
+					       vcam->videoInfo.output_height,
+					       NULL, &vcam->formatDescription);
+
+		OSStatus result =
+			CMIODeviceStartStream(vcam->deviceID, vcam->streamID);
+
+		if (result != noErr) {
+			obs_output_set_last_error(
+				vcam->output,
+				obs_module_text(
+					"Error.SystemExtension.CameraNotStarted"));
+			return false;
+		}
+	} else {
+		[vcam->machServer run];
+	}
 
 	if (!obs_output_begin_data_capture(vcam->output, 0)) {
 		return false;
@@ -227,9 +537,13 @@ static void virtualcam_output_stop(void *data, uint64_t ts)
 	struct virtualcam_data *vcam = (struct virtualcam_data *)data;
 
 	obs_output_end_data_capture(vcam->output);
-	[vcam->machServer stop];
-
-	CVPixelBufferPoolRelease(vcam->pool);
+	if (cmio_extension_supported()) {
+		CMIODeviceStopStream(vcam->deviceID, vcam->streamID);
+		CFRelease(vcam->formatDescription);
+		CVPixelBufferPoolRelease(vcam->pool);
+	} else {
+		[vcam->machServer stop];
+	}
 }
 
 static void virtualcam_output_raw_video(void *data, struct video_data *frame)
@@ -306,11 +620,23 @@ static void virtualcam_output_raw_video(void *data, struct video_data *frame)
 
 	CVPixelBufferUnlockBaseAddress(frameRef, 0);
 
-	// Share pixel buffer with clients
-	[vcam->machServer sendPixelBuffer:frameRef
-				timestamp:frame->timestamp
-			     fpsNumerator:vcam->videoInfo.fps_num
-			   fpsDenominator:vcam->videoInfo.fps_den];
+	if (cmio_extension_supported()) {
+		CMSampleBufferRef sampleBuffer;
+		CMSampleTimingInfo timingInfo{
+			.presentationTimeStamp =
+				CMTimeMake(frame->timestamp, NSEC_PER_SEC)};
+
+		OSStatus result = CMSampleBufferCreateForImageBuffer(
+			kCFAllocatorDefault, frameRef, true, NULL, NULL,
+			vcam->formatDescription, &timingInfo, &sampleBuffer);
+		result = CMSimpleQueueEnqueue(vcam->queue, sampleBuffer);
+	} else {
+		// Share pixel buffer with clients
+		[vcam->machServer sendPixelBuffer:frameRef
+					timestamp:frame->timestamp
+				     fpsNumerator:vcam->videoInfo.fps_num
+				   fpsDenominator:vcam->videoInfo.fps_den];
+	}
 
 	CVPixelBufferRelease(frameRef);
 }