0
0
mirror of https://github.com/obsproject/obs-studio.git synced 2024-09-20 04:42:18 +02:00

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

Co-authored-by: PatTheMav <PatTheMav@users.noreply.github.com>
This commit is contained in:
gxalpha 2023-04-01 16:35:47 +02:00 committed by Ryan Foster
parent 5c6e471a56
commit aae2f7e8ff
7 changed files with 424 additions and 84 deletions

View File

@ -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 "")

View File

@ -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

View File

@ -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

View File

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

View File

@ -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>

View File

@ -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."

View File

@ -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,37 +179,42 @@ 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;
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";
NSString *pluginPath = @"Contents/Resources/obs-mac-virtualcam.plugin";
NSURL *pluginUrl =
[bundleURL URLByAppendingPathComponent:pluginPath];
dalPluginSourcePath = [pluginUrl path];
NSURL *pluginUrl = [bundleURL URLByAppendingPathComponent:pluginPath];
NSString *dalPluginSourcePath = [pluginUrl path];
NSString *createPluginDirCmd =
(!dalPluginDirExists)
? [NSString stringWithFormat:
@"mkdir -p '%@' && ",
dalPluginDestinationPath]
? [NSString stringWithFormat:@"mkdir -p '%@' && ",
OBSDalDestination]
: @"";
NSString *deleteOldPluginCmd =
(dalPluginUpdateNeeded)
? [NSString stringWithFormat:@"rm -rf '%@' && ",
dalPluginFileName]
(update) ? [NSString stringWithFormat:@"rm -rf '%@' && ",
OBSDalDestination]
: @"";
NSString *copyPluginCmd =
[NSString stringWithFormat:@"cp -R '%@' '%@'",
dalPluginSourcePath,
dalPluginDestinationPath];
NSString *copyPluginCmd = [NSString
stringWithFormat:@"cp -R '%@' '%@'", dalPluginSourcePath,
OBSDalDestination];
if ([fileManager fileExistsAtPath:dalPluginSourcePath]) {
NSString *copyCmd = [NSString
stringWithFormat:
@ -91,10 +230,13 @@ static bool check_dal_plugin()
const char *errorMessage = [[errorDict
objectForKey:@"NSAppleScriptErrorMessage"]
UTF8String];
blog(LOG_INFO,
"[macOS] VirtualCam DAL Plugin Installation status: %s",
errorMessage);
return false;
} else {
return true;
}
} else {
blog(LOG_INFO,
@ -102,8 +244,29 @@ static bool check_dal_plugin()
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;
}
}
FourCharCode convert_video_format_to_mac(enum video_format format,
enum video_range_type range)
@ -148,7 +311,16 @@ static void *virtualcam_output_create(obs_data_t *settings,
(struct virtualcam_data *)bzalloc(sizeof(*vcam));
vcam->output = output;
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;
if (cmio_extension_supported()) {
vcam->extensionDelegate = nil;
} else {
vcam->machServer = nil;
}
bfree(vcam);
}
@ -164,11 +341,58 @@ 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) {
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;
}
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];
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);
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);
}