obs-studio/plugins/mac-virtualcam/src/obs-plugin/plugin-main.mm
gxalpha 79822a58c3 libobs,plugins: Remove new obs_output_*2 functions
Effectively reverting parts of d314d47, this commit removes the new
functions that got added to remove the flags parameter. Instead, it just
marks the parameter as unused and documents this. Having what is
effectively an API break just to remove a parameter is a bit overkill.
The other parts of d314d47 which cleaned up the usage of the flags
parameter are untouched here.
2023-06-10 16:13:05 -07:00

666 lines
18 KiB
Text

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