Files
obs-studio/plugins/mac-virtualcam/src/obs-plugin/plugin-main.mm

555 lines
20 KiB
Plaintext

@import CoreMediaIO;
@import SystemExtensions;
#include <obs-module.h>
#include "OBSDALMachServer.h"
#include "Defines.h"
OBS_DECLARE_MODULE()
OBS_MODULE_USE_DEFAULT_LOCALE("mac-virtualcam", "en-US")
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;
};
@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 *dalPluginFileName = [OBSDalDestination stringByAppendingString:@"/obs-mac-virtualcam.plugin"];
BOOL dalPluginInstalled = [fileManager fileExistsAtPath:dalPluginFileName];
if (dalPluginInstalled) {
NSDictionary *dalPluginInfoPlist = [NSDictionary
dictionaryWithContentsOfURL:
[NSURL fileURLWithPath:[OBSDalDestination
stringByAppendingString:@"/obs-mac-virtualcam.plugin/Contents/Info.plist"]]];
NSString *dalPluginVersion = [dalPluginInfoPlist valueForKey:@"CFBundleShortVersionString"];
NSString *dalPluginBuild = [dalPluginInfoPlist valueForKey:@"CFBundleVersion"];
NSString *obsVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
NSString *obsBuild = [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *) kCFBundleVersionKey];
BOOL dalPluginUpdateNeeded =
!([dalPluginVersion isEqualToString:obsVersion] && [dalPluginBuild isEqualToString:obsBuild]);
return dalPluginUpdateNeeded ? OBSDalPluginNeedsUpdate : OBSDalPluginInstalled;
}
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 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;
}
}
FourCharCode convert_video_format_to_mac(enum video_format format, enum video_range_type range)
{
switch (format) {
case VIDEO_FORMAT_I420:
return (range == VIDEO_RANGE_FULL) ? kCVPixelFormatType_420YpCbCr8PlanarFullRange
: kCVPixelFormatType_420YpCbCr8Planar;
case VIDEO_FORMAT_NV12:
return (range == VIDEO_RANGE_FULL) ? kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
case VIDEO_FORMAT_UYVY:
return (range == VIDEO_RANGE_FULL) ? kCVPixelFormatType_422YpCbCr8FullRange : kCVPixelFormatType_422YpCbCr8;
case VIDEO_FORMAT_P010:
return (range == VIDEO_RANGE_FULL) ? kCVPixelFormatType_420YpCbCr10BiPlanarFullRange
: kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange;
default:
// Zero indicates that the format is not supported on macOS
// Note that some formats do have an associated constant, but
// constructing such formats fails with kCVReturnInvalidPixelFormat.
return 0;
}
}
static const char *virtualcam_output_get_name(void *type_data)
{
(void) type_data;
return obs_module_text("Plugin_Name");
}
static void *virtualcam_output_create(obs_data_t *settings, obs_output_t *output)
{
UNUSED_PARAMETER(settings);
struct virtualcam_data *vcam = (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;
}
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);
}
static bool virtualcam_output_start(void *data)
{
struct virtualcam_data *vcam = (struct virtualcam_data *) data;
dal_plugin_status dal_status = check_dal_plugin();
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);
FourCharCode video_format = convert_video_format_to_mac(vcam->videoInfo.output_format, vcam->videoInfo.range);
struct video_scale_info conversion = {};
conversion.width = vcam->videoInfo.output_width;
conversion.height = vcam->videoInfo.output_height;
conversion.colorspace = vcam->videoInfo.colorspace;
conversion.range = vcam->videoInfo.range;
if (!video_format) {
// Selected output format is not supported natively by CoreVideo, CPU conversion necessary
blog(LOG_WARNING, "Selected output format (%s) not supported by CoreVideo, enabling CPU transcoding...",
get_video_format_name(vcam->videoInfo.output_format));
conversion.format = VIDEO_FORMAT_NV12;
video_format = convert_video_format_to_mac(conversion.format, conversion.range);
} else {
conversion.format = vcam->videoInfo.output_format;
}
obs_output_set_video_conversion(vcam->output, &conversion);
NSDictionary *pAttr = @ {};
NSDictionary *pbAttr = @{
(id) kCVPixelBufferPixelFormatTypeKey: @(video_format),
(id) kCVPixelBufferWidthKey: @(vcam->videoInfo.output_width),
(id) kCVPixelBufferHeightKey: @(vcam->videoInfo.output_height),
(id) kCVPixelBufferIOSurfacePropertiesKey: @ {}
};
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;
}
return true;
}
static void virtualcam_output_stop(void *data, uint64_t ts)
{
UNUSED_PARAMETER(ts);
struct virtualcam_data *vcam = (struct virtualcam_data *) data;
obs_output_end_data_capture(vcam->output);
if (cmio_extension_supported()) {
CMIODeviceStopStream(vcam->deviceID, vcam->streamID);
CFRelease(vcam->formatDescription);
} else {
[vcam->machServer stop];
}
CVPixelBufferPoolRelease(vcam->pool);
}
static void virtualcam_output_raw_video(void *data, struct video_data *frame)
{
struct virtualcam_data *vcam = (struct virtualcam_data *) data;
CVPixelBufferRef frameRef = nil;
CVReturn status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, vcam->pool, &frameRef);
if (status != kCVReturnSuccess) {
blog(LOG_ERROR, "unable to allocate pixel buffer (error %d)", status);
return;
}
// Copy all planes into pixel buffer
size_t planeCount = CVPixelBufferGetPlaneCount(frameRef);
CVPixelBufferLockBaseAddress(frameRef, 0);
if (planeCount == 0) {
uint8_t *src = frame->data[0];
uint8_t *dst = (uint8_t *) CVPixelBufferGetBaseAddress(frameRef);
size_t destBytesPerRow = CVPixelBufferGetBytesPerRow(frameRef);
size_t srcBytesPerRow = frame->linesize[0];
size_t height = CVPixelBufferGetHeight(frameRef);
// Sometimes CVPixelBufferCreate will create a pixel buffer that's a different
// size than necessary to hold the frame (probably for some optimization reason).
// If that is the case this will do a row-by-row copy into the buffer.
if (destBytesPerRow == srcBytesPerRow) {
memcpy(dst, src, destBytesPerRow * height);
} else {
for (int line = 0; (size_t) line < height; line++) {
memcpy(dst, src, srcBytesPerRow);
src += srcBytesPerRow;
dst += destBytesPerRow;
}
}
} else {
for (size_t plane = 0; plane < planeCount; plane++) {
uint8_t *src = frame->data[plane];
if (!src) {
blog(LOG_WARNING, "Video data from OBS contains less planes than CVPixelBuffer");
break;
}
uint8_t *dst = (uint8_t *) CVPixelBufferGetBaseAddressOfPlane(frameRef, plane);
size_t destBytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(frameRef, plane);
size_t srcBytesPerRow = frame->linesize[plane];
size_t height = CVPixelBufferGetHeightOfPlane(frameRef, plane);
if (destBytesPerRow == srcBytesPerRow) {
memcpy(dst, src, destBytesPerRow * height);
} else {
for (int line = 0; (size_t) line < height; line++) {
memcpy(dst, src, srcBytesPerRow);
src += srcBytesPerRow;
dst += destBytesPerRow;
}
}
}
}
CVPixelBufferUnlockBaseAddress(frameRef, 0);
if (cmio_extension_supported()) {
CMSampleBufferRef sampleBuffer;
CMSampleTimingInfo timingInfo {.presentationTimeStamp = CMTimeMake(frame->timestamp, NSEC_PER_SEC)};
CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, frameRef, true, NULL, NULL, vcam->formatDescription,
&timingInfo, &sampleBuffer);
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);
}
struct obs_output_info virtualcam_output_info = {
.id = "virtualcam_output",
.flags = OBS_OUTPUT_VIDEO,
.get_name = virtualcam_output_get_name,
.create = virtualcam_output_create,
.destroy = virtualcam_output_destroy,
.start = virtualcam_output_start,
.stop = virtualcam_output_stop,
.raw_video = virtualcam_output_raw_video,
};
bool obs_module_load(void)
{
obs_register_output(&virtualcam_output_info);
obs_data_t *obs_settings = obs_data_create();
obs_data_set_bool(obs_settings, "vcamEnabled", true);
obs_apply_private_data(obs_settings);
obs_data_release(obs_settings);
return true;
}