Add virtualcam plugin to OBS codebase

Co-authored-by: lvsti <lvsti@users.noreply.github.com>
Co-authored-by: Sebastian Beckmann <beckmann.sebastian@outlook.de>
Co-authored-by: Stefan Huber <sh@signalwerk.ch>
Co-authored-by: Ryohei Ikegami <iofg2100@gmail.com>
Co-authored-by: Colin Dean <colin.dean@target.com>
Co-authored-by: Wolfgang Ladermann <extern.ladermann_wolfgang@allianz.de>
Co-authored-by: Simon Eves <simon.eves@omnisci.com>
Co-authored-by: Colin Nelson <colnnelson@google.com>
Co-authored-by: Yoshimasa Niwa <niw@niw.at>
Co-authored-by: Michael Karliner <mike@modern-industry.com>
Co-authored-by: Jason Grout <jgrout6@bloomberg.net>
Co-authored-by: Alfredo Inostroza <jadenguy@gmail.com>
Co-authored-by: Daniel Kennett <daniel@cascable.se>
Co-authored-by: Gary Ewan Park <gep13@gep13.co.uk>
Co-authored-by: José Carlos Cieni Júnior <cienijr@outlook.com>
This commit is contained in:
John Boiles 2020-10-20 13:50:06 +02:00 committed by Jim
parent b32abbe33f
commit 2700db9ff9
34 changed files with 4682 additions and 0 deletions

View File

@ -215,6 +215,7 @@ jobs:
-x ./OBS.app/Contents/PlugIns/mac-decklink.so \
-x ./OBS.app/Contents/PlugIns/mac-syphon.so \
-x ./OBS.app/Contents/PlugIns/mac-vth264.so \
-x ./OBS.app/Contents/PlugIns/mac-virtualcam.so \
-x ./OBS.app/Contents/PlugIns/obs-browser.so \
-x ./OBS.app/Contents/PlugIns/obs-browser-page \
-x ./OBS.app/Contents/PlugIns/obs-ffmpeg.so \
@ -266,6 +267,8 @@ jobs:
codesign --force --options runtime --sign "${SIGN_IDENTITY:--}" "./OBS.app/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libswiftshader_libGLESv2.dylib"
codesign --force --options runtime --sign "${SIGN_IDENTITY:--}" --deep "./OBS.app/Contents/Frameworks/Chromium Embedded Framework.framework"
codesign --force --options runtime --deep --sign "${SIGN_IDENTITY:--}" "./OBS.app/Contents/Resources/data/obs-mac-virtualcam.plugin"
codesign --force --options runtime --entitlements "../CI/scripts/macos/app/entitlements.plist" --sign "${SIGN_IDENTITY:--}" --deep ./OBS.app
codesign -dvv ./OBS.app

View File

@ -318,6 +318,7 @@ bundle_dylibs() {
-x ./OBS.app/Contents/PlugIns/mac-decklink.so \
-x ./OBS.app/Contents/PlugIns/mac-syphon.so \
-x ./OBS.app/Contents/PlugIns/mac-vth264.so \
-x ./OBS.app/Contents/PlugIns/mac-virtualcam.so \
-x ./OBS.app/Contents/PlugIns/obs-browser.so \
-x ./OBS.app/Contents/PlugIns/obs-browser-page \
-x ./OBS.app/Contents/PlugIns/obs-ffmpeg.so \
@ -508,6 +509,11 @@ codesign_bundle() {
codesign --force --options runtime --sign "${CODESIGN_IDENT}" --deep "./OBS.app/Contents/Frameworks/Chromium Embedded Framework.framework"
echo -n "${COLOR_RESET}"
step "Code-sign DAL Plugin..."
echo -n "${COLOR_ORANGE}"
codesign --force --options runtime --deep --sign "${CODESIGN_IDENT}" "./OBS.app/Contents/Resources/data/obs-mac-virtualcam.plugin"
echo -n "${COLOR_RESET}"
step "Code-sign OBS code..."
echo -n "${COLOR_ORANGE}"
codesign --force --options runtime --entitlements "${CI_SCRIPTS}/app/entitlements.plist" --sign "${CODESIGN_IDENT}" --deep ./OBS.app

View File

@ -30,6 +30,7 @@ elseif(APPLE)
add_subdirectory(mac-capture)
add_subdirectory(mac-vth264)
add_subdirectory(mac-syphon)
add_subdirectory(mac-virtualcam)
add_subdirectory(decklink/mac)
add_subdirectory(vlc-video)
add_subdirectory(linux-jack)

View File

@ -0,0 +1,2 @@
add_subdirectory(src/obs-plugin)
add_subdirectory(src/dal-plugin)

View File

@ -0,0 +1,17 @@
//
// MachProtocol.m
// obs-mac-virtualcam
//
// Created by John Boiles on 5/5/20.
//
#define MACH_SERVICE_NAME "com.obsproject.obs-mac-virtualcam.server"
typedef enum {
//! Initial connect message sent from the client to the server to initate a connection
MachMsgIdConnect = 1,
//! Message containing data for a frame
MachMsgIdFrame = 2,
//! Indicates the server is going to stop sending frames
MachMsgIdStop = 3,
} MachMsgId;

View File

@ -0,0 +1,22 @@
//
// CMSampleBufferUtils.h
// dal-plugin
//
// Created by John Boiles on 5/8/20.
//
#include <CoreMediaIO/CMIOSampleBuffer.h>
OSStatus CMSampleBufferCreateFromData(NSSize size,
CMSampleTimingInfo timingInfo,
UInt64 sequenceNumber, NSData *data,
CMSampleBufferRef *sampleBuffer);
OSStatus CMSampleBufferCreateFromDataNoCopy(NSSize size,
CMSampleTimingInfo timingInfo,
UInt64 sequenceNumber, NSData *data,
CMSampleBufferRef *sampleBuffer);
CMSampleTimingInfo CMSampleTimingInfoForTimestamp(uint64_t timestampNanos,
uint32_t fpsNumerator,
uint32_t fpsDenominator);

View File

@ -0,0 +1,187 @@
//
// CMSampleBufferUtils.m
// dal-plugin
//
// Created by John Boiles on 5/8/20.
//
#import "CMSampleBufferUtils.h"
#include "Logging.h"
/*!
CMSampleBufferCreateFromData
Creates a CMSampleBuffer by copying bytes from NSData into a CVPixelBuffer.
*/
OSStatus CMSampleBufferCreateFromData(NSSize size,
CMSampleTimingInfo timingInfo,
UInt64 sequenceNumber, NSData *data,
CMSampleBufferRef *sampleBuffer)
{
OSStatus err = noErr;
// Create an empty pixel buffer
CVPixelBufferRef pixelBuffer;
err = CVPixelBufferCreate(kCFAllocatorDefault, size.width, size.height,
kCVPixelFormatType_422YpCbCr8, nil,
&pixelBuffer);
if (err != noErr) {
DLog(@"CVPixelBufferCreate err %d", err);
return err;
}
// Generate the video format description from that pixel buffer
CMFormatDescriptionRef format;
err = CMVideoFormatDescriptionCreateForImageBuffer(NULL, pixelBuffer,
&format);
if (err != noErr) {
DLog(@"CMVideoFormatDescriptionCreateForImageBuffer err %d",
err);
return err;
}
// Copy memory into the pixel buffer
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
uint8_t *dest =
(uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
uint8_t *src = (uint8_t *)data.bytes;
size_t destBytesPerRow =
CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
size_t srcBytesPerRow = size.width * 2;
// Sometimes CVPixelBufferCreate will create a pixelbuffer 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(dest, src, data.length);
} else {
for (int line = 0; line < size.height; line++) {
memcpy(dest, src, srcBytesPerRow);
src += srcBytesPerRow;
dest += destBytesPerRow;
}
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
err = CMIOSampleBufferCreateForImageBuffer(kCFAllocatorDefault,
pixelBuffer, format,
&timingInfo, sequenceNumber,
0, sampleBuffer);
CFRelease(format);
CFRelease(pixelBuffer);
if (err != noErr) {
DLog(@"CMIOSampleBufferCreateForImageBuffer err %d", err);
return err;
}
return noErr;
}
static void releaseNSData(void *o, void *block, size_t size)
{
NSData *data = (__bridge_transfer NSData *)o;
data = nil; // Assuming ARC is enabled
}
// From https://stackoverflow.com/questions/26158253/how-to-create-a-cmblockbufferref-from-nsdata
OSStatus createReadonlyBlockBuffer(CMBlockBufferRef *result, NSData *data)
{
CMBlockBufferCustomBlockSource blockSource = {
.version = kCMBlockBufferCustomBlockSourceVersion,
.AllocateBlock = NULL,
.FreeBlock = &releaseNSData,
.refCon = (__bridge_retained void *)data,
};
return CMBlockBufferCreateWithMemoryBlock(NULL, (void *)data.bytes,
data.length, NULL,
&blockSource, 0, data.length,
0, result);
}
/*!
CMSampleBufferCreateFromDataNoCopy
Creates a CMSampleBuffer by using the bytes directly from NSData (without copying them).
Seems to mostly work but does not work at full resolution in OBS for some reason (which prevents loopback testing).
*/
OSStatus CMSampleBufferCreateFromDataNoCopy(NSSize size,
CMSampleTimingInfo timingInfo,
UInt64 sequenceNumber, NSData *data,
CMSampleBufferRef *sampleBuffer)
{
OSStatus err = noErr;
CMBlockBufferRef dataBuffer;
createReadonlyBlockBuffer(&dataBuffer, data);
// Magic format properties snagged from https://github.com/lvsti/CoreMediaIO-DAL-Example/blob/0392cbf27ed33425a1a5bd9f495b2ccec8f20501/Sources/Extras/CoreMediaIO/DeviceAbstractionLayer/Devices/Sample/PlugIn/CMIO_DP_Sample_Stream.cpp#L830
NSDictionary *extensions = @{
@"com.apple.cmio.format_extension.video.only_has_i_frames":
@YES,
(__bridge NSString *)
kCMFormatDescriptionExtension_FieldCount: @1,
(__bridge NSString *)
kCMFormatDescriptionExtension_ColorPrimaries:
(__bridge NSString *)
kCMFormatDescriptionColorPrimaries_SMPTE_C,
(__bridge NSString *)
kCMFormatDescriptionExtension_TransferFunction: (
__bridge NSString *)
kCMFormatDescriptionTransferFunction_ITU_R_709_2,
(__bridge NSString *)
kCMFormatDescriptionExtension_YCbCrMatrix: (__bridge NSString *)
kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4,
(__bridge NSString *)
kCMFormatDescriptionExtension_BytesPerRow: @(size.width * 2),
(__bridge NSString *)kCMFormatDescriptionExtension_FormatName:
@"Component Video - CCIR-601 uyvy",
(__bridge NSString *)kCMFormatDescriptionExtension_Version: @2,
};
CMFormatDescriptionRef format;
err = CMVideoFormatDescriptionCreate(
NULL, kCMVideoCodecType_422YpCbCr8, size.width, size.height,
(__bridge CFDictionaryRef)extensions, &format);
if (err != noErr) {
DLog(@"CMVideoFormatDescriptionCreate err %d", err);
return err;
}
size_t dataSize = data.length;
err = CMIOSampleBufferCreate(kCFAllocatorDefault, dataBuffer, format, 1,
1, &timingInfo, 1, &dataSize,
sequenceNumber, 0, sampleBuffer);
CFRelease(format);
CFRelease(dataBuffer);
if (err != noErr) {
DLog(@"CMIOSampleBufferCreate err %d", err);
return err;
}
return noErr;
}
CMSampleTimingInfo CMSampleTimingInfoForTimestamp(uint64_t timestampNanos,
uint32_t fpsNumerator,
uint32_t fpsDenominator)
{
// The timing here is quite important. For frames to be delivered correctly and successfully be recorded by apps
// like QuickTime Player, we need to be accurate in both our timestamps _and_ have a sensible scale. Using large
// timestamps and scales like mach_absolute_time() and NSEC_PER_SEC will work for display, but will error out
// when trying to record.
//
// 600 is a commmon default in Apple's docs https://developer.apple.com/documentation/avfoundation/avmutablemovie/1390622-timescale
CMTimeScale scale = 600;
CMSampleTimingInfo timing;
timing.duration =
CMTimeMake(fpsDenominator * scale, fpsNumerator * scale);
timing.presentationTimeStamp = CMTimeMake(
(timestampNanos / (double)NSEC_PER_SEC) * scale, scale);
timing.decodeTimeStamp = kCMTimeInvalid;
return timing;
}

View File

@ -0,0 +1,108 @@
project(mac-dal-plugin)
find_library(AVFOUNDATION AVFoundation)
find_library(COCOA Cocoa)
find_library(COREFOUNDATION CoreFoundation)
find_library(COREMEDIA CoreMedia)
find_library(COREVIDEO CoreVideo)
find_library(COCOA Cocoa)
find_library(COREMEDIAIO CoreMediaIO)
find_library(IOSURFACE IOSurface)
find_library(IOKIT IOKit)
# Possible we could remove osme of these
include_directories(${AVFOUNDATION}
${COCOA}
${COREFOUNDATION}
${COREMEDIA}
${COREVIDEO}
${COREMEDIAIO}
${COCOA}
${IOSURFACE}
./
../common)
set(mac-dal-plugin_HEADERS
Defines.h
Logging.h
PlugInInterface.h
ObjectStore.h
PlugIn.h
Device.h
Stream.h
CMSampleBufferUtils.h
MachClient.h
TestCard.h
../common/MachProtocol.h)
set(mac-dal-plugin_SOURCES
PlugInMain.mm
PlugInInterface.mm
ObjectStore.mm
PlugIn.mm
Device.mm
Stream.mm
CMSampleBufferUtils.mm
MachClient.mm
TestCard.mm)
add_library(mac-dal-plugin MODULE
${mac-dal-plugin_SOURCES}
${mac-dal-plugin_HEADERS})
set_target_properties(mac-dal-plugin PROPERTIES
FOLDER "plugins"
BUNDLE TRUE
OUTPUT_NAME "obs-mac-virtualcam"
COMPILE_FLAGS "-std=gnu++14 -stdlib=libc++ -fobjc-arc -fobjc-weak")
if (XCODE)
set(TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/Debug")
else (XCODE)
set(TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}")
endif (XCODE)
target_link_libraries(mac-dal-plugin
${AVFOUNDATION}
${COCOA}
${COREFOUNDATION}
${COREMEDIA}
${COREVIDEO}
${COREMEDIAIO}
${IOSURFACE}
${IOKIT})
add_custom_command(TARGET mac-dal-plugin
POST_BUILD
COMMAND rm -rf ${TARGET_DIR}/obs-mac-virtualcam.plugin || true
COMMAND ${CMAKE_COMMAND} -E copy_directory ${TARGET_DIR}/obs-mac-virtualcam.bundle ${TARGET_DIR}/obs-mac-virtualcam.plugin
COMMENT "Rename bundle to plugin"
)
# Note: Xcode seems to run a command `builtin-infoPlistUtility` to generate the Info.plist, but I'm
# not sure where to find that binary. If we had access to it, the command would look something like:
# builtin-infoPlistUtility ${PROJECT_SOURCE_DIR}/../common/CoreMediaIO/DeviceAbstractionLayer/Devices/Sample/PlugIn/SampleVCam-Info.plist -producttype com.apple.product-type.bundle -expandbuildsettings -platform macosx -o mac-virtualcam.bundle/Contents/Info.plist
# Instead, just copy in one that was already generated from Xcode.
add_custom_command(TARGET mac-dal-plugin
POST_BUILD
COMMAND cp ${PROJECT_SOURCE_DIR}/Info.plist ${TARGET_DIR}/obs-mac-virtualcam.plugin/Contents/Info.plist
COMMAND mkdir ${TARGET_DIR}/obs-mac-virtualcam.plugin/Contents/Resources
COMMAND cp ${PROJECT_SOURCE_DIR}/placeholder.png ${TARGET_DIR}/obs-mac-virtualcam.plugin/Contents/Resources/placeholder.png
COMMAND /usr/bin/plutil -insert CFBundleVersion -string "${OBS_VERSION}" ${TARGET_DIR}/obs-mac-virtualcam.plugin/Contents/Info.plist
COMMAND /usr/bin/plutil -insert CFBundleShortVersionString -string "${OBS_VERSION}" ${TARGET_DIR}/obs-mac-virtualcam.plugin/Contents/Info.plist
DEPENDS {PROJECT_SOURCE_DIR}/Info.plist
COMMENT "Copy in Info.plist"
)
add_custom_command(TARGET mac-dal-plugin
POST_BUILD
COMMAND /usr/bin/codesign --force --deep --sign - --timestamp=none ${TARGET_DIR}/obs-mac-virtualcam.plugin
COMMENT "Codesign plugin"
)
add_custom_command(TARGET mac-dal-plugin
POST_BUILD
COMMAND rm -rf "${OBS_OUTPUT_DIR}/$<CONFIGURATION>/data/obs-mac-virtualcam.plugin" || true
COMMAND ${CMAKE_COMMAND} -E copy_directory ${TARGET_DIR}/obs-mac-virtualcam.plugin "${OBS_OUTPUT_DIR}/$<CONFIGURATION>/data/obs-mac-virtualcam.plugin"
COMMENT "Copy plugin to destination"
)

View File

@ -0,0 +1,21 @@
//
// Defines.h
// obs-mac-virtualcam
//
// Created by John Boiles on 5/27/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#define PLUGIN_NAME @"mac-virtualcam"
#define PLUGIN_VERSION @"1.3.0"

View File

@ -0,0 +1,34 @@
//
// Device.h
// obs-mac-virtualcam
//
// Created by John Boiles on 4/10/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#import <Foundation/Foundation.h>
#import "ObjectStore.h"
NS_ASSUME_NONNULL_BEGIN
@interface Device : NSObject <CMIOObject>
@property CMIOObjectID objectId;
@property CMIOObjectID pluginId;
@property CMIOObjectID streamId;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,295 @@
//
// Device.mm
// obs-mac-virtualcam
//
// Created by John Boiles on 4/10/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#import "Device.h"
#import <CoreFoundation/CoreFoundation.h>
#include <IOKit/audio/IOAudioTypes.h>
#import "PlugIn.h"
#import "Logging.h"
@interface Device ()
@property BOOL excludeNonDALAccess;
@property pid_t masterPid;
@end
@implementation Device
// Note that the DAL's API calls HasProperty before calling GetPropertyDataSize. This means that it can be assumed that address is valid for the property involved.
- (UInt32)getPropertyDataSizeWithAddress:(CMIOObjectPropertyAddress)address
qualifierDataSize:(UInt32)qualifierDataSize
qualifierData:(nonnull const void *)qualifierData
{
switch (address.mSelector) {
case kCMIOObjectPropertyName:
return sizeof(CFStringRef);
case kCMIOObjectPropertyManufacturer:
return sizeof(CFStringRef);
case kCMIOObjectPropertyElementCategoryName:
return sizeof(CFStringRef);
case kCMIOObjectPropertyElementNumberName:
return sizeof(CFStringRef);
case kCMIODevicePropertyPlugIn:
return sizeof(CMIOObjectID);
case kCMIODevicePropertyDeviceUID:
return sizeof(CFStringRef);
case kCMIODevicePropertyModelUID:
return sizeof(CFStringRef);
case kCMIODevicePropertyTransportType:
return sizeof(UInt32);
case kCMIODevicePropertyDeviceIsAlive:
return sizeof(UInt32);
case kCMIODevicePropertyDeviceHasChanged:
return sizeof(UInt32);
case kCMIODevicePropertyDeviceIsRunning:
return sizeof(UInt32);
case kCMIODevicePropertyDeviceIsRunningSomewhere:
return sizeof(UInt32);
case kCMIODevicePropertyDeviceCanBeDefaultDevice:
return sizeof(UInt32);
case kCMIODevicePropertyHogMode:
return sizeof(pid_t);
case kCMIODevicePropertyLatency:
return sizeof(UInt32);
case kCMIODevicePropertyStreams:
// Only one stream
return sizeof(CMIOStreamID) * 1;
case kCMIODevicePropertyStreamConfiguration:
// Only one stream
return sizeof(UInt32) + (sizeof(UInt32) * 1);
case kCMIODevicePropertyExcludeNonDALAccess:
return sizeof(UInt32);
case kCMIODevicePropertyCanProcessAVCCommand:
return sizeof(Boolean);
case kCMIODevicePropertyCanProcessRS422Command:
return sizeof(Boolean);
case kCMIODevicePropertyLinkedCoreAudioDeviceUID:
return sizeof(CFStringRef);
case kCMIODevicePropertyDeviceMaster:
return sizeof(pid_t);
default:
DLog(@"Device unhandled getPropertyDataSizeWithAddress for %@",
[ObjectStore
StringFromPropertySelector:address.mSelector]);
};
return 0;
}
// Note that the DAL's API calls HasProperty before calling GetPropertyData. This means that it can be assumed that address is valid for the property involved.
- (void)getPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
qualifierDataSize:(UInt32)qualifierDataSize
qualifierData:(nonnull const void *)qualifierData
dataSize:(UInt32)dataSize
dataUsed:(nonnull UInt32 *)dataUsed
data:(nonnull void *)data
{
switch (address.mSelector) {
case kCMIOObjectPropertyName:
*static_cast<CFStringRef *>(data) = CFSTR("OBS Virtual Camera");
*dataUsed = sizeof(CFStringRef);
break;
case kCMIOObjectPropertyManufacturer:
*static_cast<CFStringRef *>(data) = CFSTR("John Boiles");
*dataUsed = sizeof(CFStringRef);
break;
case kCMIOObjectPropertyElementCategoryName:
*static_cast<CFStringRef *>(data) = CFSTR("Virtual Camera");
*dataUsed = sizeof(CFStringRef);
break;
case kCMIOObjectPropertyElementNumberName:
*static_cast<CFStringRef *>(data) = CFSTR("0001");
*dataUsed = sizeof(CFStringRef);
break;
case kCMIODevicePropertyPlugIn:
*static_cast<CMIOObjectID *>(data) = self.pluginId;
*dataUsed = sizeof(CMIOObjectID);
break;
case kCMIODevicePropertyDeviceUID:
*static_cast<CFStringRef *>(data) =
CFSTR("obs-virtual-cam-device");
*dataUsed = sizeof(CFStringRef);
break;
case kCMIODevicePropertyModelUID:
*static_cast<CFStringRef *>(data) =
CFSTR("obs-virtual-cam-model");
*dataUsed = sizeof(CFStringRef);
break;
case kCMIODevicePropertyTransportType:
*static_cast<UInt32 *>(data) =
kIOAudioDeviceTransportTypeBuiltIn;
*dataUsed = sizeof(UInt32);
break;
case kCMIODevicePropertyDeviceIsAlive:
*static_cast<UInt32 *>(data) = 1;
*dataUsed = sizeof(UInt32);
break;
case kCMIODevicePropertyDeviceHasChanged:
*static_cast<UInt32 *>(data) = 0;
*dataUsed = sizeof(UInt32);
break;
case kCMIODevicePropertyDeviceIsRunning:
*static_cast<UInt32 *>(data) = 1;
*dataUsed = sizeof(UInt32);
break;
case kCMIODevicePropertyDeviceIsRunningSomewhere:
*static_cast<UInt32 *>(data) = 1;
*dataUsed = sizeof(UInt32);
break;
case kCMIODevicePropertyDeviceCanBeDefaultDevice:
*static_cast<UInt32 *>(data) = 1;
*dataUsed = sizeof(UInt32);
break;
case kCMIODevicePropertyHogMode:
*static_cast<pid_t *>(data) = -1;
*dataUsed = sizeof(pid_t);
break;
case kCMIODevicePropertyLatency:
*static_cast<UInt32 *>(data) = 0;
*dataUsed = sizeof(UInt32);
break;
case kCMIODevicePropertyStreams:
*static_cast<CMIOObjectID *>(data) = self.streamId;
*dataUsed = sizeof(CMIOObjectID);
break;
case kCMIODevicePropertyStreamConfiguration:
DLog(@"TODO kCMIODevicePropertyStreamConfiguration");
break;
case kCMIODevicePropertyExcludeNonDALAccess:
*static_cast<UInt32 *>(data) = self.excludeNonDALAccess ? 1 : 0;
*dataUsed = sizeof(UInt32);
break;
case kCMIODevicePropertyCanProcessAVCCommand:
*static_cast<Boolean *>(data) = false;
*dataUsed = sizeof(Boolean);
break;
case kCMIODevicePropertyCanProcessRS422Command:
*static_cast<Boolean *>(data) = false;
*dataUsed = sizeof(Boolean);
break;
case kCMIODevicePropertyDeviceMaster:
*static_cast<pid_t *>(data) = self.masterPid;
*dataUsed = sizeof(pid_t);
break;
default:
DLog(@"Device unhandled getPropertyDataWithAddress for %@",
[ObjectStore
StringFromPropertySelector:address.mSelector]);
*dataUsed = 0;
break;
};
}
- (BOOL)hasPropertyWithAddress:(CMIOObjectPropertyAddress)address
{
switch (address.mSelector) {
case kCMIOObjectPropertyName:
case kCMIOObjectPropertyManufacturer:
case kCMIOObjectPropertyElementCategoryName:
case kCMIOObjectPropertyElementNumberName:
case kCMIODevicePropertyPlugIn:
case kCMIODevicePropertyDeviceUID:
case kCMIODevicePropertyModelUID:
case kCMIODevicePropertyTransportType:
case kCMIODevicePropertyDeviceIsAlive:
case kCMIODevicePropertyDeviceHasChanged:
case kCMIODevicePropertyDeviceIsRunning:
case kCMIODevicePropertyDeviceIsRunningSomewhere:
case kCMIODevicePropertyDeviceCanBeDefaultDevice:
case kCMIODevicePropertyHogMode:
case kCMIODevicePropertyLatency:
case kCMIODevicePropertyStreams:
case kCMIODevicePropertyExcludeNonDALAccess:
case kCMIODevicePropertyCanProcessAVCCommand:
case kCMIODevicePropertyCanProcessRS422Command:
case kCMIODevicePropertyDeviceMaster:
return true;
case kCMIODevicePropertyStreamConfiguration:
case kCMIODevicePropertyLinkedCoreAudioDeviceUID:
return false;
default:
DLog(@"Device unhandled hasPropertyWithAddress for %@",
[ObjectStore
StringFromPropertySelector:address.mSelector]);
return false;
};
}
- (BOOL)isPropertySettableWithAddress:(CMIOObjectPropertyAddress)address
{
switch (address.mSelector) {
case kCMIOObjectPropertyName:
case kCMIOObjectPropertyManufacturer:
case kCMIOObjectPropertyElementCategoryName:
case kCMIOObjectPropertyElementNumberName:
case kCMIODevicePropertyPlugIn:
case kCMIODevicePropertyDeviceUID:
case kCMIODevicePropertyModelUID:
case kCMIODevicePropertyTransportType:
case kCMIODevicePropertyDeviceIsAlive:
case kCMIODevicePropertyDeviceHasChanged:
case kCMIODevicePropertyDeviceIsRunning:
case kCMIODevicePropertyDeviceIsRunningSomewhere:
case kCMIODevicePropertyDeviceCanBeDefaultDevice:
case kCMIODevicePropertyHogMode:
case kCMIODevicePropertyLatency:
case kCMIODevicePropertyStreams:
case kCMIODevicePropertyStreamConfiguration:
case kCMIODevicePropertyCanProcessAVCCommand:
case kCMIODevicePropertyCanProcessRS422Command:
case kCMIODevicePropertyLinkedCoreAudioDeviceUID:
return false;
case kCMIODevicePropertyExcludeNonDALAccess:
case kCMIODevicePropertyDeviceMaster:
return true;
default:
DLog(@"Device unhandled isPropertySettableWithAddress for %@",
[ObjectStore
StringFromPropertySelector:address.mSelector]);
return false;
};
}
- (void)setPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
qualifierDataSize:(UInt32)qualifierDataSize
qualifierData:(nonnull const void *)qualifierData
dataSize:(UInt32)dataSize
data:(nonnull const void *)data
{
switch (address.mSelector) {
case kCMIODevicePropertyExcludeNonDALAccess:
self.excludeNonDALAccess =
(*static_cast<const UInt32 *>(data) != 0);
break;
case kCMIODevicePropertyDeviceMaster:
self.masterPid = *static_cast<const pid_t *>(data);
break;
default:
DLog(@"Device unhandled setPropertyDataWithAddress for %@",
[ObjectStore
StringFromPropertySelector:address.mSelector]);
break;
};
}
@end

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>obs-mac-virtualcam</string>
<key>CFBundleIdentifier</key>
<string>com.obsproject.obs-mac-virtualcam.dal-plugin</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>OBS Virtual Camera</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFPlugInFactories</key>
<dict>
<key>35FDFF29-BFCF-4644-AB77-B759DE932ABE</key>
<string>PlugInMain</string>
</dict>
<key>CFPlugInTypes</key>
<dict>
<key>30010C1C-93BF-11D8-8B5B-000A95AF9C6A</key>
<array>
<string>35FDFF29-BFCF-4644-AB77-B759DE932ABE</string>
</array>
</dict>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
<key>CMIOHardwareAssistantServiceNames</key>
<array>
<string>com.obsproject.obs-mac-virtualcam.server</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,32 @@
//
// Logging.h
// obs-mac-virtualcam
//
// Created by John Boiles on 4/10/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#ifndef Logging_h
#define Logging_h
#include "Defines.h"
#define DLog(fmt, ...) NSLog((PLUGIN_NAME @"(DAL): " fmt), ##__VA_ARGS__)
#define DLogFunc(fmt, ...) \
NSLog((PLUGIN_NAME @"(DAL): %s " fmt), __FUNCTION__, ##__VA_ARGS__)
#define VLog(fmt, ...)
#define VLogFunc(fmt, ...)
#define ELog(fmt, ...) DLog(fmt, ##__VA_ARGS__)
#endif /* Logging_h */

View File

@ -0,0 +1,33 @@
//
// MachClient.h
// dal-plugin
//
// Created by John Boiles on 5/5/20.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol MachClientDelegate
- (void)receivedFrameWithSize:(NSSize)size
timestamp:(uint64_t)timestamp
fpsNumerator:(uint32_t)fpsNumerator
fpsDenominator:(uint32_t)fpsDenominator
frameData:(NSData *)frameData;
- (void)receivedStop;
@end
@interface MachClient : NSObject
@property (nullable, weak) id<MachClientDelegate> delegate;
- (BOOL)isServerAvailable;
- (BOOL)connectToServer;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,140 @@
//
// MachClient.m
// dal-plugin
//
// Created by John Boiles on 5/5/20.
//
#import "MachClient.h"
#import "MachProtocol.h"
#import "Logging.h"
@interface MachClient () <NSPortDelegate> {
NSPort *_receivePort;
}
@end
@implementation MachClient
- (void)dealloc
{
DLogFunc(@"");
_receivePort.delegate = nil;
}
- (NSPort *)serverPort
{
// See note in MachServer.mm and don't judge me
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [[NSMachBootstrapServer sharedInstance]
portForName:@MACH_SERVICE_NAME];
#pragma clang diagnostic pop
}
- (BOOL)isServerAvailable
{
return [self serverPort] != nil;
}
- (NSPort *)receivePort
{
if (_receivePort == nil) {
NSPort *receivePort = [NSMachPort port];
_receivePort = receivePort;
_receivePort.delegate = self;
__weak __typeof(self) weakSelf = self;
dispatch_async(
dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:receivePort
forMode:NSDefaultRunLoopMode];
// weakSelf should become nil when this object gets destroyed
while (weakSelf) {
[[NSRunLoop currentRunLoop]
runUntilDate:
[NSDate dateWithTimeIntervalSinceNow:
0.1]];
}
DLog(@"Shutting down receive run loop");
});
DLog(@"Initialized mach port %d for receiving",
((NSMachPort *)_receivePort).machPort);
}
return _receivePort;
}
- (BOOL)connectToServer
{
DLogFunc(@"");
NSPort *sendPort = [self serverPort];
if (sendPort == nil) {
ELog(@"Unable to connect to server port");
return NO;
}
NSPortMessage *message = [[NSPortMessage alloc]
initWithSendPort:sendPort
receivePort:self.receivePort
components:nil];
message.msgid = MachMsgIdConnect;
NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:5.0];
if (![message sendBeforeDate:timeout]) {
ELog(@"sendBeforeDate failed");
return NO;
}
return YES;
}
- (void)handlePortMessage:(NSPortMessage *)message
{
VLogFunc(@"");
NSArray *components = message.components;
switch (message.msgid) {
case MachMsgIdConnect:
DLog(@"Received connect response");
break;
case MachMsgIdFrame:
VLog(@"Received frame message");
if (components.count >= 6) {
CGFloat width;
[components[0] getBytes:&width length:sizeof(width)];
CGFloat height;
[components[1] getBytes:&height length:sizeof(height)];
uint64_t timestamp;
[components[2] getBytes:&timestamp
length:sizeof(timestamp)];
VLog(@"Received frame data: %fx%f (%llu)", width,
height, timestamp);
NSData *frameData = components[3];
uint32_t fpsNumerator;
[components[4] getBytes:&fpsNumerator
length:sizeof(fpsNumerator)];
uint32_t fpsDenominator;
[components[5] getBytes:&fpsDenominator
length:sizeof(fpsDenominator)];
[self.delegate
receivedFrameWithSize:NSMakeSize(width, height)
timestamp:timestamp
fpsNumerator:fpsNumerator
fpsDenominator:fpsDenominator
frameData:frameData];
}
break;
case MachMsgIdStop:
DLog(@"Received stop message");
[self.delegate receivedStop];
break;
default:
ELog(@"Received unexpected response msgid %u",
(unsigned)message.msgid);
break;
}
}
@end

View File

@ -0,0 +1,62 @@
//
// ObjectStore.h
// obs-mac-virtualcam
//
// Created by John Boiles on 4/10/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#import <Foundation/Foundation.h>
#import <CoreMediaIO/CMIOHardwarePlugIn.h>
NS_ASSUME_NONNULL_BEGIN
@protocol CMIOObject
- (BOOL)hasPropertyWithAddress:(CMIOObjectPropertyAddress)address;
- (BOOL)isPropertySettableWithAddress:(CMIOObjectPropertyAddress)address;
- (UInt32)getPropertyDataSizeWithAddress:(CMIOObjectPropertyAddress)address
qualifierDataSize:(UInt32)qualifierDataSize
qualifierData:(const void *)qualifierData;
- (void)getPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
qualifierDataSize:(UInt32)qualifierDataSize
qualifierData:(const void *)qualifierData
dataSize:(UInt32)dataSize
dataUsed:(UInt32 *)dataUsed
data:(void *)data;
- (void)setPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
qualifierDataSize:(UInt32)qualifierDataSize
qualifierData:(const void *)qualifierData
dataSize:(UInt32)dataSize
data:(const void *)data;
@end
@interface ObjectStore : NSObject
+ (ObjectStore *)SharedObjectStore;
+ (NSObject<CMIOObject> *)GetObjectWithId:(CMIOObjectID)objectId;
+ (NSString *)StringFromPropertySelector:(CMIOObjectPropertySelector)selector;
+ (BOOL)IsBridgedTypeForSelector:(CMIOObjectPropertySelector)selector;
- (NSObject<CMIOObject> *)getObject:(CMIOObjectID)objectID;
- (void)setObject:(id<CMIOObject>)object forObjectId:(CMIOObjectID)objectId;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,281 @@
//
// ObjectStore.mm
// obs-mac-virtualcam
//
// Created by John Boiles on 4/10/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#import "ObjectStore.h"
@interface ObjectStore ()
@property NSMutableDictionary *objectMap;
@end
@implementation ObjectStore
// 4-byte selectors to string for easy debugging
+ (NSString *)StringFromPropertySelector:(CMIOObjectPropertySelector)selector
{
switch (selector) {
case kCMIODevicePropertyPlugIn:
return @"kCMIODevicePropertyPlugIn";
case kCMIODevicePropertyDeviceUID:
return @"kCMIODevicePropertyDeviceUID";
case kCMIODevicePropertyModelUID:
return @"kCMIODevicePropertyModelUID";
case kCMIODevicePropertyTransportType:
return @"kCMIODevicePropertyTransportType";
case kCMIODevicePropertyDeviceIsAlive:
return @"kCMIODevicePropertyDeviceIsAlive";
case kCMIODevicePropertyDeviceHasChanged:
return @"kCMIODevicePropertyDeviceHasChanged";
case kCMIODevicePropertyDeviceIsRunning:
return @"kCMIODevicePropertyDeviceIsRunning";
case kCMIODevicePropertyDeviceIsRunningSomewhere:
return @"kCMIODevicePropertyDeviceIsRunningSomewhere";
case kCMIODevicePropertyDeviceCanBeDefaultDevice:
return @"kCMIODevicePropertyDeviceCanBeDefaultDevice";
case kCMIODevicePropertyHogMode:
return @"kCMIODevicePropertyHogMode";
case kCMIODevicePropertyLatency:
return @"kCMIODevicePropertyLatency";
case kCMIODevicePropertyStreams:
return @"kCMIODevicePropertyStreams";
case kCMIODevicePropertyStreamConfiguration:
return @"kCMIODevicePropertyStreamConfiguration";
case kCMIODevicePropertyDeviceMaster:
return @"kCMIODevicePropertyDeviceMaster";
case kCMIODevicePropertyExcludeNonDALAccess:
return @"kCMIODevicePropertyExcludeNonDALAccess";
case kCMIODevicePropertyClientSyncDiscontinuity:
return @"kCMIODevicePropertyClientSyncDiscontinuity";
case kCMIODevicePropertySMPTETimeCallback:
return @"kCMIODevicePropertySMPTETimeCallback";
case kCMIODevicePropertyCanProcessAVCCommand:
return @"kCMIODevicePropertyCanProcessAVCCommand";
case kCMIODevicePropertyAVCDeviceType:
return @"kCMIODevicePropertyAVCDeviceType";
case kCMIODevicePropertyAVCDeviceSignalMode:
return @"kCMIODevicePropertyAVCDeviceSignalMode";
case kCMIODevicePropertyCanProcessRS422Command:
return @"kCMIODevicePropertyCanProcessRS422Command";
case kCMIODevicePropertyLinkedCoreAudioDeviceUID:
return @"kCMIODevicePropertyLinkedCoreAudioDeviceUID";
case kCMIODevicePropertyVideoDigitizerComponents:
return @"kCMIODevicePropertyVideoDigitizerComponents";
case kCMIODevicePropertySuspendedByUser:
return @"kCMIODevicePropertySuspendedByUser";
case kCMIODevicePropertyLinkedAndSyncedCoreAudioDeviceUID:
return @"kCMIODevicePropertyLinkedAndSyncedCoreAudioDeviceUID";
case kCMIODevicePropertyIIDCInitialUnitSpace:
return @"kCMIODevicePropertyIIDCInitialUnitSpace";
case kCMIODevicePropertyIIDCCSRData:
return @"kCMIODevicePropertyIIDCCSRData";
case kCMIODevicePropertyCanSwitchFrameRatesWithoutFrameDrops:
return @"kCMIODevicePropertyCanSwitchFrameRatesWithoutFrameDrops";
case kCMIODevicePropertyLocation:
return @"kCMIODevicePropertyLocation";
case kCMIODevicePropertyDeviceHasStreamingError:
return @"kCMIODevicePropertyDeviceHasStreamingError";
case kCMIODevicePropertyScopeInput:
return @"kCMIODevicePropertyScopeInput";
case kCMIODevicePropertyScopeOutput:
return @"kCMIODevicePropertyScopeOutput";
case kCMIODevicePropertyScopePlayThrough:
return @"kCMIODevicePropertyScopePlayThrough";
case kCMIOObjectPropertyClass:
return @"kCMIOObjectPropertyClass";
case kCMIOObjectPropertyOwner:
return @"kCMIOObjectPropertyOwner";
case kCMIOObjectPropertyCreator:
return @"kCMIOObjectPropertyCreator";
case kCMIOObjectPropertyName:
return @"kCMIOObjectPropertyName";
case kCMIOObjectPropertyManufacturer:
return @"kCMIOObjectPropertyManufacturer";
case kCMIOObjectPropertyElementName:
return @"kCMIOObjectPropertyElementName";
case kCMIOObjectPropertyElementCategoryName:
return @"kCMIOObjectPropertyElementCategoryName";
case kCMIOObjectPropertyElementNumberName:
return @"kCMIOObjectPropertyElementNumberName";
case kCMIOObjectPropertyOwnedObjects:
return @"kCMIOObjectPropertyOwnedObjects";
case kCMIOObjectPropertyListenerAdded:
return @"kCMIOObjectPropertyListenerAdded";
case kCMIOObjectPropertyListenerRemoved:
return @"kCMIOObjectPropertyListenerRemoved";
case kCMIOStreamPropertyDirection:
return @"kCMIOStreamPropertyDirection";
case kCMIOStreamPropertyTerminalType:
return @"kCMIOStreamPropertyTerminalType";
case kCMIOStreamPropertyStartingChannel:
return @"kCMIOStreamPropertyStartingChannel";
// Same value as kCMIODevicePropertyLatency
// case kCMIOStreamPropertyLatency:
// return @"kCMIOStreamPropertyLatency";
case kCMIOStreamPropertyFormatDescription:
return @"kCMIOStreamPropertyFormatDescription";
case kCMIOStreamPropertyFormatDescriptions:
return @"kCMIOStreamPropertyFormatDescriptions";
case kCMIOStreamPropertyStillImage:
return @"kCMIOStreamPropertyStillImage";
case kCMIOStreamPropertyStillImageFormatDescriptions:
return @"kCMIOStreamPropertyStillImageFormatDescriptions";
case kCMIOStreamPropertyFrameRate:
return @"kCMIOStreamPropertyFrameRate";
case kCMIOStreamPropertyMinimumFrameRate:
return @"kCMIOStreamPropertyMinimumFrameRate";
case kCMIOStreamPropertyFrameRates:
return @"kCMIOStreamPropertyFrameRates";
case kCMIOStreamPropertyFrameRateRanges:
return @"kCMIOStreamPropertyFrameRateRanges";
case kCMIOStreamPropertyNoDataTimeoutInMSec:
return @"kCMIOStreamPropertyNoDataTimeoutInMSec";
case kCMIOStreamPropertyDeviceSyncTimeoutInMSec:
return @"kCMIOStreamPropertyDeviceSyncTimeoutInMSec";
case kCMIOStreamPropertyNoDataEventCount:
return @"kCMIOStreamPropertyNoDataEventCount";
case kCMIOStreamPropertyOutputBufferUnderrunCount:
return @"kCMIOStreamPropertyOutputBufferUnderrunCount";
case kCMIOStreamPropertyOutputBufferRepeatCount:
return @"kCMIOStreamPropertyOutputBufferRepeatCount";
case kCMIOStreamPropertyOutputBufferQueueSize:
return @"kCMIOStreamPropertyOutputBufferQueueSize";
case kCMIOStreamPropertyOutputBuffersRequiredForStartup:
return @"kCMIOStreamPropertyOutputBuffersRequiredForStartup";
case kCMIOStreamPropertyOutputBuffersNeededForThrottledPlayback:
return @"kCMIOStreamPropertyOutputBuffersNeededForThrottledPlayback";
case kCMIOStreamPropertyFirstOutputPresentationTimeStamp:
return @"kCMIOStreamPropertyFirstOutputPresentationTimeStamp";
case kCMIOStreamPropertyEndOfData:
return @"kCMIOStreamPropertyEndOfData";
case kCMIOStreamPropertyClock:
return @"kCMIOStreamPropertyClock";
case kCMIOStreamPropertyCanProcessDeckCommand:
return @"kCMIOStreamPropertyCanProcessDeckCommand";
case kCMIOStreamPropertyDeck:
return @"kCMIOStreamPropertyDeck";
case kCMIOStreamPropertyDeckFrameNumber:
return @"kCMIOStreamPropertyDeckFrameNumber";
case kCMIOStreamPropertyDeckDropness:
return @"kCMIOStreamPropertyDeckDropness";
case kCMIOStreamPropertyDeckThreaded:
return @"kCMIOStreamPropertyDeckThreaded";
case kCMIOStreamPropertyDeckLocal:
return @"kCMIOStreamPropertyDeckLocal";
case kCMIOStreamPropertyDeckCueing:
return @"kCMIOStreamPropertyDeckCueing";
case kCMIOStreamPropertyInitialPresentationTimeStampForLinkedAndSyncedAudio:
return @"kCMIOStreamPropertyInitialPresentationTimeStampForLinkedAndSyncedAudio";
case kCMIOStreamPropertyScheduledOutputNotificationProc:
return @"kCMIOStreamPropertyScheduledOutputNotificationProc";
case kCMIOStreamPropertyPreferredFormatDescription:
return @"kCMIOStreamPropertyPreferredFormatDescription";
case kCMIOStreamPropertyPreferredFrameRate:
return @"kCMIOStreamPropertyPreferredFrameRate";
case kCMIOControlPropertyScope:
return @"kCMIOControlPropertyScope";
case kCMIOControlPropertyElement:
return @"kCMIOControlPropertyElement";
case kCMIOControlPropertyVariant:
return @"kCMIOControlPropertyVariant";
case kCMIOHardwarePropertyProcessIsMaster:
return @"kCMIOHardwarePropertyProcessIsMaster";
case kCMIOHardwarePropertyIsInitingOrExiting:
return @"kCMIOHardwarePropertyIsInitingOrExiting";
case kCMIOHardwarePropertyDevices:
return @"kCMIOHardwarePropertyDevices";
case kCMIOHardwarePropertyDefaultInputDevice:
return @"kCMIOHardwarePropertyDefaultInputDevice";
case kCMIOHardwarePropertyDefaultOutputDevice:
return @"kCMIOHardwarePropertyDefaultOutputDevice";
case kCMIOHardwarePropertyDeviceForUID:
return @"kCMIOHardwarePropertyDeviceForUID";
case kCMIOHardwarePropertySleepingIsAllowed:
return @"kCMIOHardwarePropertySleepingIsAllowed";
case kCMIOHardwarePropertyUnloadingIsAllowed:
return @"kCMIOHardwarePropertyUnloadingIsAllowed";
case kCMIOHardwarePropertyPlugInForBundleID:
return @"kCMIOHardwarePropertyPlugInForBundleID";
case kCMIOHardwarePropertyUserSessionIsActiveOrHeadless:
return @"kCMIOHardwarePropertyUserSessionIsActiveOrHeadless";
case kCMIOHardwarePropertySuspendedBySystem:
return @"kCMIOHardwarePropertySuspendedBySystem";
case kCMIOHardwarePropertyAllowScreenCaptureDevices:
return @"kCMIOHardwarePropertyAllowScreenCaptureDevices";
case kCMIOHardwarePropertyAllowWirelessScreenCaptureDevices:
return @"kCMIOHardwarePropertyAllowWirelessScreenCaptureDevices";
default:
uint8_t *chars = (uint8_t *)&selector;
return [NSString stringWithFormat:@"Unknown selector: %c%c%c%c",
chars[0], chars[1], chars[2],
chars[3]];
}
}
+ (BOOL)IsBridgedTypeForSelector:(CMIOObjectPropertySelector)selector
{
switch (selector) {
case kCMIOObjectPropertyName:
case kCMIOObjectPropertyManufacturer:
case kCMIOObjectPropertyElementName:
case kCMIOObjectPropertyElementCategoryName:
case kCMIOObjectPropertyElementNumberName:
case kCMIODevicePropertyDeviceUID:
case kCMIODevicePropertyModelUID:
case kCMIOStreamPropertyFormatDescriptions:
case kCMIOStreamPropertyFormatDescription:
case kCMIOStreamPropertyClock:
return YES;
default:
return NO;
}
}
+ (ObjectStore *)SharedObjectStore
{
static ObjectStore *sObjectStore = nil;
static dispatch_once_t sOnceToken;
dispatch_once(&sOnceToken, ^{
sObjectStore = [[self alloc] init];
});
return sObjectStore;
}
+ (NSObject<CMIOObject> *)GetObjectWithId:(CMIOObjectID)objectId
{
return [[ObjectStore SharedObjectStore] getObject:objectId];
}
- (id)init
{
if (self = [super init]) {
self.objectMap = [[NSMutableDictionary alloc] init];
}
return self;
}
- (NSObject<CMIOObject> *)getObject:(CMIOObjectID)objectID
{
return self.objectMap[@(objectID)];
}
- (void)setObject:(id<CMIOObject>)object forObjectId:(CMIOObjectID)objectId
{
self.objectMap[@(objectId)] = object;
}
@end

View File

@ -0,0 +1,51 @@
//
// PlugIn.h
// obs-mac-virtualcam
//
// Created by John Boiles on 4/9/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#import <Foundation/Foundation.h>
#import <CoreMediaIO/CMIOHardwarePlugIn.h>
#import "ObjectStore.h"
#import "MachClient.h"
#import "Stream.h"
#define kTestCardWidthKey @"obs-mac-virtualcam-test-card-width"
#define kTestCardHeightKey @"obs-mac-virtualcam-test-card-height"
#define kTestCardFPSKey @"obs-mac-virtualcam-test-card-fps"
NS_ASSUME_NONNULL_BEGIN
@interface PlugIn : NSObject <CMIOObject>
@property CMIOObjectID objectId;
@property (readonly) MachClient *machClient;
@property Stream *stream;
+ (PlugIn *)SharedPlugIn;
- (void)initialize;
- (void)teardown;
- (void)startStream;
- (void)stopStream;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,255 @@
//
// PlugIn.mm
// obs-mac-virtualcam
//
// Created by John Boiles on 4/9/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#import "PlugIn.h"
#import <CoreMediaIO/CMIOHardwarePlugin.h>
#import "Logging.h"
typedef enum {
PlugInStateNotStarted = 0,
PlugInStateWaitingForServer,
PlugInStateReceivingFrames,
} PlugInState;
@interface PlugIn () <MachClientDelegate> {
//! Serial queue for all state changes that need to be concerned with thread safety
dispatch_queue_t _stateQueue;
//! Repeated timer for driving the mach server re-connection
dispatch_source_t _machConnectTimer;
//! Timeout timer when we haven't received frames for 5s
dispatch_source_t _timeoutTimer;
}
@property PlugInState state;
@property MachClient *machClient;
@end
@implementation PlugIn
+ (PlugIn *)SharedPlugIn
{
static PlugIn *sPlugIn = nil;
static dispatch_once_t sOnceToken;
dispatch_once(&sOnceToken, ^{
sPlugIn = [[self alloc] init];
});
return sPlugIn;
}
- (instancetype)init
{
if (self = [super init]) {
_stateQueue = dispatch_queue_create(
"com.obsproject.obs-mac-virtualcam.dal.state",
DISPATCH_QUEUE_SERIAL);
_timeoutTimer = dispatch_source_create(
DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _stateQueue);
__weak __typeof(self) weakSelf = self;
dispatch_source_set_event_handler(_timeoutTimer, ^{
if (weakSelf.state == PlugInStateReceivingFrames) {
DLog(@"No frames received for 5s, restarting connection");
[self stopStream];
[self startStream];
}
});
_machClient = [[MachClient alloc] init];
_machClient.delegate = self;
_machConnectTimer = dispatch_source_create(
DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _stateQueue);
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 0);
uint64_t intervalTime = (int64_t)(1 * NSEC_PER_SEC);
dispatch_source_set_timer(_machConnectTimer, startTime,
intervalTime, 0);
dispatch_source_set_event_handler(_machConnectTimer, ^{
if (![[weakSelf machClient] isServerAvailable]) {
DLog(@"Server is not available");
} else if (weakSelf.state ==
PlugInStateWaitingForServer) {
DLog(@"Attempting connection");
[[weakSelf machClient] connectToServer];
}
});
}
return self;
}
- (void)startStream
{
DLogFunc(@"");
dispatch_async(_stateQueue, ^{
if (_state == PlugInStateNotStarted) {
dispatch_resume(_machConnectTimer);
[self.stream startServingDefaultFrames];
_state = PlugInStateWaitingForServer;
}
});
}
- (void)stopStream
{
DLogFunc(@"");
dispatch_async(_stateQueue, ^{
if (_state == PlugInStateWaitingForServer) {
dispatch_suspend(_machConnectTimer);
[self.stream stopServingDefaultFrames];
} else if (_state == PlugInStateReceivingFrames) {
// TODO: Disconnect from the mach server?
dispatch_suspend(_timeoutTimer);
}
_state = PlugInStateNotStarted;
});
}
- (void)initialize
{
}
- (void)teardown
{
}
#pragma mark - CMIOObject
- (BOOL)hasPropertyWithAddress:(CMIOObjectPropertyAddress)address
{
switch (address.mSelector) {
case kCMIOObjectPropertyName:
return true;
default:
DLog(@"PlugIn unhandled hasPropertyWithAddress for %@",
[ObjectStore
StringFromPropertySelector:address.mSelector]);
return false;
};
}
- (BOOL)isPropertySettableWithAddress:(CMIOObjectPropertyAddress)address
{
switch (address.mSelector) {
case kCMIOObjectPropertyName:
return false;
default:
DLog(@"PlugIn unhandled isPropertySettableWithAddress for %@",
[ObjectStore
StringFromPropertySelector:address.mSelector]);
return false;
};
}
- (UInt32)getPropertyDataSizeWithAddress:(CMIOObjectPropertyAddress)address
qualifierDataSize:(UInt32)qualifierDataSize
qualifierData:(const void *)qualifierData
{
switch (address.mSelector) {
case kCMIOObjectPropertyName:
return sizeof(CFStringRef);
default:
DLog(@"PlugIn unhandled getPropertyDataSizeWithAddress for %@",
[ObjectStore
StringFromPropertySelector:address.mSelector]);
return 0;
};
}
- (void)getPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
qualifierDataSize:(UInt32)qualifierDataSize
qualifierData:(nonnull const void *)qualifierData
dataSize:(UInt32)dataSize
dataUsed:(nonnull UInt32 *)dataUsed
data:(nonnull void *)data
{
switch (address.mSelector) {
case kCMIOObjectPropertyName:
*static_cast<CFStringRef *>(data) =
CFSTR("OBS Virtual Camera Plugin");
*dataUsed = sizeof(CFStringRef);
return;
default:
DLog(@"PlugIn unhandled getPropertyDataWithAddress for %@",
[ObjectStore
StringFromPropertySelector:address.mSelector]);
return;
};
}
- (void)setPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
qualifierDataSize:(UInt32)qualifierDataSize
qualifierData:(nonnull const void *)qualifierData
dataSize:(UInt32)dataSize
data:(nonnull const void *)data
{
DLog(@"PlugIn unhandled setPropertyDataWithAddress for %@",
[ObjectStore StringFromPropertySelector:address.mSelector]);
}
#pragma mark - MachClientDelegate
- (void)receivedFrameWithSize:(NSSize)size
timestamp:(uint64_t)timestamp
fpsNumerator:(uint32_t)fpsNumerator
fpsDenominator:(uint32_t)fpsDenominator
frameData:(NSData *)frameData
{
dispatch_sync(_stateQueue, ^{
if (_state == PlugInStateWaitingForServer) {
NSUserDefaults *defaults =
[NSUserDefaults standardUserDefaults];
[defaults setInteger:size.width
forKey:kTestCardWidthKey];
[defaults setInteger:size.height
forKey:kTestCardHeightKey];
[defaults setDouble:(double)fpsNumerator /
(double)fpsDenominator
forKey:kTestCardFPSKey];
dispatch_suspend(_machConnectTimer);
[self.stream stopServingDefaultFrames];
dispatch_resume(_timeoutTimer);
_state = PlugInStateReceivingFrames;
}
});
// Add 5 more seconds onto the timeout timer
dispatch_source_set_timer(
_timeoutTimer,
dispatch_time(DISPATCH_TIME_NOW, 5.0 * NSEC_PER_SEC),
5.0 * NSEC_PER_SEC, (1ull * NSEC_PER_SEC) / 10);
[self.stream queueFrameWithSize:size
timestamp:timestamp
fpsNumerator:fpsNumerator
fpsDenominator:fpsDenominator
frameData:frameData];
}
- (void)receivedStop
{
DLogFunc(@"Restarting connection");
[self stopStream];
[self startStream];
}
@end

View File

@ -0,0 +1,23 @@
//
// PlugInInterface.h
// obs-mac-virtualcam
//
// Created by John Boiles on 4/9/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#import <CoreMediaIO/CMIOHardwarePlugIn.h>
// The static singleton of the plugin interface
CMIOHardwarePlugInRef PlugInRef();

View File

@ -0,0 +1,444 @@
//
// PlugInInterface.mm
// obs-mac-virtualcam
//
// This file implements the CMIO DAL plugin interface
//
// Created by John Boiles on 4/9/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#import "PlugInInterface.h"
#import <CoreFoundation/CFUUID.h>
#import "PlugIn.h"
#import "Device.h"
#import "Stream.h"
#import "Logging.h"
#pragma mark Plug-In Operations
static UInt32 sRefCount = 0;
ULONG HardwarePlugIn_AddRef(CMIOHardwarePlugInRef self)
{
sRefCount += 1;
DLogFunc(@"sRefCount now = %d", sRefCount);
return sRefCount;
}
ULONG HardwarePlugIn_Release(CMIOHardwarePlugInRef self)
{
sRefCount -= 1;
DLogFunc(@"sRefCount now = %d", sRefCount);
return sRefCount;
}
HRESULT HardwarePlugIn_QueryInterface(CMIOHardwarePlugInRef self, REFIID uuid,
LPVOID *interface)
{
DLogFunc(@"");
if (!interface) {
DLogFunc(@"Received an empty interface");
return E_POINTER;
}
// Set the returned interface to NULL in case the UUIDs don't match
*interface = NULL;
// Create a CoreFoundation UUIDRef for the requested interface.
CFUUIDRef cfUuid = CFUUIDCreateFromUUIDBytes(kCFAllocatorDefault, uuid);
CFStringRef uuidString = CFUUIDCreateString(NULL, cfUuid);
CFStringRef hardwarePluginUuid =
CFUUIDCreateString(NULL, kCMIOHardwarePlugInInterfaceID);
if (CFEqual(uuidString, hardwarePluginUuid)) {
// Return the interface;
sRefCount += 1;
*interface = PlugInRef();
return kCMIOHardwareNoError;
} else {
DLogFunc(@"ERR Queried for some weird UUID %@", uuidString);
}
return E_NOINTERFACE;
}
// I think this is deprecated, seems that HardwarePlugIn_InitializeWithObjectID gets called instead
OSStatus HardwarePlugIn_Initialize(CMIOHardwarePlugInRef self)
{
DLogFunc(@"ERR self=%p", self);
return kCMIOHardwareUnspecifiedError;
}
OSStatus HardwarePlugIn_InitializeWithObjectID(CMIOHardwarePlugInRef self,
CMIOObjectID objectID)
{
DLogFunc(@"self=%p", self);
OSStatus error = kCMIOHardwareNoError;
PlugIn *plugIn = [PlugIn SharedPlugIn];
plugIn.objectId = objectID;
[[ObjectStore SharedObjectStore] setObject:plugIn forObjectId:objectID];
Device *device = [[Device alloc] init];
CMIOObjectID deviceId;
error = CMIOObjectCreate(PlugInRef(), kCMIOObjectSystemObject,
kCMIODeviceClassID, &deviceId);
if (error != noErr) {
DLog(@"CMIOObjectCreate Error %d", error);
return error;
}
device.objectId = deviceId;
device.pluginId = objectID;
[[ObjectStore SharedObjectStore] setObject:device forObjectId:deviceId];
Stream *stream = [[Stream alloc] init];
CMIOObjectID streamId;
error = CMIOObjectCreate(PlugInRef(), deviceId, kCMIOStreamClassID,
&streamId);
if (error != noErr) {
DLog(@"CMIOObjectCreate Error %d", error);
return error;
}
stream.objectId = streamId;
[[ObjectStore SharedObjectStore] setObject:stream forObjectId:streamId];
device.streamId = streamId;
plugIn.stream = stream;
// Tell the system about the Device
error = CMIOObjectsPublishedAndDied(
PlugInRef(), kCMIOObjectSystemObject, 1, &deviceId, 0, 0);
if (error != kCMIOHardwareNoError) {
DLog(@"CMIOObjectsPublishedAndDied plugin/device Error %d",
error);
return error;
}
// Tell the system about the Stream
error = CMIOObjectsPublishedAndDied(PlugInRef(), deviceId, 1, &streamId,
0, 0);
if (error != kCMIOHardwareNoError) {
DLog(@"CMIOObjectsPublishedAndDied device/stream Error %d",
error);
return error;
}
return error;
}
OSStatus HardwarePlugIn_Teardown(CMIOHardwarePlugInRef self)
{
DLogFunc(@"self=%p", self);
OSStatus error = kCMIOHardwareNoError;
PlugIn *plugIn = [PlugIn SharedPlugIn];
[plugIn teardown];
return error;
}
#pragma mark CMIOObject Operations
void HardwarePlugIn_ObjectShow(CMIOHardwarePlugInRef self,
CMIOObjectID objectID)
{
DLogFunc(@"self=%p", self);
}
Boolean
HardwarePlugIn_ObjectHasProperty(CMIOHardwarePlugInRef self,
CMIOObjectID objectID,
const CMIOObjectPropertyAddress *address)
{
NSObject<CMIOObject> *object = [ObjectStore GetObjectWithId:objectID];
if (object == nil) {
DLogFunc(@"ERR nil object");
return false;
}
Boolean answer = [object hasPropertyWithAddress:*address];
// Disabling Noisy logs
// DLogFunc(@"%@(%d) %@ self=%p hasProperty=%d", NSStringFromClass([object class]), objectID, [ObjectStore StringFromPropertySelector:address->mSelector], self, answer);
return answer;
}
OSStatus HardwarePlugIn_ObjectIsPropertySettable(
CMIOHardwarePlugInRef self, CMIOObjectID objectID,
const CMIOObjectPropertyAddress *address, Boolean *isSettable)
{
NSObject<CMIOObject> *object = [ObjectStore GetObjectWithId:objectID];
if (object == nil) {
DLogFunc(@"ERR nil object");
return kCMIOHardwareBadObjectError;
}
*isSettable = [object isPropertySettableWithAddress:*address];
DLogFunc(@"%@(%d) %@ self=%p settable=%d",
NSStringFromClass([object class]), objectID,
[ObjectStore StringFromPropertySelector:address->mSelector],
self, *isSettable);
return kCMIOHardwareNoError;
}
OSStatus HardwarePlugIn_ObjectGetPropertyDataSize(
CMIOHardwarePlugInRef self, CMIOObjectID objectID,
const CMIOObjectPropertyAddress *address, UInt32 qualifierDataSize,
const void *qualifierData, UInt32 *dataSize)
{
NSObject<CMIOObject> *object = [ObjectStore GetObjectWithId:objectID];
if (object == nil) {
DLogFunc(@"ERR nil object");
return kCMIOHardwareBadObjectError;
}
*dataSize = [object getPropertyDataSizeWithAddress:*address
qualifierDataSize:qualifierDataSize
qualifierData:qualifierData];
// Disabling Noisy logs
// DLogFunc(@"%@(%d) %@ self=%p size=%d", NSStringFromClass([object class]), objectID, [ObjectStore StringFromPropertySelector:address->mSelector], self, *dataSize);
return kCMIOHardwareNoError;
}
OSStatus HardwarePlugIn_ObjectGetPropertyData(
CMIOHardwarePlugInRef self, CMIOObjectID objectID,
const CMIOObjectPropertyAddress *address, UInt32 qualifierDataSize,
const void *qualifierData, UInt32 dataSize, UInt32 *dataUsed,
void *data)
{
NSObject<CMIOObject> *object = [ObjectStore GetObjectWithId:objectID];
if (object == nil) {
DLogFunc(@"ERR nil object");
return kCMIOHardwareBadObjectError;
}
[object getPropertyDataWithAddress:*address
qualifierDataSize:qualifierDataSize
qualifierData:qualifierData
dataSize:dataSize
dataUsed:dataUsed
data:data];
// Disabling Noisy logs
// if ([ObjectStore IsBridgedTypeForSelector:address->mSelector]) {
// id dataObj = (__bridge NSObject *)*static_cast<CFTypeRef*>(data);
// DLogFunc(@"%@(%d) %@ self=%p data(id)=%@", NSStringFromClass([object class]), objectID, [ObjectStore StringFromPropertySelector:address->mSelector], self, dataObj);
// } else {
// UInt32 *dataInt = (UInt32 *)data;
// DLogFunc(@"%@(%d) %@ self=%p data(int)=%d", NSStringFromClass([object class]), objectID, [ObjectStore StringFromPropertySelector:address->mSelector], self, *dataInt);
// }
return kCMIOHardwareNoError;
}
OSStatus HardwarePlugIn_ObjectSetPropertyData(
CMIOHardwarePlugInRef self, CMIOObjectID objectID,
const CMIOObjectPropertyAddress *address, UInt32 qualifierDataSize,
const void *qualifierData, UInt32 dataSize, const void *data)
{
NSObject<CMIOObject> *object = [ObjectStore GetObjectWithId:objectID];
if (object == nil) {
DLogFunc(@"ERR nil object");
return kCMIOHardwareBadObjectError;
}
UInt32 *dataInt = (UInt32 *)data;
DLogFunc(@"%@(%d) %@ self=%p data(int)=%d",
NSStringFromClass([object class]), objectID,
[ObjectStore StringFromPropertySelector:address->mSelector],
self, *dataInt);
[object setPropertyDataWithAddress:*address
qualifierDataSize:qualifierDataSize
qualifierData:qualifierData
dataSize:dataSize
data:data];
return kCMIOHardwareNoError;
}
#pragma mark CMIOStream Operations
OSStatus HardwarePlugIn_StreamCopyBufferQueue(
CMIOHardwarePlugInRef self, CMIOStreamID streamID,
CMIODeviceStreamQueueAlteredProc queueAlteredProc,
void *queueAlteredRefCon, CMSimpleQueueRef *queue)
{
Stream *stream = (Stream *)[ObjectStore GetObjectWithId:streamID];
if (stream == nil) {
DLogFunc(@"ERR nil object");
return kCMIOHardwareBadObjectError;
}
*queue = [stream copyBufferQueueWithAlteredProc:queueAlteredProc
alteredRefCon:queueAlteredRefCon];
DLogFunc(@"%@ (id=%d) self=%p queue=%@", stream, streamID, self,
(__bridge NSObject *)*queue);
return kCMIOHardwareNoError;
}
#pragma mark CMIODevice Operations
OSStatus HardwarePlugIn_DeviceStartStream(CMIOHardwarePlugInRef self,
CMIODeviceID deviceID,
CMIOStreamID streamID)
{
DLogFunc(@"self=%p device=%d stream=%d", self, deviceID, streamID);
Stream *stream = (Stream *)[ObjectStore GetObjectWithId:streamID];
if (stream == nil) {
DLogFunc(@"ERR nil object");
return kCMIOHardwareBadObjectError;
}
[[PlugIn SharedPlugIn] startStream];
return kCMIOHardwareNoError;
}
OSStatus HardwarePlugIn_DeviceSuspend(CMIOHardwarePlugInRef self,
CMIODeviceID deviceID)
{
DLogFunc(@"self=%p", self);
return kCMIOHardwareNoError;
}
OSStatus HardwarePlugIn_DeviceResume(CMIOHardwarePlugInRef self,
CMIODeviceID deviceID)
{
DLogFunc(@"self=%p", self);
return kCMIOHardwareNoError;
}
OSStatus HardwarePlugIn_DeviceStopStream(CMIOHardwarePlugInRef self,
CMIODeviceID deviceID,
CMIOStreamID streamID)
{
DLogFunc(@"self=%p device=%d stream=%d", self, deviceID, streamID);
Stream *stream = (Stream *)[ObjectStore GetObjectWithId:streamID];
if (stream == nil) {
DLogFunc(@"ERR nil object");
return kCMIOHardwareBadObjectError;
}
[[PlugIn SharedPlugIn] stopStream];
return kCMIOHardwareNoError;
}
OSStatus
HardwarePlugIn_DeviceProcessAVCCommand(CMIOHardwarePlugInRef self,
CMIODeviceID deviceID,
CMIODeviceAVCCommand *ioAVCCommand)
{
DLogFunc(@"self=%p", self);
return kCMIOHardwareNoError;
}
OSStatus
HardwarePlugIn_DeviceProcessRS422Command(CMIOHardwarePlugInRef self,
CMIODeviceID deviceID,
CMIODeviceRS422Command *ioRS422Command)
{
DLogFunc(@"self=%p", self);
return kCMIOHardwareNoError;
}
OSStatus HardwarePlugIn_StreamDeckPlay(CMIOHardwarePlugInRef self,
CMIOStreamID streamID)
{
DLogFunc(@"self=%p", self);
return kCMIOHardwareIllegalOperationError;
}
OSStatus HardwarePlugIn_StreamDeckStop(CMIOHardwarePlugInRef self,
CMIOStreamID streamID)
{
DLogFunc(@"self=%p", self);
return kCMIOHardwareIllegalOperationError;
}
OSStatus HardwarePlugIn_StreamDeckJog(CMIOHardwarePlugInRef self,
CMIOStreamID streamID, SInt32 speed)
{
DLogFunc(@"self=%p", self);
return kCMIOHardwareIllegalOperationError;
}
OSStatus HardwarePlugIn_StreamDeckCueTo(CMIOHardwarePlugInRef self,
CMIOStreamID streamID,
Float64 requestedTimecode,
Boolean playOnCue)
{
DLogFunc(@"self=%p", self);
return kCMIOHardwareIllegalOperationError;
}
static CMIOHardwarePlugInInterface sInterface = {
// Padding for COM
NULL,
// IUnknown Routines
(HRESULT (*)(void *, CFUUIDBytes,
void **))HardwarePlugIn_QueryInterface,
(ULONG(*)(void *))HardwarePlugIn_AddRef,
(ULONG(*)(void *))HardwarePlugIn_Release,
// DAL Plug-In Routines
HardwarePlugIn_Initialize, HardwarePlugIn_InitializeWithObjectID,
HardwarePlugIn_Teardown, HardwarePlugIn_ObjectShow,
HardwarePlugIn_ObjectHasProperty,
HardwarePlugIn_ObjectIsPropertySettable,
HardwarePlugIn_ObjectGetPropertyDataSize,
HardwarePlugIn_ObjectGetPropertyData,
HardwarePlugIn_ObjectSetPropertyData, HardwarePlugIn_DeviceSuspend,
HardwarePlugIn_DeviceResume, HardwarePlugIn_DeviceStartStream,
HardwarePlugIn_DeviceStopStream, HardwarePlugIn_DeviceProcessAVCCommand,
HardwarePlugIn_DeviceProcessRS422Command,
HardwarePlugIn_StreamCopyBufferQueue, HardwarePlugIn_StreamDeckPlay,
HardwarePlugIn_StreamDeckStop, HardwarePlugIn_StreamDeckJog,
HardwarePlugIn_StreamDeckCueTo};
static CMIOHardwarePlugInInterface *sInterfacePtr = &sInterface;
static CMIOHardwarePlugInRef sPlugInRef = &sInterfacePtr;
CMIOHardwarePlugInRef PlugInRef()
{
return sPlugInRef;
}

View File

@ -0,0 +1,37 @@
//
// PlugInMain.mm
// obs-mac-virtualcam
//
// Created by John Boiles on 4/9/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#import <CoreMediaIO/CMIOHardwarePlugin.h>
#import "PlugInInterface.h"
#import "Logging.h"
#import "Defines.h"
//! PlugInMain is the entrypoint for the plugin
extern "C" {
void *PlugInMain(CFAllocatorRef allocator, CFUUIDRef requestedTypeUUID)
{
DLogFunc(@"version=%@", PLUGIN_VERSION);
if (!CFEqual(requestedTypeUUID, kCMIOHardwarePlugInTypeID)) {
return 0;
}
return PlugInRef();
}
}

View File

@ -0,0 +1,48 @@
//
// Stream.h
// obs-mac-virtualcam
//
// Created by John Boiles on 4/10/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#import <Foundation/Foundation.h>
#import "ObjectStore.h"
NS_ASSUME_NONNULL_BEGIN
@interface Stream : NSObject <CMIOObject>
@property CMIOStreamID objectId;
- (instancetype _Nonnull)init;
- (CMSimpleQueueRef)copyBufferQueueWithAlteredProc:
(CMIODeviceStreamQueueAlteredProc)alteredProc
alteredRefCon:(void *)alteredRefCon;
- (void)startServingDefaultFrames;
- (void)stopServingDefaultFrames;
- (void)queueFrameWithSize:(NSSize)size
timestamp:(uint64_t)timestamp
fpsNumerator:(uint32_t)fpsNumerator
fpsDenominator:(uint32_t)fpsDenominator
frameData:(NSData *)frameData;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,571 @@
//
// Stream.mm
// obs-mac-virtualcam
//
// Created by John Boiles on 4/10/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#import "Stream.h"
#import <AppKit/AppKit.h>
#import <mach/mach_time.h>
#include <CoreMediaIO/CMIOSampleBuffer.h>
#import "Logging.h"
#import "CMSampleBufferUtils.h"
#import "TestCard.h"
#import "PlugIn.h"
@interface Stream () {
CMSimpleQueueRef _queue;
CFTypeRef _clock;
NSImage *_testCardImage;
dispatch_source_t _frameDispatchSource;
NSSize _testCardSize;
Float64 _fps;
}
@property CMIODeviceStreamQueueAlteredProc alteredProc;
@property void *alteredRefCon;
@property (readonly) CMSimpleQueueRef queue;
@property (readonly) CFTypeRef clock;
@property UInt64 sequenceNumber;
@property (readonly) NSImage *testCardImage;
@property (readonly) NSSize testCardSize;
@property (readonly) Float64 fps;
@end
@implementation Stream
#define DEFAULT_FPS 30.0
#define DEFAULT_WIDTH 1280
#define DEFAULT_HEIGHT 720
- (instancetype _Nonnull)init
{
self = [super init];
if (self) {
_frameDispatchSource = dispatch_source_create(
DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
__weak __typeof(self) wself = self;
dispatch_source_set_event_handler(_frameDispatchSource, ^{
[wself fillFrame];
});
}
return self;
}
- (void)dealloc
{
DLog(@"Stream Dealloc");
CMIOStreamClockInvalidate(_clock);
CFRelease(_clock);
_clock = NULL;
CFRelease(_queue);
_queue = NULL;
dispatch_suspend(_frameDispatchSource);
}
- (void)startServingDefaultFrames
{
DLogFunc(@"");
_testCardImage = nil;
_testCardSize = NSZeroSize;
_fps = 0;
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 0);
uint64_t intervalTime = (int64_t)(NSEC_PER_SEC / self.fps);
dispatch_source_set_timer(_frameDispatchSource, startTime, intervalTime,
0);
dispatch_resume(_frameDispatchSource);
}
- (void)stopServingDefaultFrames
{
DLogFunc(@"");
dispatch_suspend(_frameDispatchSource);
}
- (CMSimpleQueueRef)queue
{
if (_queue == NULL) {
// Allocate a one-second long queue, which we can use our FPS constant for.
OSStatus err = CMSimpleQueueCreate(kCFAllocatorDefault,
self.fps, &_queue);
if (err != noErr) {
DLog(@"Err %d in CMSimpleQueueCreate", err);
}
}
return _queue;
}
- (CFTypeRef)clock
{
if (_clock == NULL) {
OSStatus err = CMIOStreamClockCreate(
kCFAllocatorDefault,
CFSTR("obs-mac-virtualcam::Stream::clock"),
(__bridge void *)self, CMTimeMake(1, 10), 100, 10,
&_clock);
if (err != noErr) {
DLog(@"Error %d from CMIOStreamClockCreate", err);
}
}
return _clock;
}
- (NSSize)testCardSize
{
if (NSEqualSizes(_testCardSize, NSZeroSize)) {
NSUserDefaults *defaults =
[NSUserDefaults standardUserDefaults];
int width = [[defaults objectForKey:kTestCardWidthKey]
integerValue];
int height = [[defaults objectForKey:kTestCardHeightKey]
integerValue];
if (width == 0 || height == 0) {
_testCardSize =
NSMakeSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
} else {
_testCardSize = NSMakeSize(width, height);
}
}
return _testCardSize;
}
- (Float64)fps
{
if (_fps == 0) {
NSUserDefaults *defaults =
[NSUserDefaults standardUserDefaults];
double fps =
[[defaults objectForKey:kTestCardFPSKey] doubleValue];
if (fps == 0) {
_fps = DEFAULT_FPS;
} else {
_fps = fps;
}
}
return _fps;
}
- (NSImage *)testCardImage
{
if (_testCardImage == nil) {
NSString *bundlePath =
[[NSBundle bundleForClass:[Stream class]] bundlePath];
NSString *placeHolderPath = [bundlePath
stringByAppendingString:
@"/Contents/Resources/placeholder.png"];
NSImage *placeholderImage = [[NSImage alloc]
initWithContentsOfFile:placeHolderPath];
NSBitmapImageRep *rep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:self.testCardSize.width
pixelsHigh:self.testCardSize.height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bytesPerRow:0
bitsPerPixel:0];
rep.size = self.testCardSize;
float hScale =
placeholderImage.size.width / self.testCardSize.width;
float vScale =
placeholderImage.size.height / self.testCardSize.height;
float scaling = fmax(hScale, vScale);
float newWidth = placeholderImage.size.width / scaling;
float newHeight = placeholderImage.size.height / scaling;
float leftOffset = (self.testCardSize.width - newWidth) / 2;
float topOffset = (self.testCardSize.height - newHeight) / 2;
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext
setCurrentContext:
[NSGraphicsContext
graphicsContextWithBitmapImageRep:rep]];
NSColor *backgroundColor = [NSColor blackColor];
[backgroundColor set];
NSRectFill(NSMakeRect(0, 0, self.testCardSize.width,
self.testCardSize.height));
[placeholderImage drawInRect:NSMakeRect(leftOffset, topOffset,
newWidth, newHeight)
fromRect:NSZeroRect
operation:NSCompositingOperationCopy
fraction:1.0];
[NSGraphicsContext restoreGraphicsState];
NSImage *testCardImage =
[[NSImage alloc] initWithSize:self.testCardSize];
[testCardImage addRepresentation:rep];
_testCardImage = testCardImage;
}
return _testCardImage;
}
- (CMSimpleQueueRef)copyBufferQueueWithAlteredProc:
(CMIODeviceStreamQueueAlteredProc)alteredProc
alteredRefCon:(void *)alteredRefCon
{
self.alteredProc = alteredProc;
self.alteredRefCon = alteredRefCon;
// Retain this since it's a copy operation
CFRetain(self.queue);
return self.queue;
}
- (CVPixelBufferRef)createPixelBufferWithTestAnimation
{
int width = self.testCardSize.width;
int height = self.testCardSize.height;
NSDictionary *options = [NSDictionary
dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES],
kCVPixelBufferCGImageCompatibilityKey,
[NSNumber numberWithBool:YES],
kCVPixelBufferCGBitmapContextCompatibilityKey, nil];
CVPixelBufferRef pxbuffer = NULL;
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, width,
height, kCVPixelFormatType_32ARGB,
(__bridge CFDictionaryRef)options,
&pxbuffer);
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddressOfPlane(pxbuffer, 0);
NSParameterAssert(pxdata != NULL);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(
pxdata, width, height, 8,
CVPixelBufferGetBytesPerRowOfPlane(pxbuffer, 0), rgbColorSpace,
kCGImageAlphaPremultipliedFirst | kCGImageByteOrder32Big);
NSParameterAssert(context);
NSGraphicsContext *nsContext = [NSGraphicsContext
graphicsContextWithCGContext:context
flipped:NO];
[NSGraphicsContext setCurrentContext:nsContext];
NSRect rect = NSMakeRect(0, 0, self.testCardImage.size.width,
self.testCardImage.size.height);
CGImageRef image = [self.testCardImage CGImageForProposedRect:&rect
context:nsContext
hints:nil];
CGContextDrawImage(context,
CGRectMake(0, 0, CGImageGetWidth(image),
CGImageGetHeight(image)),
image);
// DrawDialWithFrame(
// NSMakeRect(0, 0, width, height),
// (int(self.fps) - self.sequenceNumber % int(self.fps)) * 360 /
// int(self.fps));
CGContextRelease(context);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
return pxbuffer;
}
- (void)fillFrame
{
if (CMSimpleQueueGetFullness(self.queue) >= 1.0) {
DLog(@"Queue is full, bailing out");
return;
}
CVPixelBufferRef pixelBuffer =
[self createPixelBufferWithTestAnimation];
uint64_t hostTime = mach_absolute_time();
CMSampleTimingInfo timingInfo =
CMSampleTimingInfoForTimestamp(hostTime, self.fps, 1);
OSStatus err = CMIOStreamClockPostTimingEvent(
timingInfo.presentationTimeStamp, hostTime, true, self.clock);
if (err != noErr) {
DLog(@"CMIOStreamClockPostTimingEvent err %d", err);
}
CMFormatDescriptionRef format;
CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault,
pixelBuffer, &format);
self.sequenceNumber = CMIOGetNextSequenceNumber(self.sequenceNumber);
CMSampleBufferRef buffer;
err = CMIOSampleBufferCreateForImageBuffer(
kCFAllocatorDefault, pixelBuffer, format, &timingInfo,
self.sequenceNumber, kCMIOSampleBufferNoDiscontinuities,
&buffer);
CFRelease(pixelBuffer);
CFRelease(format);
if (err != noErr) {
DLog(@"CMIOSampleBufferCreateForImageBuffer err %d", err);
}
CMSimpleQueueEnqueue(self.queue, buffer);
// Inform the clients that the queue has been altered
if (self.alteredProc != NULL) {
(self.alteredProc)(self.objectId, buffer, self.alteredRefCon);
}
}
- (void)queueFrameWithSize:(NSSize)size
timestamp:(uint64_t)timestamp
fpsNumerator:(uint32_t)fpsNumerator
fpsDenominator:(uint32_t)fpsDenominator
frameData:(NSData *)frameData
{
if (CMSimpleQueueGetFullness(self.queue) >= 1.0) {
DLog(@"Queue is full, bailing out");
return;
}
OSStatus err = noErr;
CMSampleTimingInfo timingInfo = CMSampleTimingInfoForTimestamp(
timestamp, fpsNumerator, fpsDenominator);
err = CMIOStreamClockPostTimingEvent(timingInfo.presentationTimeStamp,
mach_absolute_time(), true,
self.clock);
if (err != noErr) {
DLog(@"CMIOStreamClockPostTimingEvent err %d", err);
}
self.sequenceNumber = CMIOGetNextSequenceNumber(self.sequenceNumber);
CMSampleBufferRef sampleBuffer;
CMSampleBufferCreateFromData(size, timingInfo, self.sequenceNumber,
frameData, &sampleBuffer);
CMSimpleQueueEnqueue(self.queue, sampleBuffer);
// Inform the clients that the queue has been altered
if (self.alteredProc != NULL) {
(self.alteredProc)(self.objectId, sampleBuffer,
self.alteredRefCon);
}
}
- (CMVideoFormatDescriptionRef)getFormatDescription
{
CMVideoFormatDescriptionRef formatDescription;
OSStatus err = CMVideoFormatDescriptionCreate(
kCFAllocatorDefault, kCMVideoCodecType_422YpCbCr8,
self.testCardSize.width, self.testCardSize.height, NULL,
&formatDescription);
if (err != noErr) {
DLog(@"Error %d from CMVideoFormatDescriptionCreate", err);
}
return formatDescription;
}
#pragma mark - CMIOObject
- (UInt32)getPropertyDataSizeWithAddress:(CMIOObjectPropertyAddress)address
qualifierDataSize:(UInt32)qualifierDataSize
qualifierData:(nonnull const void *)qualifierData
{
switch (address.mSelector) {
case kCMIOStreamPropertyInitialPresentationTimeStampForLinkedAndSyncedAudio:
return sizeof(CMTime);
case kCMIOStreamPropertyOutputBuffersNeededForThrottledPlayback:
return sizeof(UInt32);
case kCMIOObjectPropertyName:
return sizeof(CFStringRef);
case kCMIOObjectPropertyManufacturer:
return sizeof(CFStringRef);
case kCMIOObjectPropertyElementName:
return sizeof(CFStringRef);
case kCMIOObjectPropertyElementCategoryName:
return sizeof(CFStringRef);
case kCMIOObjectPropertyElementNumberName:
return sizeof(CFStringRef);
case kCMIOStreamPropertyDirection:
return sizeof(UInt32);
case kCMIOStreamPropertyTerminalType:
return sizeof(UInt32);
case kCMIOStreamPropertyStartingChannel:
return sizeof(UInt32);
case kCMIOStreamPropertyLatency:
return sizeof(UInt32);
case kCMIOStreamPropertyFormatDescriptions:
return sizeof(CFArrayRef);
case kCMIOStreamPropertyFormatDescription:
return sizeof(CMFormatDescriptionRef);
case kCMIOStreamPropertyFrameRateRanges:
return sizeof(AudioValueRange);
case kCMIOStreamPropertyFrameRate:
case kCMIOStreamPropertyFrameRates:
return sizeof(Float64);
case kCMIOStreamPropertyMinimumFrameRate:
return sizeof(Float64);
case kCMIOStreamPropertyClock:
return sizeof(CFTypeRef);
default:
DLog(@"Stream unhandled getPropertyDataSizeWithAddress for %@",
[ObjectStore
StringFromPropertySelector:address.mSelector]);
return 0;
};
}
- (void)getPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
qualifierDataSize:(UInt32)qualifierDataSize
qualifierData:(nonnull const void *)qualifierData
dataSize:(UInt32)dataSize
dataUsed:(nonnull UInt32 *)dataUsed
data:(nonnull void *)data
{
switch (address.mSelector) {
case kCMIOObjectPropertyName:
*static_cast<CFStringRef *>(data) = CFSTR("OBS Virtual Camera");
*dataUsed = sizeof(CFStringRef);
break;
case kCMIOObjectPropertyElementName:
*static_cast<CFStringRef *>(data) =
CFSTR("OBS Virtual Camera Stream Element");
*dataUsed = sizeof(CFStringRef);
break;
case kCMIOObjectPropertyManufacturer:
case kCMIOObjectPropertyElementCategoryName:
case kCMIOObjectPropertyElementNumberName:
case kCMIOStreamPropertyTerminalType:
case kCMIOStreamPropertyStartingChannel:
case kCMIOStreamPropertyLatency:
case kCMIOStreamPropertyInitialPresentationTimeStampForLinkedAndSyncedAudio:
case kCMIOStreamPropertyOutputBuffersNeededForThrottledPlayback:
break;
case kCMIOStreamPropertyDirection:
*static_cast<UInt32 *>(data) = 1;
*dataUsed = sizeof(UInt32);
break;
case kCMIOStreamPropertyFormatDescriptions:
*static_cast<CFArrayRef *>(
data) = (__bridge_retained CFArrayRef)[NSArray
arrayWithObject:(__bridge_transfer NSObject *)
[self getFormatDescription]];
*dataUsed = sizeof(CFArrayRef);
break;
case kCMIOStreamPropertyFormatDescription:
*static_cast<CMVideoFormatDescriptionRef *>(data) =
[self getFormatDescription];
*dataUsed = sizeof(CMVideoFormatDescriptionRef);
break;
case kCMIOStreamPropertyFrameRateRanges:
AudioValueRange range;
range.mMinimum = self.fps;
range.mMaximum = self.fps;
*static_cast<AudioValueRange *>(data) = range;
*dataUsed = sizeof(AudioValueRange);
break;
case kCMIOStreamPropertyFrameRate:
case kCMIOStreamPropertyFrameRates:
*static_cast<Float64 *>(data) = self.fps;
*dataUsed = sizeof(Float64);
break;
case kCMIOStreamPropertyMinimumFrameRate:
*static_cast<Float64 *>(data) = self.fps;
*dataUsed = sizeof(Float64);
break;
case kCMIOStreamPropertyClock:
*static_cast<CFTypeRef *>(data) = self.clock;
// This one was incredibly tricky and cost me many hours to find. It seems that DAL expects
// the clock to be retained when returned. It's unclear why, and that seems inconsistent
// with other properties that don't have the same behavior. But this is what Apple's sample
// code does.
// https://github.com/lvsti/CoreMediaIO-DAL-Example/blob/0392cb/Sources/Extras/CoreMediaIO/DeviceAbstractionLayer/Devices/DP/Properties/CMIO_DP_Property_Clock.cpp#L75
CFRetain(*static_cast<CFTypeRef *>(data));
*dataUsed = sizeof(CFTypeRef);
break;
default:
DLog(@"Stream unhandled getPropertyDataWithAddress for %@",
[ObjectStore
StringFromPropertySelector:address.mSelector]);
*dataUsed = 0;
};
}
- (BOOL)hasPropertyWithAddress:(CMIOObjectPropertyAddress)address
{
switch (address.mSelector) {
case kCMIOObjectPropertyName:
case kCMIOObjectPropertyElementName:
case kCMIOStreamPropertyFormatDescriptions:
case kCMIOStreamPropertyFormatDescription:
case kCMIOStreamPropertyFrameRateRanges:
case kCMIOStreamPropertyFrameRate:
case kCMIOStreamPropertyFrameRates:
case kCMIOStreamPropertyMinimumFrameRate:
case kCMIOStreamPropertyClock:
return true;
case kCMIOObjectPropertyManufacturer:
case kCMIOObjectPropertyElementCategoryName:
case kCMIOObjectPropertyElementNumberName:
case kCMIOStreamPropertyDirection:
case kCMIOStreamPropertyTerminalType:
case kCMIOStreamPropertyStartingChannel:
case kCMIOStreamPropertyLatency:
case kCMIOStreamPropertyInitialPresentationTimeStampForLinkedAndSyncedAudio:
case kCMIOStreamPropertyOutputBuffersNeededForThrottledPlayback:
DLog(@"TODO: %@",
[ObjectStore
StringFromPropertySelector:address.mSelector]);
return false;
default:
DLog(@"Stream unhandled hasPropertyWithAddress for %@",
[ObjectStore
StringFromPropertySelector:address.mSelector]);
return false;
};
}
- (BOOL)isPropertySettableWithAddress:(CMIOObjectPropertyAddress)address
{
DLog(@"Stream unhandled isPropertySettableWithAddress for %@",
[ObjectStore StringFromPropertySelector:address.mSelector]);
return false;
}
- (void)setPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
qualifierDataSize:(UInt32)qualifierDataSize
qualifierData:(nonnull const void *)qualifierData
dataSize:(UInt32)dataSize
data:(nonnull const void *)data
{
DLog(@"Stream unhandled setPropertyDataWithAddress for %@",
[ObjectStore StringFromPropertySelector:address.mSelector]);
}
@end

View File

@ -0,0 +1,14 @@
//
// TestCard.h
// dal-plugin
//
// Created by John Boiles on 5/8/20.
//
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
void DrawTestCardWithFrame(CGContextRef context, NSRect frame);
void DrawDialWithFrame(NSRect frame, CGFloat rotation);
NSImage *ImageOfTestCardWithSize(NSSize imageSize);

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

View File

@ -0,0 +1,59 @@
project(mac-virtualcam)
find_library(AVFOUNDATION AVFoundation)
find_library(APPKIT AppKit)
find_library(COCOA Cocoa)
find_library(COREFOUNDATION CoreFoundation)
find_library(COREMEDIA CoreMedia)
find_library(COREVIDEO CoreVideo)
find_library(COCOA Cocoa)
find_library(COREMEDIAIO CoreMediaIO)
find_library(IOSURFACE IOSurface)
find_library(IOKIT IOKit)
include_directories(${AVFOUNDATION}
${APPKIT}
${COCOA}
${COREFOUNDATION}
${COREMEDIA}
${COREVIDEO}
${COREMEDIAIO}
${COCOA}
${IOSURFACE}
"${CMAKE_SOURCE_DIR}/UI/obs-frontend-api"
../common)
set(mac-virtualcam_HEADERS
Defines.h
MachServer.h
../common/MachProtocol.h)
set(mac-virtualcam_SOURCES
plugin-main.mm
MachServer.mm)
add_library(mac-virtualcam MODULE
${mac-virtualcam_SOURCES}
${mac-virtualcam_HEADERS})
target_link_libraries(mac-virtualcam
libobs
obs-frontend-api
Qt5::Core
Qt5::Widgets
${AVFOUNDATION}
${APPKIT}
${COCOA}
${COREFOUNDATION}
${COREMEDIA}
${COREVIDEO}
${COREMEDIAIO}
${IOSURFACE}
${IOKIT})
set_target_properties(mac-virtualcam PROPERTIES
FOLDER "plugins"
COMPILE_FLAGS "-std=gnu++14 -stdlib=libc++ -fobjc-arc -fobjc-weak"
)
install_obs_plugin_with_data(mac-virtualcam data)

View File

@ -0,0 +1,24 @@
//
// Defines.h
// obs-mac-virtualcam
//
// Created by John Boiles on 5/27/20.
//
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// obs-mac-virtualcam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
#define PLUGIN_NAME "mac-virtualcam"
#define PLUGIN_VERSION "1.3.0"
#define blog(level, msg, ...) \
blog(level, "[" PLUGIN_NAME "] " msg, ##__VA_ARGS__)

View File

@ -0,0 +1,29 @@
//
// MachServer.h
// obs-mac-virtualcam
//
// Created by John Boiles on 5/5/20.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface MachServer : NSObject
- (void)run;
/*!
Will eventually be used for sending frames to all connected clients
*/
- (void)sendFrameWithSize:(NSSize)size
timestamp:(uint64_t)timestamp
fpsNumerator:(uint32_t)fpsNumerator
fpsDenominator:(uint32_t)fpsDenominator
frameBytes:(uint8_t *)frameBytes;
- (void)stop;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,178 @@
//
// MachServer.m
// mac-virtualcam
//
// Created by John Boiles on 5/5/20.
//
#import "MachServer.h"
#import <Foundation/Foundation.h>
#include <obs-module.h>
#include "MachProtocol.h"
#include "Defines.h"
@interface MachServer () <NSPortDelegate>
@property NSPort *port;
@property NSMutableSet *clientPorts;
@property NSRunLoop *runLoop;
@end
@implementation MachServer
- (id)init
{
if (self = [super init]) {
self.clientPorts = [[NSMutableSet alloc] init];
}
return self;
}
- (void)dealloc
{
blog(LOG_DEBUG, "tearing down MachServer");
[self.runLoop removePort:self.port forMode:NSDefaultRunLoopMode];
[self.port invalidate];
self.port.delegate = nil;
}
- (void)run
{
if (self.port != nil) {
blog(LOG_DEBUG, "mach server already running!");
return;
}
// It's a bummer this is deprecated. The replacement, NSXPCConnection, seems to require
// an assistant process that lives inside the .app bundle. This would be more modern, but adds
// complexity and I think makes it impossible to just run the `obs` binary from the commandline.
// So let's stick with NSMachBootstrapServer at least until it fully goes away.
// At that point we can decide between NSXPCConnection and using the CoreFoundation versions of
// these APIs (which are, interestingly, not deprecated)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
self.port = [[NSMachBootstrapServer sharedInstance]
servicePortWithName:@MACH_SERVICE_NAME];
#pragma clang diagnostic pop
if (self.port == nil) {
// This probably means another instance is running.
blog(LOG_ERROR, "Unable to open mach server port.");
return;
}
self.port.delegate = self;
self.runLoop = [NSRunLoop currentRunLoop];
[self.runLoop addPort:self.port forMode:NSDefaultRunLoopMode];
blog(LOG_DEBUG, "mach server running!");
}
- (void)handlePortMessage:(NSPortMessage *)message
{
switch (message.msgid) {
case MachMsgIdConnect:
if (message.sendPort != nil) {
blog(LOG_DEBUG,
"mach server received connect message from port %d!",
((NSMachPort *)message.sendPort).machPort);
[self.clientPorts addObject:message.sendPort];
}
break;
default:
blog(LOG_ERROR, "Unexpected mach message ID %u",
(unsigned)message.msgid);
break;
}
}
- (void)sendMessageToClientsWithMsgId:(uint32_t)msgId
components:(nullable NSArray *)components
{
if ([self.clientPorts count] <= 0) {
return;
}
NSMutableSet *removedPorts = [NSMutableSet set];
for (NSPort *port in self.clientPorts) {
@try {
NSPortMessage *message = [[NSPortMessage alloc]
initWithSendPort:port
receivePort:nil
components:components];
message.msgid = msgId;
if (![message
sendBeforeDate:
[NSDate dateWithTimeIntervalSinceNow:
1.0]]) {
blog(LOG_DEBUG,
"failed to send message to %d, removing it from the clients!",
((NSMachPort *)port).machPort);
[removedPorts addObject:port];
}
} @catch (NSException *exception) {
blog(LOG_DEBUG,
"failed to send message (exception) to %d, removing it from the clients!",
((NSMachPort *)port).machPort);
[removedPorts addObject:port];
}
}
// Remove dead ports if necessary
[self.clientPorts minusSet:removedPorts];
}
- (void)sendFrameWithSize:(NSSize)size
timestamp:(uint64_t)timestamp
fpsNumerator:(uint32_t)fpsNumerator
fpsDenominator:(uint32_t)fpsDenominator
frameBytes:(uint8_t *)frameBytes
{
if ([self.clientPorts count] <= 0) {
return;
}
@autoreleasepool {
CGFloat width = size.width;
NSData *widthData = [NSData dataWithBytes:&width
length:sizeof(width)];
CGFloat height = size.height;
NSData *heightData = [NSData dataWithBytes:&height
length:sizeof(height)];
NSData *timestampData = [NSData
dataWithBytes:&timestamp
length:sizeof(timestamp)];
NSData *fpsNumeratorData = [NSData
dataWithBytes:&fpsNumerator
length:sizeof(fpsNumerator)];
NSData *fpsDenominatorData = [NSData
dataWithBytes:&fpsDenominator
length:sizeof(fpsDenominator)];
// NOTE: I'm not totally sure about the safety of dataWithBytesNoCopy in this context.
// Seems like there could potentially be an issue if the frameBuffer went away before the
// mach message finished sending. But it seems to be working and avoids a memory copy. Alternately
// we could do something like
// NSData *frameData = [NSData dataWithBytes:(void *)frameBytes length:size.width * size.height * 2];
NSData *frameData = [NSData
dataWithBytesNoCopy:(void *)frameBytes
length:size.width * size.height * 2
freeWhenDone:NO];
[self sendMessageToClientsWithMsgId:MachMsgIdFrame
components:@[
widthData, heightData,
timestampData, frameData,
fpsNumeratorData,
fpsDenominatorData
]];
}
}
- (void)stop
{
blog(LOG_DEBUG, "sending stop message to %lu clients",
self.clientPorts.count);
[self sendMessageToClientsWithMsgId:MachMsgIdStop components:nil];
}
@end

View File

@ -0,0 +1,5 @@
UnsupportedResolution_Title="Unsupported resolution"
UnsupportedResolution_Main="Your output resolution not supported. Please use one of the following:"
VirtualCamera_Start="Start Virtual Camera"
VirtualCamera_Stop="Stop Virtual Camera"
Plugin_Name="macOS Virtual Webcam"

View File

@ -0,0 +1,208 @@
#include <obs-module.h>
#include <obs.hpp>
#include <pthread.h>
#include <QMainWindow.h>
#include <QAction.h>
#include <obs-frontend-api.h>
#include <obs.h>
#include <CoreFoundation/CoreFoundation.h>
#include <AppKit/AppKit.h>
#include "MachServer.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";
}
obs_output_t *outputRef;
obs_video_info videoInfo;
static MachServer *sMachServer;
static bool check_dal_plugin()
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *dalPluginDestinationPath =
@"/Library/CoreMediaIO/Plug-Ins/DAL/";
NSString *dalPluginFileName = [dalPluginDestinationPath
stringByAppendingString:@"obs-mac-virtualcam.plugin"];
BOOL dalPluginInstalled =
[fileManager fileExistsAtPath:dalPluginFileName];
BOOL dalPluginUpdateNeeded = NO;
if (dalPluginInstalled) {
NSString *dalPluginPlistPath = [dalPluginFileName
stringByAppendingString:@"/Contents/Info.plist"];
NSDictionary *dalPluginInfoPlist = [NSDictionary
dictionaryWithContentsOfURL:
[NSURL fileURLWithPath:dalPluginPlistPath]
error:nil];
NSString *dalPluginVersion = [dalPluginInfoPlist
valueForKey:@"CFBundleShortVersionString"];
const char *obsVersion = obs_get_version_string();
if (![dalPluginVersion isEqualToString:@(obsVersion)]) {
dalPluginUpdateNeeded = YES;
}
} else {
dalPluginUpdateNeeded = YES;
}
if (dalPluginUpdateNeeded) {
NSString *dalPluginSourcePath;
NSRunningApplication *app =
[NSRunningApplication currentApplication];
if ([app bundleIdentifier] != nil) {
NSURL *bundleURL = [app bundleURL];
NSString *pluginPath =
@"Contents/Resources/data/obs-mac-virtualcam.plugin";
NSURL *pluginUrl = [bundleURL
URLByAppendingPathComponent:pluginPath];
dalPluginSourcePath = [pluginUrl path];
} else {
dalPluginSourcePath = [[[[app executableURL]
URLByAppendingPathComponent:
@"../data/obs-mac-virtualcam.plugin"]
path]
stringByReplacingOccurrencesOfString:@"obs/"
withString:@""];
}
if ([fileManager fileExistsAtPath:dalPluginSourcePath]) {
NSString *copyCmd = [NSString
stringWithFormat:
@"do shell script \"cp -R '%@' '%@'\" with administrator privileges",
dalPluginSourcePath,
dalPluginDestinationPath];
NSDictionary *errorDict;
NSAppleEventDescriptor *returnDescriptor = NULL;
NSAppleScript *scriptObject =
[[NSAppleScript alloc] initWithSource:copyCmd];
returnDescriptor =
[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 {
blog(LOG_INFO,
"[macOS] VirtualCam DAL Plugin not shipped with OBS");
return false;
}
}
return true;
}
static const char *virtualcam_output_get_name(void *type_data)
{
(void)type_data;
return obs_module_text("macOS Virtual Webcam");
}
// This is a dummy pointer so we have something to return from virtualcam_output_create
static void *data = &data;
static void *virtualcam_output_create(obs_data_t *settings,
obs_output_t *output)
{
outputRef = output;
blog(LOG_DEBUG, "output_create");
sMachServer = [[MachServer alloc] init];
return data;
}
static void virtualcam_output_destroy(void *data)
{
blog(LOG_DEBUG, "output_destroy");
sMachServer = nil;
}
static bool virtualcam_output_start(void *data)
{
bool hasDalPlugin = check_dal_plugin();
if (!hasDalPlugin) {
return false;
}
blog(LOG_DEBUG, "output_start");
[sMachServer run];
obs_get_video_info(&videoInfo);
struct video_scale_info conversion = {};
conversion.format = VIDEO_FORMAT_UYVY;
conversion.width = videoInfo.output_width;
conversion.height = videoInfo.output_height;
obs_output_set_video_conversion(outputRef, &conversion);
if (!obs_output_begin_data_capture(outputRef, 0)) {
return false;
}
return true;
}
static void virtualcam_output_stop(void *data, uint64_t ts)
{
blog(LOG_DEBUG, "output_stop");
obs_output_end_data_capture(outputRef);
[sMachServer stop];
}
static void virtualcam_output_raw_video(void *data, struct video_data *frame)
{
uint8_t *outData = frame->data[0];
if (frame->linesize[0] != (videoInfo.output_width * 2)) {
blog(LOG_ERROR,
"unexpected frame->linesize (expected:%d actual:%d)",
(videoInfo.output_width * 2), frame->linesize[0]);
}
CGFloat width = videoInfo.output_width;
CGFloat height = videoInfo.output_height;
[sMachServer sendFrameWithSize:NSMakeSize(width, height)
timestamp:frame->timestamp
fpsNumerator:videoInfo.fps_num
fpsDenominator:videoInfo.fps_den
frameBytes:outData];
}
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)
{
blog(LOG_INFO, "version=%s", PLUGIN_VERSION);
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;
}