New upstream version 26.1.0+dfsg1

This commit is contained in:
Sebastian Ramacher 2020-12-22 18:32:50 +01:00
parent 040dcc3fc2
commit 013818c4af
594 changed files with 19576 additions and 4478 deletions

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,109 @@
# Universal build for Apple Silicon
set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64")
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
OBSDALPlugInInterface.h
OBSDALObjectStore.h
OBSDALPlugIn.h
OBSDALDevice.h
OBSDALStream.h
CMSampleBufferUtils.h
OBSDALMachClient.h
../common/MachProtocol.h)
set(mac-dal-plugin_SOURCES
OBSDALPlugInMain.mm
OBSDALPlugInInterface.mm
OBSDALObjectStore.mm
OBSDALPlugIn.mm
OBSDALDevice.mm
OBSDALStream.mm
CMSampleBufferUtils.mm
OBSDALMachClient.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,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>7E950B8C-5E49-4B9E-B7D0-B3608A08E8F6</key>
<string>PlugInMain</string>
</dict>
<key>CFPlugInTypes</key>
<dict>
<key>30010C1C-93BF-11D8-8B5B-000A95AF9C6A</key>
<array>
<string>7E950B8C-5E49-4B9E-B7D0-B3608A08E8F6</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,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 "OBSDALObjectStore.h"
NS_ASSUME_NONNULL_BEGIN
@interface OBSDALDevice : 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 "OBSDALDevice.h"
#import <CoreFoundation/CoreFoundation.h>
#include <IOKit/audio/IOAudioTypes.h>
#import "OBSDALPlugin.h"
#import "Logging.h"
@interface OBSDALDevice ()
@property BOOL excludeNonDALAccess;
@property pid_t masterPid;
@end
@implementation OBSDALDevice
// 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 %@",
[OBSDALObjectStore
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 %@",
[OBSDALObjectStore
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 %@",
[OBSDALObjectStore
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 %@",
[OBSDALObjectStore
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 %@",
[OBSDALObjectStore
StringFromPropertySelector:address.mSelector]);
break;
};
}
@end

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 OBSDALMachClient : 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 "OBSDALMachClient.h"
#import "MachProtocol.h"
#import "Logging.h"
@interface OBSDALMachClient () <NSPortDelegate> {
NSPort *_receivePort;
}
@end
@implementation OBSDALMachClient
- (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 OBSDALObjectStore : NSObject
+ (OBSDALObjectStore *)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 "OBSDALObjectStore.h"
@interface OBSDALObjectStore ()
@property NSMutableDictionary *objectMap;
@end
@implementation OBSDALObjectStore
// 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;
}
}
+ (OBSDALObjectStore *)SharedObjectStore
{
static OBSDALObjectStore *sObjectStore = nil;
static dispatch_once_t sOnceToken;
dispatch_once(&sOnceToken, ^{
sObjectStore = [[self alloc] init];
});
return sObjectStore;
}
+ (NSObject<CMIOObject> *)GetObjectWithId:(CMIOObjectID)objectId
{
return [[OBSDALObjectStore 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,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 OBSDALPlugInRef();

View file

@ -0,0 +1,457 @@
//
// 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 "OBSDALPlugInInterface.h"
#import <CoreFoundation/CFUUID.h>
#import "OBSDALPlugin.h"
#import "OBSDALDevice.h"
#import "OBSDALStream.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 = OBSDALPlugInRef();
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;
OBSDALPlugin *plugIn = [OBSDALPlugin SharedPlugIn];
plugIn.objectId = objectID;
[[OBSDALObjectStore SharedObjectStore] setObject:plugIn
forObjectId:objectID];
OBSDALDevice *device = [[OBSDALDevice alloc] init];
CMIOObjectID deviceId;
error = CMIOObjectCreate(OBSDALPlugInRef(), kCMIOObjectSystemObject,
kCMIODeviceClassID, &deviceId);
if (error != noErr) {
DLog(@"CMIOObjectCreate Error %d", error);
return error;
}
device.objectId = deviceId;
device.pluginId = objectID;
[[OBSDALObjectStore SharedObjectStore] setObject:device
forObjectId:deviceId];
OBSDALStream *stream = [[OBSDALStream alloc] init];
CMIOObjectID streamId;
error = CMIOObjectCreate(OBSDALPlugInRef(), deviceId,
kCMIOStreamClassID, &streamId);
if (error != noErr) {
DLog(@"CMIOObjectCreate Error %d", error);
return error;
}
stream.objectId = streamId;
[[OBSDALObjectStore SharedObjectStore] setObject:stream
forObjectId:streamId];
device.streamId = streamId;
plugIn.stream = stream;
// Tell the system about the Device
error = CMIOObjectsPublishedAndDied(
OBSDALPlugInRef(), 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(OBSDALPlugInRef(), 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;
OBSDALPlugin *plugIn = [OBSDALPlugin 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 =
[OBSDALObjectStore 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 =
[OBSDALObjectStore 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,
[OBSDALObjectStore
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 =
[OBSDALObjectStore 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 =
[OBSDALObjectStore 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 =
[OBSDALObjectStore 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,
[OBSDALObjectStore
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)
{
OBSDALStream *stream =
(OBSDALStream *)[OBSDALObjectStore 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);
OBSDALStream *stream =
(OBSDALStream *)[OBSDALObjectStore GetObjectWithId:streamID];
if (stream == nil) {
DLogFunc(@"ERR nil object");
return kCMIOHardwareBadObjectError;
}
[[OBSDALPlugin 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);
OBSDALStream *stream =
(OBSDALStream *)[OBSDALObjectStore GetObjectWithId:streamID];
if (stream == nil) {
DLogFunc(@"ERR nil object");
return kCMIOHardwareBadObjectError;
}
[[OBSDALPlugin 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 OBSDALPlugInRef()
{
return sPlugInRef;
}

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 "OBSDALObjectStore.h"
#import "OBSDALMachClient.h"
#import "OBSDALStream.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 OBSDALPlugin : NSObject <CMIOObject>
@property CMIOObjectID objectId;
@property (readonly) OBSDALMachClient *machClient;
@property OBSDALStream *stream;
+ (OBSDALPlugin *)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 "OBSDALPlugin.h"
#import <CoreMediaIO/CMIOHardwarePlugin.h>
#import "Logging.h"
typedef enum {
PlugInStateNotStarted = 0,
PlugInStateWaitingForServer,
PlugInStateReceivingFrames,
} OBSDALPlugInState;
@interface OBSDALPlugin () <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 OBSDALPlugInState state;
@property OBSDALMachClient *machClient;
@end
@implementation OBSDALPlugin
+ (OBSDALPlugin *)SharedPlugIn
{
static OBSDALPlugin *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 = [[OBSDALMachClient 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 %@",
[OBSDALObjectStore
StringFromPropertySelector:address.mSelector]);
return false;
};
}
- (BOOL)isPropertySettableWithAddress:(CMIOObjectPropertyAddress)address
{
switch (address.mSelector) {
case kCMIOObjectPropertyName:
return false;
default:
DLog(@"PlugIn unhandled isPropertySettableWithAddress for %@",
[OBSDALObjectStore
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 %@",
[OBSDALObjectStore
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 %@",
[OBSDALObjectStore
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 %@",
[OBSDALObjectStore 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,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 "OBSDALPlugInInterface.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 OBSDALPlugInRef();
}
}

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 "OBSDALObjectStore.h"
NS_ASSUME_NONNULL_BEGIN
@interface OBSDALStream : 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,570 @@
//
// 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 "OBSDALStream.h"
#import <AppKit/AppKit.h>
#import <mach/mach_time.h>
#include <CoreMediaIO/CMIOSampleBuffer.h>
#import "Logging.h"
#import "CMSampleBufferUtils.h"
#import "OBSDALPlugin.h"
@interface OBSDALStream () {
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 OBSDALStream
#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:[OBSDALStream 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 %@",
[OBSDALObjectStore
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 %@",
[OBSDALObjectStore
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: %@",
[OBSDALObjectStore
StringFromPropertySelector:address.mSelector]);
return false;
default:
DLog(@"Stream unhandled hasPropertyWithAddress for %@",
[OBSDALObjectStore
StringFromPropertySelector:address.mSelector]);
return false;
};
}
- (BOOL)isPropertySettableWithAddress:(CMIOObjectPropertyAddress)address
{
DLog(@"Stream unhandled isPropertySettableWithAddress for %@",
[OBSDALObjectStore 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 %@",
[OBSDALObjectStore StringFromPropertySelector:address.mSelector]);
}
@end

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
OBSDALMachServer.h
../common/MachProtocol.h)
set(mac-virtualcam_SOURCES
plugin-main.mm
OBSDALMachServer.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 OBSDALMachServer : 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 "OBSDALMachServer.h"
#import <Foundation/Foundation.h>
#include <obs-module.h>
#include "MachProtocol.h"
#include "Defines.h"
@interface OBSDALMachServer () <NSPortDelegate>
@property NSPort *port;
@property NSMutableSet *clientPorts;
@property NSRunLoop *runLoop;
@end
@implementation OBSDALMachServer
- (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,2 @@
Plugin_Name="كاميرا ويب افتراضية macOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS Əyani Veb Kamera"

View file

@ -0,0 +1,2 @@
Plugin_Name="Càmera web virtual del macOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS virtuální webkamera"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS Virtual Webcam"

View file

@ -0,0 +1,2 @@
Plugin_Name="Virtuelle Kamera für macOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS Virtual Webcam"

View file

@ -0,0 +1 @@
Plugin_Name="macOS Virtual Webcam"

View file

@ -0,0 +1,2 @@
Plugin_Name="Cámara Web Virtual macOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS virtuaalne veebikaamera"

View file

@ -0,0 +1,2 @@
Plugin_Name="وبکم مجازی سیستم عامل مک"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS virtuaalinen web-kamera"

View file

@ -0,0 +1,2 @@
Plugin_Name="Webcam virtuelle macOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="Cámara Web Virtual do macOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="מצלמת רשת וירטואלית macOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS Virtuális Webkamera"

View file

@ -0,0 +1,2 @@
Plugin_Name="kamera web Virtual macOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="Webcam Virtuale macOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS仮想ウェブカメラ"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS ვირტუალური კამერა"

View file

@ -0,0 +1,2 @@
Plugin_Name="takamiṛat tuhlist macOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS 가상 웹캠"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS Virtual Webcam"

View file

@ -0,0 +1,2 @@
Plugin_Name="Wirtualna kamera macOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="Webcam Virtual do macOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="Webcam Virtual do MacOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS виртуальная веб-камера"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS Virtuálna Webkamera"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS virtualna spletna kamera"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS virtuell webbkamera"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS Sanal Web Kamerası"

View file

@ -0,0 +1,2 @@
Plugin_Name="Віртуальна вебкамера macOS"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS 虚拟摄像头"

View file

@ -0,0 +1,2 @@
Plugin_Name="macOS 虛擬攝像機"

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 "OBSDALMachServer.h"
#include "Defines.h"
OBS_DECLARE_MODULE()
OBS_MODULE_USE_DEFAULT_LOCALE("mac-virtualcam", "en-US")
MODULE_EXPORT const char *obs_module_description(void)
{
return "macOS virtual webcam output";
}
obs_output_t *outputRef;
obs_video_info videoInfo;
static OBSDALMachServer *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("Plugin_Name");
}
// 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 = [[OBSDALMachServer 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;
}