Add virtualcam plugin to OBS codebase
Co-authored-by: lvsti <lvsti@users.noreply.github.com> Co-authored-by: Sebastian Beckmann <beckmann.sebastian@outlook.de> Co-authored-by: Stefan Huber <sh@signalwerk.ch> Co-authored-by: Ryohei Ikegami <iofg2100@gmail.com> Co-authored-by: Colin Dean <colin.dean@target.com> Co-authored-by: Wolfgang Ladermann <extern.ladermann_wolfgang@allianz.de> Co-authored-by: Simon Eves <simon.eves@omnisci.com> Co-authored-by: Colin Nelson <colnnelson@google.com> Co-authored-by: Yoshimasa Niwa <niw@niw.at> Co-authored-by: Michael Karliner <mike@modern-industry.com> Co-authored-by: Jason Grout <jgrout6@bloomberg.net> Co-authored-by: Alfredo Inostroza <jadenguy@gmail.com> Co-authored-by: Daniel Kennett <daniel@cascable.se> Co-authored-by: Gary Ewan Park <gep13@gep13.co.uk> Co-authored-by: José Carlos Cieni Júnior <cienijr@outlook.com>
This commit is contained in:
parent
b32abbe33f
commit
2700db9ff9
3
.github/workflows/main.yml
vendored
3
.github/workflows/main.yml
vendored
@ -215,6 +215,7 @@ jobs:
|
||||
-x ./OBS.app/Contents/PlugIns/mac-decklink.so \
|
||||
-x ./OBS.app/Contents/PlugIns/mac-syphon.so \
|
||||
-x ./OBS.app/Contents/PlugIns/mac-vth264.so \
|
||||
-x ./OBS.app/Contents/PlugIns/mac-virtualcam.so \
|
||||
-x ./OBS.app/Contents/PlugIns/obs-browser.so \
|
||||
-x ./OBS.app/Contents/PlugIns/obs-browser-page \
|
||||
-x ./OBS.app/Contents/PlugIns/obs-ffmpeg.so \
|
||||
@ -266,6 +267,8 @@ jobs:
|
||||
codesign --force --options runtime --sign "${SIGN_IDENTITY:--}" "./OBS.app/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libswiftshader_libGLESv2.dylib"
|
||||
codesign --force --options runtime --sign "${SIGN_IDENTITY:--}" --deep "./OBS.app/Contents/Frameworks/Chromium Embedded Framework.framework"
|
||||
|
||||
codesign --force --options runtime --deep --sign "${SIGN_IDENTITY:--}" "./OBS.app/Contents/Resources/data/obs-mac-virtualcam.plugin"
|
||||
|
||||
codesign --force --options runtime --entitlements "../CI/scripts/macos/app/entitlements.plist" --sign "${SIGN_IDENTITY:--}" --deep ./OBS.app
|
||||
|
||||
codesign -dvv ./OBS.app
|
||||
|
@ -318,6 +318,7 @@ bundle_dylibs() {
|
||||
-x ./OBS.app/Contents/PlugIns/mac-decklink.so \
|
||||
-x ./OBS.app/Contents/PlugIns/mac-syphon.so \
|
||||
-x ./OBS.app/Contents/PlugIns/mac-vth264.so \
|
||||
-x ./OBS.app/Contents/PlugIns/mac-virtualcam.so \
|
||||
-x ./OBS.app/Contents/PlugIns/obs-browser.so \
|
||||
-x ./OBS.app/Contents/PlugIns/obs-browser-page \
|
||||
-x ./OBS.app/Contents/PlugIns/obs-ffmpeg.so \
|
||||
@ -508,6 +509,11 @@ codesign_bundle() {
|
||||
codesign --force --options runtime --sign "${CODESIGN_IDENT}" --deep "./OBS.app/Contents/Frameworks/Chromium Embedded Framework.framework"
|
||||
echo -n "${COLOR_RESET}"
|
||||
|
||||
step "Code-sign DAL Plugin..."
|
||||
echo -n "${COLOR_ORANGE}"
|
||||
codesign --force --options runtime --deep --sign "${CODESIGN_IDENT}" "./OBS.app/Contents/Resources/data/obs-mac-virtualcam.plugin"
|
||||
echo -n "${COLOR_RESET}"
|
||||
|
||||
step "Code-sign OBS code..."
|
||||
echo -n "${COLOR_ORANGE}"
|
||||
codesign --force --options runtime --entitlements "${CI_SCRIPTS}/app/entitlements.plist" --sign "${CODESIGN_IDENT}" --deep ./OBS.app
|
||||
|
@ -30,6 +30,7 @@ elseif(APPLE)
|
||||
add_subdirectory(mac-capture)
|
||||
add_subdirectory(mac-vth264)
|
||||
add_subdirectory(mac-syphon)
|
||||
add_subdirectory(mac-virtualcam)
|
||||
add_subdirectory(decklink/mac)
|
||||
add_subdirectory(vlc-video)
|
||||
add_subdirectory(linux-jack)
|
||||
|
2
plugins/mac-virtualcam/CMakeLists.txt
Normal file
2
plugins/mac-virtualcam/CMakeLists.txt
Normal file
@ -0,0 +1,2 @@
|
||||
add_subdirectory(src/obs-plugin)
|
||||
add_subdirectory(src/dal-plugin)
|
0
plugins/mac-virtualcam/data/locale/en-US.ini
Normal file
0
plugins/mac-virtualcam/data/locale/en-US.ini
Normal file
17
plugins/mac-virtualcam/src/common/MachProtocol.h
Normal file
17
plugins/mac-virtualcam/src/common/MachProtocol.h
Normal 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;
|
22
plugins/mac-virtualcam/src/dal-plugin/CMSampleBufferUtils.h
Normal file
22
plugins/mac-virtualcam/src/dal-plugin/CMSampleBufferUtils.h
Normal 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);
|
187
plugins/mac-virtualcam/src/dal-plugin/CMSampleBufferUtils.mm
Normal file
187
plugins/mac-virtualcam/src/dal-plugin/CMSampleBufferUtils.mm
Normal 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;
|
||||
}
|
108
plugins/mac-virtualcam/src/dal-plugin/CMakeLists.txt
Normal file
108
plugins/mac-virtualcam/src/dal-plugin/CMakeLists.txt
Normal file
@ -0,0 +1,108 @@
|
||||
project(mac-dal-plugin)
|
||||
|
||||
find_library(AVFOUNDATION AVFoundation)
|
||||
find_library(COCOA Cocoa)
|
||||
find_library(COREFOUNDATION CoreFoundation)
|
||||
find_library(COREMEDIA CoreMedia)
|
||||
find_library(COREVIDEO CoreVideo)
|
||||
find_library(COCOA Cocoa)
|
||||
find_library(COREMEDIAIO CoreMediaIO)
|
||||
find_library(IOSURFACE IOSurface)
|
||||
find_library(IOKIT IOKit)
|
||||
|
||||
# Possible we could remove osme of these
|
||||
include_directories(${AVFOUNDATION}
|
||||
${COCOA}
|
||||
${COREFOUNDATION}
|
||||
${COREMEDIA}
|
||||
${COREVIDEO}
|
||||
${COREMEDIAIO}
|
||||
${COCOA}
|
||||
${IOSURFACE}
|
||||
./
|
||||
../common)
|
||||
|
||||
set(mac-dal-plugin_HEADERS
|
||||
Defines.h
|
||||
Logging.h
|
||||
PlugInInterface.h
|
||||
ObjectStore.h
|
||||
PlugIn.h
|
||||
Device.h
|
||||
Stream.h
|
||||
CMSampleBufferUtils.h
|
||||
MachClient.h
|
||||
TestCard.h
|
||||
../common/MachProtocol.h)
|
||||
|
||||
set(mac-dal-plugin_SOURCES
|
||||
PlugInMain.mm
|
||||
PlugInInterface.mm
|
||||
ObjectStore.mm
|
||||
PlugIn.mm
|
||||
Device.mm
|
||||
Stream.mm
|
||||
CMSampleBufferUtils.mm
|
||||
MachClient.mm
|
||||
TestCard.mm)
|
||||
|
||||
add_library(mac-dal-plugin MODULE
|
||||
${mac-dal-plugin_SOURCES}
|
||||
${mac-dal-plugin_HEADERS})
|
||||
|
||||
set_target_properties(mac-dal-plugin PROPERTIES
|
||||
FOLDER "plugins"
|
||||
BUNDLE TRUE
|
||||
OUTPUT_NAME "obs-mac-virtualcam"
|
||||
COMPILE_FLAGS "-std=gnu++14 -stdlib=libc++ -fobjc-arc -fobjc-weak")
|
||||
|
||||
if (XCODE)
|
||||
set(TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/Debug")
|
||||
else (XCODE)
|
||||
set(TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
endif (XCODE)
|
||||
|
||||
target_link_libraries(mac-dal-plugin
|
||||
${AVFOUNDATION}
|
||||
${COCOA}
|
||||
${COREFOUNDATION}
|
||||
${COREMEDIA}
|
||||
${COREVIDEO}
|
||||
${COREMEDIAIO}
|
||||
${IOSURFACE}
|
||||
${IOKIT})
|
||||
|
||||
add_custom_command(TARGET mac-dal-plugin
|
||||
POST_BUILD
|
||||
COMMAND rm -rf ${TARGET_DIR}/obs-mac-virtualcam.plugin || true
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${TARGET_DIR}/obs-mac-virtualcam.bundle ${TARGET_DIR}/obs-mac-virtualcam.plugin
|
||||
COMMENT "Rename bundle to plugin"
|
||||
)
|
||||
|
||||
# Note: Xcode seems to run a command `builtin-infoPlistUtility` to generate the Info.plist, but I'm
|
||||
# not sure where to find that binary. If we had access to it, the command would look something like:
|
||||
# builtin-infoPlistUtility ${PROJECT_SOURCE_DIR}/../common/CoreMediaIO/DeviceAbstractionLayer/Devices/Sample/PlugIn/SampleVCam-Info.plist -producttype com.apple.product-type.bundle -expandbuildsettings -platform macosx -o mac-virtualcam.bundle/Contents/Info.plist
|
||||
# Instead, just copy in one that was already generated from Xcode.
|
||||
add_custom_command(TARGET mac-dal-plugin
|
||||
POST_BUILD
|
||||
COMMAND cp ${PROJECT_SOURCE_DIR}/Info.plist ${TARGET_DIR}/obs-mac-virtualcam.plugin/Contents/Info.plist
|
||||
COMMAND mkdir ${TARGET_DIR}/obs-mac-virtualcam.plugin/Contents/Resources
|
||||
COMMAND cp ${PROJECT_SOURCE_DIR}/placeholder.png ${TARGET_DIR}/obs-mac-virtualcam.plugin/Contents/Resources/placeholder.png
|
||||
COMMAND /usr/bin/plutil -insert CFBundleVersion -string "${OBS_VERSION}" ${TARGET_DIR}/obs-mac-virtualcam.plugin/Contents/Info.plist
|
||||
COMMAND /usr/bin/plutil -insert CFBundleShortVersionString -string "${OBS_VERSION}" ${TARGET_DIR}/obs-mac-virtualcam.plugin/Contents/Info.plist
|
||||
DEPENDS {PROJECT_SOURCE_DIR}/Info.plist
|
||||
COMMENT "Copy in Info.plist"
|
||||
)
|
||||
|
||||
add_custom_command(TARGET mac-dal-plugin
|
||||
POST_BUILD
|
||||
COMMAND /usr/bin/codesign --force --deep --sign - --timestamp=none ${TARGET_DIR}/obs-mac-virtualcam.plugin
|
||||
COMMENT "Codesign plugin"
|
||||
)
|
||||
|
||||
add_custom_command(TARGET mac-dal-plugin
|
||||
POST_BUILD
|
||||
COMMAND rm -rf "${OBS_OUTPUT_DIR}/$<CONFIGURATION>/data/obs-mac-virtualcam.plugin" || true
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${TARGET_DIR}/obs-mac-virtualcam.plugin "${OBS_OUTPUT_DIR}/$<CONFIGURATION>/data/obs-mac-virtualcam.plugin"
|
||||
COMMENT "Copy plugin to destination"
|
||||
)
|
21
plugins/mac-virtualcam/src/dal-plugin/Defines.h
Executable file
21
plugins/mac-virtualcam/src/dal-plugin/Defines.h
Executable 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"
|
34
plugins/mac-virtualcam/src/dal-plugin/Device.h
Normal file
34
plugins/mac-virtualcam/src/dal-plugin/Device.h
Normal file
@ -0,0 +1,34 @@
|
||||
//
|
||||
// Device.h
|
||||
// obs-mac-virtualcam
|
||||
//
|
||||
// Created by John Boiles on 4/10/20.
|
||||
//
|
||||
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// obs-mac-virtualcam is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "ObjectStore.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface Device : NSObject <CMIOObject>
|
||||
|
||||
@property CMIOObjectID objectId;
|
||||
@property CMIOObjectID pluginId;
|
||||
@property CMIOObjectID streamId;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
295
plugins/mac-virtualcam/src/dal-plugin/Device.mm
Normal file
295
plugins/mac-virtualcam/src/dal-plugin/Device.mm
Normal file
@ -0,0 +1,295 @@
|
||||
//
|
||||
// Device.mm
|
||||
// obs-mac-virtualcam
|
||||
//
|
||||
// Created by John Boiles on 4/10/20.
|
||||
//
|
||||
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// obs-mac-virtualcam is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#import "Device.h"
|
||||
|
||||
#import <CoreFoundation/CoreFoundation.h>
|
||||
#include <IOKit/audio/IOAudioTypes.h>
|
||||
|
||||
#import "PlugIn.h"
|
||||
#import "Logging.h"
|
||||
|
||||
@interface Device ()
|
||||
@property BOOL excludeNonDALAccess;
|
||||
@property pid_t masterPid;
|
||||
@end
|
||||
|
||||
@implementation Device
|
||||
|
||||
// Note that the DAL's API calls HasProperty before calling GetPropertyDataSize. This means that it can be assumed that address is valid for the property involved.
|
||||
- (UInt32)getPropertyDataSizeWithAddress:(CMIOObjectPropertyAddress)address
|
||||
qualifierDataSize:(UInt32)qualifierDataSize
|
||||
qualifierData:(nonnull const void *)qualifierData
|
||||
{
|
||||
|
||||
switch (address.mSelector) {
|
||||
case kCMIOObjectPropertyName:
|
||||
return sizeof(CFStringRef);
|
||||
case kCMIOObjectPropertyManufacturer:
|
||||
return sizeof(CFStringRef);
|
||||
case kCMIOObjectPropertyElementCategoryName:
|
||||
return sizeof(CFStringRef);
|
||||
case kCMIOObjectPropertyElementNumberName:
|
||||
return sizeof(CFStringRef);
|
||||
case kCMIODevicePropertyPlugIn:
|
||||
return sizeof(CMIOObjectID);
|
||||
case kCMIODevicePropertyDeviceUID:
|
||||
return sizeof(CFStringRef);
|
||||
case kCMIODevicePropertyModelUID:
|
||||
return sizeof(CFStringRef);
|
||||
case kCMIODevicePropertyTransportType:
|
||||
return sizeof(UInt32);
|
||||
case kCMIODevicePropertyDeviceIsAlive:
|
||||
return sizeof(UInt32);
|
||||
case kCMIODevicePropertyDeviceHasChanged:
|
||||
return sizeof(UInt32);
|
||||
case kCMIODevicePropertyDeviceIsRunning:
|
||||
return sizeof(UInt32);
|
||||
case kCMIODevicePropertyDeviceIsRunningSomewhere:
|
||||
return sizeof(UInt32);
|
||||
case kCMIODevicePropertyDeviceCanBeDefaultDevice:
|
||||
return sizeof(UInt32);
|
||||
case kCMIODevicePropertyHogMode:
|
||||
return sizeof(pid_t);
|
||||
case kCMIODevicePropertyLatency:
|
||||
return sizeof(UInt32);
|
||||
case kCMIODevicePropertyStreams:
|
||||
// Only one stream
|
||||
return sizeof(CMIOStreamID) * 1;
|
||||
case kCMIODevicePropertyStreamConfiguration:
|
||||
// Only one stream
|
||||
return sizeof(UInt32) + (sizeof(UInt32) * 1);
|
||||
case kCMIODevicePropertyExcludeNonDALAccess:
|
||||
return sizeof(UInt32);
|
||||
case kCMIODevicePropertyCanProcessAVCCommand:
|
||||
return sizeof(Boolean);
|
||||
case kCMIODevicePropertyCanProcessRS422Command:
|
||||
return sizeof(Boolean);
|
||||
case kCMIODevicePropertyLinkedCoreAudioDeviceUID:
|
||||
return sizeof(CFStringRef);
|
||||
case kCMIODevicePropertyDeviceMaster:
|
||||
return sizeof(pid_t);
|
||||
default:
|
||||
DLog(@"Device unhandled getPropertyDataSizeWithAddress for %@",
|
||||
[ObjectStore
|
||||
StringFromPropertySelector:address.mSelector]);
|
||||
};
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Note that the DAL's API calls HasProperty before calling GetPropertyData. This means that it can be assumed that address is valid for the property involved.
|
||||
- (void)getPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
|
||||
qualifierDataSize:(UInt32)qualifierDataSize
|
||||
qualifierData:(nonnull const void *)qualifierData
|
||||
dataSize:(UInt32)dataSize
|
||||
dataUsed:(nonnull UInt32 *)dataUsed
|
||||
data:(nonnull void *)data
|
||||
{
|
||||
|
||||
switch (address.mSelector) {
|
||||
case kCMIOObjectPropertyName:
|
||||
*static_cast<CFStringRef *>(data) = CFSTR("OBS Virtual Camera");
|
||||
*dataUsed = sizeof(CFStringRef);
|
||||
break;
|
||||
case kCMIOObjectPropertyManufacturer:
|
||||
*static_cast<CFStringRef *>(data) = CFSTR("John Boiles");
|
||||
*dataUsed = sizeof(CFStringRef);
|
||||
break;
|
||||
case kCMIOObjectPropertyElementCategoryName:
|
||||
*static_cast<CFStringRef *>(data) = CFSTR("Virtual Camera");
|
||||
*dataUsed = sizeof(CFStringRef);
|
||||
break;
|
||||
case kCMIOObjectPropertyElementNumberName:
|
||||
*static_cast<CFStringRef *>(data) = CFSTR("0001");
|
||||
*dataUsed = sizeof(CFStringRef);
|
||||
break;
|
||||
case kCMIODevicePropertyPlugIn:
|
||||
*static_cast<CMIOObjectID *>(data) = self.pluginId;
|
||||
*dataUsed = sizeof(CMIOObjectID);
|
||||
break;
|
||||
case kCMIODevicePropertyDeviceUID:
|
||||
*static_cast<CFStringRef *>(data) =
|
||||
CFSTR("obs-virtual-cam-device");
|
||||
*dataUsed = sizeof(CFStringRef);
|
||||
break;
|
||||
case kCMIODevicePropertyModelUID:
|
||||
*static_cast<CFStringRef *>(data) =
|
||||
CFSTR("obs-virtual-cam-model");
|
||||
*dataUsed = sizeof(CFStringRef);
|
||||
break;
|
||||
case kCMIODevicePropertyTransportType:
|
||||
*static_cast<UInt32 *>(data) =
|
||||
kIOAudioDeviceTransportTypeBuiltIn;
|
||||
*dataUsed = sizeof(UInt32);
|
||||
break;
|
||||
case kCMIODevicePropertyDeviceIsAlive:
|
||||
*static_cast<UInt32 *>(data) = 1;
|
||||
*dataUsed = sizeof(UInt32);
|
||||
break;
|
||||
case kCMIODevicePropertyDeviceHasChanged:
|
||||
*static_cast<UInt32 *>(data) = 0;
|
||||
*dataUsed = sizeof(UInt32);
|
||||
break;
|
||||
case kCMIODevicePropertyDeviceIsRunning:
|
||||
*static_cast<UInt32 *>(data) = 1;
|
||||
*dataUsed = sizeof(UInt32);
|
||||
break;
|
||||
case kCMIODevicePropertyDeviceIsRunningSomewhere:
|
||||
*static_cast<UInt32 *>(data) = 1;
|
||||
*dataUsed = sizeof(UInt32);
|
||||
break;
|
||||
case kCMIODevicePropertyDeviceCanBeDefaultDevice:
|
||||
*static_cast<UInt32 *>(data) = 1;
|
||||
*dataUsed = sizeof(UInt32);
|
||||
break;
|
||||
case kCMIODevicePropertyHogMode:
|
||||
*static_cast<pid_t *>(data) = -1;
|
||||
*dataUsed = sizeof(pid_t);
|
||||
break;
|
||||
case kCMIODevicePropertyLatency:
|
||||
*static_cast<UInt32 *>(data) = 0;
|
||||
*dataUsed = sizeof(UInt32);
|
||||
break;
|
||||
case kCMIODevicePropertyStreams:
|
||||
*static_cast<CMIOObjectID *>(data) = self.streamId;
|
||||
*dataUsed = sizeof(CMIOObjectID);
|
||||
break;
|
||||
case kCMIODevicePropertyStreamConfiguration:
|
||||
DLog(@"TODO kCMIODevicePropertyStreamConfiguration");
|
||||
break;
|
||||
case kCMIODevicePropertyExcludeNonDALAccess:
|
||||
*static_cast<UInt32 *>(data) = self.excludeNonDALAccess ? 1 : 0;
|
||||
*dataUsed = sizeof(UInt32);
|
||||
break;
|
||||
case kCMIODevicePropertyCanProcessAVCCommand:
|
||||
*static_cast<Boolean *>(data) = false;
|
||||
*dataUsed = sizeof(Boolean);
|
||||
break;
|
||||
case kCMIODevicePropertyCanProcessRS422Command:
|
||||
*static_cast<Boolean *>(data) = false;
|
||||
*dataUsed = sizeof(Boolean);
|
||||
break;
|
||||
case kCMIODevicePropertyDeviceMaster:
|
||||
*static_cast<pid_t *>(data) = self.masterPid;
|
||||
*dataUsed = sizeof(pid_t);
|
||||
break;
|
||||
default:
|
||||
DLog(@"Device unhandled getPropertyDataWithAddress for %@",
|
||||
[ObjectStore
|
||||
StringFromPropertySelector:address.mSelector]);
|
||||
*dataUsed = 0;
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
- (BOOL)hasPropertyWithAddress:(CMIOObjectPropertyAddress)address
|
||||
{
|
||||
switch (address.mSelector) {
|
||||
case kCMIOObjectPropertyName:
|
||||
case kCMIOObjectPropertyManufacturer:
|
||||
case kCMIOObjectPropertyElementCategoryName:
|
||||
case kCMIOObjectPropertyElementNumberName:
|
||||
case kCMIODevicePropertyPlugIn:
|
||||
case kCMIODevicePropertyDeviceUID:
|
||||
case kCMIODevicePropertyModelUID:
|
||||
case kCMIODevicePropertyTransportType:
|
||||
case kCMIODevicePropertyDeviceIsAlive:
|
||||
case kCMIODevicePropertyDeviceHasChanged:
|
||||
case kCMIODevicePropertyDeviceIsRunning:
|
||||
case kCMIODevicePropertyDeviceIsRunningSomewhere:
|
||||
case kCMIODevicePropertyDeviceCanBeDefaultDevice:
|
||||
case kCMIODevicePropertyHogMode:
|
||||
case kCMIODevicePropertyLatency:
|
||||
case kCMIODevicePropertyStreams:
|
||||
case kCMIODevicePropertyExcludeNonDALAccess:
|
||||
case kCMIODevicePropertyCanProcessAVCCommand:
|
||||
case kCMIODevicePropertyCanProcessRS422Command:
|
||||
case kCMIODevicePropertyDeviceMaster:
|
||||
return true;
|
||||
case kCMIODevicePropertyStreamConfiguration:
|
||||
case kCMIODevicePropertyLinkedCoreAudioDeviceUID:
|
||||
return false;
|
||||
default:
|
||||
DLog(@"Device unhandled hasPropertyWithAddress for %@",
|
||||
[ObjectStore
|
||||
StringFromPropertySelector:address.mSelector]);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
- (BOOL)isPropertySettableWithAddress:(CMIOObjectPropertyAddress)address
|
||||
{
|
||||
switch (address.mSelector) {
|
||||
case kCMIOObjectPropertyName:
|
||||
case kCMIOObjectPropertyManufacturer:
|
||||
case kCMIOObjectPropertyElementCategoryName:
|
||||
case kCMIOObjectPropertyElementNumberName:
|
||||
case kCMIODevicePropertyPlugIn:
|
||||
case kCMIODevicePropertyDeviceUID:
|
||||
case kCMIODevicePropertyModelUID:
|
||||
case kCMIODevicePropertyTransportType:
|
||||
case kCMIODevicePropertyDeviceIsAlive:
|
||||
case kCMIODevicePropertyDeviceHasChanged:
|
||||
case kCMIODevicePropertyDeviceIsRunning:
|
||||
case kCMIODevicePropertyDeviceIsRunningSomewhere:
|
||||
case kCMIODevicePropertyDeviceCanBeDefaultDevice:
|
||||
case kCMIODevicePropertyHogMode:
|
||||
case kCMIODevicePropertyLatency:
|
||||
case kCMIODevicePropertyStreams:
|
||||
case kCMIODevicePropertyStreamConfiguration:
|
||||
case kCMIODevicePropertyCanProcessAVCCommand:
|
||||
case kCMIODevicePropertyCanProcessRS422Command:
|
||||
case kCMIODevicePropertyLinkedCoreAudioDeviceUID:
|
||||
return false;
|
||||
case kCMIODevicePropertyExcludeNonDALAccess:
|
||||
case kCMIODevicePropertyDeviceMaster:
|
||||
return true;
|
||||
default:
|
||||
DLog(@"Device unhandled isPropertySettableWithAddress for %@",
|
||||
[ObjectStore
|
||||
StringFromPropertySelector:address.mSelector]);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
- (void)setPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
|
||||
qualifierDataSize:(UInt32)qualifierDataSize
|
||||
qualifierData:(nonnull const void *)qualifierData
|
||||
dataSize:(UInt32)dataSize
|
||||
data:(nonnull const void *)data
|
||||
{
|
||||
|
||||
switch (address.mSelector) {
|
||||
case kCMIODevicePropertyExcludeNonDALAccess:
|
||||
self.excludeNonDALAccess =
|
||||
(*static_cast<const UInt32 *>(data) != 0);
|
||||
break;
|
||||
case kCMIODevicePropertyDeviceMaster:
|
||||
self.masterPid = *static_cast<const pid_t *>(data);
|
||||
break;
|
||||
default:
|
||||
DLog(@"Device unhandled setPropertyDataWithAddress for %@",
|
||||
[ObjectStore
|
||||
StringFromPropertySelector:address.mSelector]);
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
40
plugins/mac-virtualcam/src/dal-plugin/Info.plist
Normal file
40
plugins/mac-virtualcam/src/dal-plugin/Info.plist
Normal file
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>obs-mac-virtualcam</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.obsproject.obs-mac-virtualcam.dal-plugin</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>OBS Virtual Camera</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CFPlugInFactories</key>
|
||||
<dict>
|
||||
<key>35FDFF29-BFCF-4644-AB77-B759DE932ABE</key>
|
||||
<string>PlugInMain</string>
|
||||
</dict>
|
||||
<key>CFPlugInTypes</key>
|
||||
<dict>
|
||||
<key>30010C1C-93BF-11D8-8B5B-000A95AF9C6A</key>
|
||||
<array>
|
||||
<string>35FDFF29-BFCF-4644-AB77-B759DE932ABE</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13</string>
|
||||
<key>CMIOHardwareAssistantServiceNames</key>
|
||||
<array>
|
||||
<string>com.obsproject.obs-mac-virtualcam.server</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
32
plugins/mac-virtualcam/src/dal-plugin/Logging.h
Normal file
32
plugins/mac-virtualcam/src/dal-plugin/Logging.h
Normal 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 */
|
33
plugins/mac-virtualcam/src/dal-plugin/MachClient.h
Normal file
33
plugins/mac-virtualcam/src/dal-plugin/MachClient.h
Normal file
@ -0,0 +1,33 @@
|
||||
//
|
||||
// MachClient.h
|
||||
// dal-plugin
|
||||
//
|
||||
// Created by John Boiles on 5/5/20.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol MachClientDelegate
|
||||
|
||||
- (void)receivedFrameWithSize:(NSSize)size
|
||||
timestamp:(uint64_t)timestamp
|
||||
fpsNumerator:(uint32_t)fpsNumerator
|
||||
fpsDenominator:(uint32_t)fpsDenominator
|
||||
frameData:(NSData *)frameData;
|
||||
- (void)receivedStop;
|
||||
|
||||
@end
|
||||
|
||||
@interface MachClient : NSObject
|
||||
|
||||
@property (nullable, weak) id<MachClientDelegate> delegate;
|
||||
|
||||
- (BOOL)isServerAvailable;
|
||||
|
||||
- (BOOL)connectToServer;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
140
plugins/mac-virtualcam/src/dal-plugin/MachClient.mm
Normal file
140
plugins/mac-virtualcam/src/dal-plugin/MachClient.mm
Normal file
@ -0,0 +1,140 @@
|
||||
//
|
||||
// MachClient.m
|
||||
// dal-plugin
|
||||
//
|
||||
// Created by John Boiles on 5/5/20.
|
||||
//
|
||||
|
||||
#import "MachClient.h"
|
||||
#import "MachProtocol.h"
|
||||
#import "Logging.h"
|
||||
|
||||
@interface MachClient () <NSPortDelegate> {
|
||||
NSPort *_receivePort;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation MachClient
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
DLogFunc(@"");
|
||||
_receivePort.delegate = nil;
|
||||
}
|
||||
|
||||
- (NSPort *)serverPort
|
||||
{
|
||||
// See note in MachServer.mm and don't judge me
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return [[NSMachBootstrapServer sharedInstance]
|
||||
portForName:@MACH_SERVICE_NAME];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
- (BOOL)isServerAvailable
|
||||
{
|
||||
return [self serverPort] != nil;
|
||||
}
|
||||
|
||||
- (NSPort *)receivePort
|
||||
{
|
||||
if (_receivePort == nil) {
|
||||
NSPort *receivePort = [NSMachPort port];
|
||||
_receivePort = receivePort;
|
||||
_receivePort.delegate = self;
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
dispatch_async(
|
||||
dispatch_get_global_queue(
|
||||
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
|
||||
^{
|
||||
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
|
||||
[runLoop addPort:receivePort
|
||||
forMode:NSDefaultRunLoopMode];
|
||||
// weakSelf should become nil when this object gets destroyed
|
||||
while (weakSelf) {
|
||||
[[NSRunLoop currentRunLoop]
|
||||
runUntilDate:
|
||||
[NSDate dateWithTimeIntervalSinceNow:
|
||||
0.1]];
|
||||
}
|
||||
DLog(@"Shutting down receive run loop");
|
||||
});
|
||||
DLog(@"Initialized mach port %d for receiving",
|
||||
((NSMachPort *)_receivePort).machPort);
|
||||
}
|
||||
return _receivePort;
|
||||
}
|
||||
|
||||
- (BOOL)connectToServer
|
||||
{
|
||||
DLogFunc(@"");
|
||||
|
||||
NSPort *sendPort = [self serverPort];
|
||||
if (sendPort == nil) {
|
||||
ELog(@"Unable to connect to server port");
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSPortMessage *message = [[NSPortMessage alloc]
|
||||
initWithSendPort:sendPort
|
||||
receivePort:self.receivePort
|
||||
components:nil];
|
||||
message.msgid = MachMsgIdConnect;
|
||||
|
||||
NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:5.0];
|
||||
if (![message sendBeforeDate:timeout]) {
|
||||
ELog(@"sendBeforeDate failed");
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)handlePortMessage:(NSPortMessage *)message
|
||||
{
|
||||
VLogFunc(@"");
|
||||
NSArray *components = message.components;
|
||||
switch (message.msgid) {
|
||||
case MachMsgIdConnect:
|
||||
DLog(@"Received connect response");
|
||||
break;
|
||||
case MachMsgIdFrame:
|
||||
VLog(@"Received frame message");
|
||||
if (components.count >= 6) {
|
||||
CGFloat width;
|
||||
[components[0] getBytes:&width length:sizeof(width)];
|
||||
CGFloat height;
|
||||
[components[1] getBytes:&height length:sizeof(height)];
|
||||
uint64_t timestamp;
|
||||
[components[2] getBytes:×tamp
|
||||
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
|
62
plugins/mac-virtualcam/src/dal-plugin/ObjectStore.h
Normal file
62
plugins/mac-virtualcam/src/dal-plugin/ObjectStore.h
Normal file
@ -0,0 +1,62 @@
|
||||
//
|
||||
// ObjectStore.h
|
||||
// obs-mac-virtualcam
|
||||
//
|
||||
// Created by John Boiles on 4/10/20.
|
||||
//
|
||||
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// obs-mac-virtualcam is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreMediaIO/CMIOHardwarePlugIn.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol CMIOObject
|
||||
|
||||
- (BOOL)hasPropertyWithAddress:(CMIOObjectPropertyAddress)address;
|
||||
- (BOOL)isPropertySettableWithAddress:(CMIOObjectPropertyAddress)address;
|
||||
- (UInt32)getPropertyDataSizeWithAddress:(CMIOObjectPropertyAddress)address
|
||||
qualifierDataSize:(UInt32)qualifierDataSize
|
||||
qualifierData:(const void *)qualifierData;
|
||||
- (void)getPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
|
||||
qualifierDataSize:(UInt32)qualifierDataSize
|
||||
qualifierData:(const void *)qualifierData
|
||||
dataSize:(UInt32)dataSize
|
||||
dataUsed:(UInt32 *)dataUsed
|
||||
data:(void *)data;
|
||||
- (void)setPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
|
||||
qualifierDataSize:(UInt32)qualifierDataSize
|
||||
qualifierData:(const void *)qualifierData
|
||||
dataSize:(UInt32)dataSize
|
||||
data:(const void *)data;
|
||||
|
||||
@end
|
||||
|
||||
@interface ObjectStore : NSObject
|
||||
|
||||
+ (ObjectStore *)SharedObjectStore;
|
||||
|
||||
+ (NSObject<CMIOObject> *)GetObjectWithId:(CMIOObjectID)objectId;
|
||||
|
||||
+ (NSString *)StringFromPropertySelector:(CMIOObjectPropertySelector)selector;
|
||||
|
||||
+ (BOOL)IsBridgedTypeForSelector:(CMIOObjectPropertySelector)selector;
|
||||
|
||||
- (NSObject<CMIOObject> *)getObject:(CMIOObjectID)objectID;
|
||||
|
||||
- (void)setObject:(id<CMIOObject>)object forObjectId:(CMIOObjectID)objectId;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
281
plugins/mac-virtualcam/src/dal-plugin/ObjectStore.mm
Normal file
281
plugins/mac-virtualcam/src/dal-plugin/ObjectStore.mm
Normal file
@ -0,0 +1,281 @@
|
||||
//
|
||||
// ObjectStore.mm
|
||||
// obs-mac-virtualcam
|
||||
//
|
||||
// Created by John Boiles on 4/10/20.
|
||||
//
|
||||
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// obs-mac-virtualcam is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#import "ObjectStore.h"
|
||||
|
||||
@interface ObjectStore ()
|
||||
@property NSMutableDictionary *objectMap;
|
||||
@end
|
||||
|
||||
@implementation ObjectStore
|
||||
|
||||
// 4-byte selectors to string for easy debugging
|
||||
+ (NSString *)StringFromPropertySelector:(CMIOObjectPropertySelector)selector
|
||||
{
|
||||
switch (selector) {
|
||||
case kCMIODevicePropertyPlugIn:
|
||||
return @"kCMIODevicePropertyPlugIn";
|
||||
case kCMIODevicePropertyDeviceUID:
|
||||
return @"kCMIODevicePropertyDeviceUID";
|
||||
case kCMIODevicePropertyModelUID:
|
||||
return @"kCMIODevicePropertyModelUID";
|
||||
case kCMIODevicePropertyTransportType:
|
||||
return @"kCMIODevicePropertyTransportType";
|
||||
case kCMIODevicePropertyDeviceIsAlive:
|
||||
return @"kCMIODevicePropertyDeviceIsAlive";
|
||||
case kCMIODevicePropertyDeviceHasChanged:
|
||||
return @"kCMIODevicePropertyDeviceHasChanged";
|
||||
case kCMIODevicePropertyDeviceIsRunning:
|
||||
return @"kCMIODevicePropertyDeviceIsRunning";
|
||||
case kCMIODevicePropertyDeviceIsRunningSomewhere:
|
||||
return @"kCMIODevicePropertyDeviceIsRunningSomewhere";
|
||||
case kCMIODevicePropertyDeviceCanBeDefaultDevice:
|
||||
return @"kCMIODevicePropertyDeviceCanBeDefaultDevice";
|
||||
case kCMIODevicePropertyHogMode:
|
||||
return @"kCMIODevicePropertyHogMode";
|
||||
case kCMIODevicePropertyLatency:
|
||||
return @"kCMIODevicePropertyLatency";
|
||||
case kCMIODevicePropertyStreams:
|
||||
return @"kCMIODevicePropertyStreams";
|
||||
case kCMIODevicePropertyStreamConfiguration:
|
||||
return @"kCMIODevicePropertyStreamConfiguration";
|
||||
case kCMIODevicePropertyDeviceMaster:
|
||||
return @"kCMIODevicePropertyDeviceMaster";
|
||||
case kCMIODevicePropertyExcludeNonDALAccess:
|
||||
return @"kCMIODevicePropertyExcludeNonDALAccess";
|
||||
case kCMIODevicePropertyClientSyncDiscontinuity:
|
||||
return @"kCMIODevicePropertyClientSyncDiscontinuity";
|
||||
case kCMIODevicePropertySMPTETimeCallback:
|
||||
return @"kCMIODevicePropertySMPTETimeCallback";
|
||||
case kCMIODevicePropertyCanProcessAVCCommand:
|
||||
return @"kCMIODevicePropertyCanProcessAVCCommand";
|
||||
case kCMIODevicePropertyAVCDeviceType:
|
||||
return @"kCMIODevicePropertyAVCDeviceType";
|
||||
case kCMIODevicePropertyAVCDeviceSignalMode:
|
||||
return @"kCMIODevicePropertyAVCDeviceSignalMode";
|
||||
case kCMIODevicePropertyCanProcessRS422Command:
|
||||
return @"kCMIODevicePropertyCanProcessRS422Command";
|
||||
case kCMIODevicePropertyLinkedCoreAudioDeviceUID:
|
||||
return @"kCMIODevicePropertyLinkedCoreAudioDeviceUID";
|
||||
case kCMIODevicePropertyVideoDigitizerComponents:
|
||||
return @"kCMIODevicePropertyVideoDigitizerComponents";
|
||||
case kCMIODevicePropertySuspendedByUser:
|
||||
return @"kCMIODevicePropertySuspendedByUser";
|
||||
case kCMIODevicePropertyLinkedAndSyncedCoreAudioDeviceUID:
|
||||
return @"kCMIODevicePropertyLinkedAndSyncedCoreAudioDeviceUID";
|
||||
case kCMIODevicePropertyIIDCInitialUnitSpace:
|
||||
return @"kCMIODevicePropertyIIDCInitialUnitSpace";
|
||||
case kCMIODevicePropertyIIDCCSRData:
|
||||
return @"kCMIODevicePropertyIIDCCSRData";
|
||||
case kCMIODevicePropertyCanSwitchFrameRatesWithoutFrameDrops:
|
||||
return @"kCMIODevicePropertyCanSwitchFrameRatesWithoutFrameDrops";
|
||||
case kCMIODevicePropertyLocation:
|
||||
return @"kCMIODevicePropertyLocation";
|
||||
case kCMIODevicePropertyDeviceHasStreamingError:
|
||||
return @"kCMIODevicePropertyDeviceHasStreamingError";
|
||||
case kCMIODevicePropertyScopeInput:
|
||||
return @"kCMIODevicePropertyScopeInput";
|
||||
case kCMIODevicePropertyScopeOutput:
|
||||
return @"kCMIODevicePropertyScopeOutput";
|
||||
case kCMIODevicePropertyScopePlayThrough:
|
||||
return @"kCMIODevicePropertyScopePlayThrough";
|
||||
case kCMIOObjectPropertyClass:
|
||||
return @"kCMIOObjectPropertyClass";
|
||||
case kCMIOObjectPropertyOwner:
|
||||
return @"kCMIOObjectPropertyOwner";
|
||||
case kCMIOObjectPropertyCreator:
|
||||
return @"kCMIOObjectPropertyCreator";
|
||||
case kCMIOObjectPropertyName:
|
||||
return @"kCMIOObjectPropertyName";
|
||||
case kCMIOObjectPropertyManufacturer:
|
||||
return @"kCMIOObjectPropertyManufacturer";
|
||||
case kCMIOObjectPropertyElementName:
|
||||
return @"kCMIOObjectPropertyElementName";
|
||||
case kCMIOObjectPropertyElementCategoryName:
|
||||
return @"kCMIOObjectPropertyElementCategoryName";
|
||||
case kCMIOObjectPropertyElementNumberName:
|
||||
return @"kCMIOObjectPropertyElementNumberName";
|
||||
case kCMIOObjectPropertyOwnedObjects:
|
||||
return @"kCMIOObjectPropertyOwnedObjects";
|
||||
case kCMIOObjectPropertyListenerAdded:
|
||||
return @"kCMIOObjectPropertyListenerAdded";
|
||||
case kCMIOObjectPropertyListenerRemoved:
|
||||
return @"kCMIOObjectPropertyListenerRemoved";
|
||||
case kCMIOStreamPropertyDirection:
|
||||
return @"kCMIOStreamPropertyDirection";
|
||||
case kCMIOStreamPropertyTerminalType:
|
||||
return @"kCMIOStreamPropertyTerminalType";
|
||||
case kCMIOStreamPropertyStartingChannel:
|
||||
return @"kCMIOStreamPropertyStartingChannel";
|
||||
// Same value as kCMIODevicePropertyLatency
|
||||
// case kCMIOStreamPropertyLatency:
|
||||
// return @"kCMIOStreamPropertyLatency";
|
||||
case kCMIOStreamPropertyFormatDescription:
|
||||
return @"kCMIOStreamPropertyFormatDescription";
|
||||
case kCMIOStreamPropertyFormatDescriptions:
|
||||
return @"kCMIOStreamPropertyFormatDescriptions";
|
||||
case kCMIOStreamPropertyStillImage:
|
||||
return @"kCMIOStreamPropertyStillImage";
|
||||
case kCMIOStreamPropertyStillImageFormatDescriptions:
|
||||
return @"kCMIOStreamPropertyStillImageFormatDescriptions";
|
||||
case kCMIOStreamPropertyFrameRate:
|
||||
return @"kCMIOStreamPropertyFrameRate";
|
||||
case kCMIOStreamPropertyMinimumFrameRate:
|
||||
return @"kCMIOStreamPropertyMinimumFrameRate";
|
||||
case kCMIOStreamPropertyFrameRates:
|
||||
return @"kCMIOStreamPropertyFrameRates";
|
||||
case kCMIOStreamPropertyFrameRateRanges:
|
||||
return @"kCMIOStreamPropertyFrameRateRanges";
|
||||
case kCMIOStreamPropertyNoDataTimeoutInMSec:
|
||||
return @"kCMIOStreamPropertyNoDataTimeoutInMSec";
|
||||
case kCMIOStreamPropertyDeviceSyncTimeoutInMSec:
|
||||
return @"kCMIOStreamPropertyDeviceSyncTimeoutInMSec";
|
||||
case kCMIOStreamPropertyNoDataEventCount:
|
||||
return @"kCMIOStreamPropertyNoDataEventCount";
|
||||
case kCMIOStreamPropertyOutputBufferUnderrunCount:
|
||||
return @"kCMIOStreamPropertyOutputBufferUnderrunCount";
|
||||
case kCMIOStreamPropertyOutputBufferRepeatCount:
|
||||
return @"kCMIOStreamPropertyOutputBufferRepeatCount";
|
||||
case kCMIOStreamPropertyOutputBufferQueueSize:
|
||||
return @"kCMIOStreamPropertyOutputBufferQueueSize";
|
||||
case kCMIOStreamPropertyOutputBuffersRequiredForStartup:
|
||||
return @"kCMIOStreamPropertyOutputBuffersRequiredForStartup";
|
||||
case kCMIOStreamPropertyOutputBuffersNeededForThrottledPlayback:
|
||||
return @"kCMIOStreamPropertyOutputBuffersNeededForThrottledPlayback";
|
||||
case kCMIOStreamPropertyFirstOutputPresentationTimeStamp:
|
||||
return @"kCMIOStreamPropertyFirstOutputPresentationTimeStamp";
|
||||
case kCMIOStreamPropertyEndOfData:
|
||||
return @"kCMIOStreamPropertyEndOfData";
|
||||
case kCMIOStreamPropertyClock:
|
||||
return @"kCMIOStreamPropertyClock";
|
||||
case kCMIOStreamPropertyCanProcessDeckCommand:
|
||||
return @"kCMIOStreamPropertyCanProcessDeckCommand";
|
||||
case kCMIOStreamPropertyDeck:
|
||||
return @"kCMIOStreamPropertyDeck";
|
||||
case kCMIOStreamPropertyDeckFrameNumber:
|
||||
return @"kCMIOStreamPropertyDeckFrameNumber";
|
||||
case kCMIOStreamPropertyDeckDropness:
|
||||
return @"kCMIOStreamPropertyDeckDropness";
|
||||
case kCMIOStreamPropertyDeckThreaded:
|
||||
return @"kCMIOStreamPropertyDeckThreaded";
|
||||
case kCMIOStreamPropertyDeckLocal:
|
||||
return @"kCMIOStreamPropertyDeckLocal";
|
||||
case kCMIOStreamPropertyDeckCueing:
|
||||
return @"kCMIOStreamPropertyDeckCueing";
|
||||
case kCMIOStreamPropertyInitialPresentationTimeStampForLinkedAndSyncedAudio:
|
||||
return @"kCMIOStreamPropertyInitialPresentationTimeStampForLinkedAndSyncedAudio";
|
||||
case kCMIOStreamPropertyScheduledOutputNotificationProc:
|
||||
return @"kCMIOStreamPropertyScheduledOutputNotificationProc";
|
||||
case kCMIOStreamPropertyPreferredFormatDescription:
|
||||
return @"kCMIOStreamPropertyPreferredFormatDescription";
|
||||
case kCMIOStreamPropertyPreferredFrameRate:
|
||||
return @"kCMIOStreamPropertyPreferredFrameRate";
|
||||
case kCMIOControlPropertyScope:
|
||||
return @"kCMIOControlPropertyScope";
|
||||
case kCMIOControlPropertyElement:
|
||||
return @"kCMIOControlPropertyElement";
|
||||
case kCMIOControlPropertyVariant:
|
||||
return @"kCMIOControlPropertyVariant";
|
||||
case kCMIOHardwarePropertyProcessIsMaster:
|
||||
return @"kCMIOHardwarePropertyProcessIsMaster";
|
||||
case kCMIOHardwarePropertyIsInitingOrExiting:
|
||||
return @"kCMIOHardwarePropertyIsInitingOrExiting";
|
||||
case kCMIOHardwarePropertyDevices:
|
||||
return @"kCMIOHardwarePropertyDevices";
|
||||
case kCMIOHardwarePropertyDefaultInputDevice:
|
||||
return @"kCMIOHardwarePropertyDefaultInputDevice";
|
||||
case kCMIOHardwarePropertyDefaultOutputDevice:
|
||||
return @"kCMIOHardwarePropertyDefaultOutputDevice";
|
||||
case kCMIOHardwarePropertyDeviceForUID:
|
||||
return @"kCMIOHardwarePropertyDeviceForUID";
|
||||
case kCMIOHardwarePropertySleepingIsAllowed:
|
||||
return @"kCMIOHardwarePropertySleepingIsAllowed";
|
||||
case kCMIOHardwarePropertyUnloadingIsAllowed:
|
||||
return @"kCMIOHardwarePropertyUnloadingIsAllowed";
|
||||
case kCMIOHardwarePropertyPlugInForBundleID:
|
||||
return @"kCMIOHardwarePropertyPlugInForBundleID";
|
||||
case kCMIOHardwarePropertyUserSessionIsActiveOrHeadless:
|
||||
return @"kCMIOHardwarePropertyUserSessionIsActiveOrHeadless";
|
||||
case kCMIOHardwarePropertySuspendedBySystem:
|
||||
return @"kCMIOHardwarePropertySuspendedBySystem";
|
||||
case kCMIOHardwarePropertyAllowScreenCaptureDevices:
|
||||
return @"kCMIOHardwarePropertyAllowScreenCaptureDevices";
|
||||
case kCMIOHardwarePropertyAllowWirelessScreenCaptureDevices:
|
||||
return @"kCMIOHardwarePropertyAllowWirelessScreenCaptureDevices";
|
||||
default:
|
||||
uint8_t *chars = (uint8_t *)&selector;
|
||||
return [NSString stringWithFormat:@"Unknown selector: %c%c%c%c",
|
||||
chars[0], chars[1], chars[2],
|
||||
chars[3]];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)IsBridgedTypeForSelector:(CMIOObjectPropertySelector)selector
|
||||
{
|
||||
switch (selector) {
|
||||
case kCMIOObjectPropertyName:
|
||||
case kCMIOObjectPropertyManufacturer:
|
||||
case kCMIOObjectPropertyElementName:
|
||||
case kCMIOObjectPropertyElementCategoryName:
|
||||
case kCMIOObjectPropertyElementNumberName:
|
||||
case kCMIODevicePropertyDeviceUID:
|
||||
case kCMIODevicePropertyModelUID:
|
||||
case kCMIOStreamPropertyFormatDescriptions:
|
||||
case kCMIOStreamPropertyFormatDescription:
|
||||
case kCMIOStreamPropertyClock:
|
||||
return YES;
|
||||
default:
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
+ (ObjectStore *)SharedObjectStore
|
||||
{
|
||||
static ObjectStore *sObjectStore = nil;
|
||||
static dispatch_once_t sOnceToken;
|
||||
dispatch_once(&sOnceToken, ^{
|
||||
sObjectStore = [[self alloc] init];
|
||||
});
|
||||
return sObjectStore;
|
||||
}
|
||||
|
||||
+ (NSObject<CMIOObject> *)GetObjectWithId:(CMIOObjectID)objectId
|
||||
{
|
||||
return [[ObjectStore SharedObjectStore] getObject:objectId];
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
self.objectMap = [[NSMutableDictionary alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSObject<CMIOObject> *)getObject:(CMIOObjectID)objectID
|
||||
{
|
||||
return self.objectMap[@(objectID)];
|
||||
}
|
||||
|
||||
- (void)setObject:(id<CMIOObject>)object forObjectId:(CMIOObjectID)objectId
|
||||
{
|
||||
self.objectMap[@(objectId)] = object;
|
||||
}
|
||||
|
||||
@end
|
51
plugins/mac-virtualcam/src/dal-plugin/PlugIn.h
Normal file
51
plugins/mac-virtualcam/src/dal-plugin/PlugIn.h
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// PlugIn.h
|
||||
// obs-mac-virtualcam
|
||||
//
|
||||
// Created by John Boiles on 4/9/20.
|
||||
//
|
||||
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// obs-mac-virtualcam is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreMediaIO/CMIOHardwarePlugIn.h>
|
||||
|
||||
#import "ObjectStore.h"
|
||||
#import "MachClient.h"
|
||||
#import "Stream.h"
|
||||
|
||||
#define kTestCardWidthKey @"obs-mac-virtualcam-test-card-width"
|
||||
#define kTestCardHeightKey @"obs-mac-virtualcam-test-card-height"
|
||||
#define kTestCardFPSKey @"obs-mac-virtualcam-test-card-fps"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface PlugIn : NSObject <CMIOObject>
|
||||
|
||||
@property CMIOObjectID objectId;
|
||||
@property (readonly) MachClient *machClient;
|
||||
@property Stream *stream;
|
||||
|
||||
+ (PlugIn *)SharedPlugIn;
|
||||
|
||||
- (void)initialize;
|
||||
|
||||
- (void)teardown;
|
||||
|
||||
- (void)startStream;
|
||||
|
||||
- (void)stopStream;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
255
plugins/mac-virtualcam/src/dal-plugin/PlugIn.mm
Normal file
255
plugins/mac-virtualcam/src/dal-plugin/PlugIn.mm
Normal file
@ -0,0 +1,255 @@
|
||||
//
|
||||
// PlugIn.mm
|
||||
// obs-mac-virtualcam
|
||||
//
|
||||
// Created by John Boiles on 4/9/20.
|
||||
//
|
||||
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// obs-mac-virtualcam is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#import "PlugIn.h"
|
||||
|
||||
#import <CoreMediaIO/CMIOHardwarePlugin.h>
|
||||
|
||||
#import "Logging.h"
|
||||
|
||||
typedef enum {
|
||||
PlugInStateNotStarted = 0,
|
||||
PlugInStateWaitingForServer,
|
||||
PlugInStateReceivingFrames,
|
||||
} PlugInState;
|
||||
|
||||
@interface PlugIn () <MachClientDelegate> {
|
||||
//! Serial queue for all state changes that need to be concerned with thread safety
|
||||
dispatch_queue_t _stateQueue;
|
||||
|
||||
//! Repeated timer for driving the mach server re-connection
|
||||
dispatch_source_t _machConnectTimer;
|
||||
|
||||
//! Timeout timer when we haven't received frames for 5s
|
||||
dispatch_source_t _timeoutTimer;
|
||||
}
|
||||
@property PlugInState state;
|
||||
@property MachClient *machClient;
|
||||
|
||||
@end
|
||||
|
||||
@implementation PlugIn
|
||||
|
||||
+ (PlugIn *)SharedPlugIn
|
||||
{
|
||||
static PlugIn *sPlugIn = nil;
|
||||
static dispatch_once_t sOnceToken;
|
||||
dispatch_once(&sOnceToken, ^{
|
||||
sPlugIn = [[self alloc] init];
|
||||
});
|
||||
return sPlugIn;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_stateQueue = dispatch_queue_create(
|
||||
"com.obsproject.obs-mac-virtualcam.dal.state",
|
||||
DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_timeoutTimer = dispatch_source_create(
|
||||
DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _stateQueue);
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
dispatch_source_set_event_handler(_timeoutTimer, ^{
|
||||
if (weakSelf.state == PlugInStateReceivingFrames) {
|
||||
DLog(@"No frames received for 5s, restarting connection");
|
||||
[self stopStream];
|
||||
[self startStream];
|
||||
}
|
||||
});
|
||||
|
||||
_machClient = [[MachClient alloc] init];
|
||||
_machClient.delegate = self;
|
||||
|
||||
_machConnectTimer = dispatch_source_create(
|
||||
DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _stateQueue);
|
||||
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 0);
|
||||
uint64_t intervalTime = (int64_t)(1 * NSEC_PER_SEC);
|
||||
dispatch_source_set_timer(_machConnectTimer, startTime,
|
||||
intervalTime, 0);
|
||||
dispatch_source_set_event_handler(_machConnectTimer, ^{
|
||||
if (![[weakSelf machClient] isServerAvailable]) {
|
||||
DLog(@"Server is not available");
|
||||
} else if (weakSelf.state ==
|
||||
PlugInStateWaitingForServer) {
|
||||
DLog(@"Attempting connection");
|
||||
[[weakSelf machClient] connectToServer];
|
||||
}
|
||||
});
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)startStream
|
||||
{
|
||||
DLogFunc(@"");
|
||||
dispatch_async(_stateQueue, ^{
|
||||
if (_state == PlugInStateNotStarted) {
|
||||
dispatch_resume(_machConnectTimer);
|
||||
[self.stream startServingDefaultFrames];
|
||||
_state = PlugInStateWaitingForServer;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)stopStream
|
||||
{
|
||||
DLogFunc(@"");
|
||||
dispatch_async(_stateQueue, ^{
|
||||
if (_state == PlugInStateWaitingForServer) {
|
||||
dispatch_suspend(_machConnectTimer);
|
||||
[self.stream stopServingDefaultFrames];
|
||||
} else if (_state == PlugInStateReceivingFrames) {
|
||||
// TODO: Disconnect from the mach server?
|
||||
dispatch_suspend(_timeoutTimer);
|
||||
}
|
||||
_state = PlugInStateNotStarted;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)initialize
|
||||
{
|
||||
}
|
||||
|
||||
- (void)teardown
|
||||
{
|
||||
}
|
||||
|
||||
#pragma mark - CMIOObject
|
||||
|
||||
- (BOOL)hasPropertyWithAddress:(CMIOObjectPropertyAddress)address
|
||||
{
|
||||
switch (address.mSelector) {
|
||||
case kCMIOObjectPropertyName:
|
||||
return true;
|
||||
default:
|
||||
DLog(@"PlugIn unhandled hasPropertyWithAddress for %@",
|
||||
[ObjectStore
|
||||
StringFromPropertySelector:address.mSelector]);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
- (BOOL)isPropertySettableWithAddress:(CMIOObjectPropertyAddress)address
|
||||
{
|
||||
switch (address.mSelector) {
|
||||
case kCMIOObjectPropertyName:
|
||||
return false;
|
||||
default:
|
||||
DLog(@"PlugIn unhandled isPropertySettableWithAddress for %@",
|
||||
[ObjectStore
|
||||
StringFromPropertySelector:address.mSelector]);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
- (UInt32)getPropertyDataSizeWithAddress:(CMIOObjectPropertyAddress)address
|
||||
qualifierDataSize:(UInt32)qualifierDataSize
|
||||
qualifierData:(const void *)qualifierData
|
||||
{
|
||||
switch (address.mSelector) {
|
||||
case kCMIOObjectPropertyName:
|
||||
return sizeof(CFStringRef);
|
||||
default:
|
||||
DLog(@"PlugIn unhandled getPropertyDataSizeWithAddress for %@",
|
||||
[ObjectStore
|
||||
StringFromPropertySelector:address.mSelector]);
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
- (void)getPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
|
||||
qualifierDataSize:(UInt32)qualifierDataSize
|
||||
qualifierData:(nonnull const void *)qualifierData
|
||||
dataSize:(UInt32)dataSize
|
||||
dataUsed:(nonnull UInt32 *)dataUsed
|
||||
data:(nonnull void *)data
|
||||
{
|
||||
switch (address.mSelector) {
|
||||
case kCMIOObjectPropertyName:
|
||||
*static_cast<CFStringRef *>(data) =
|
||||
CFSTR("OBS Virtual Camera Plugin");
|
||||
*dataUsed = sizeof(CFStringRef);
|
||||
return;
|
||||
default:
|
||||
DLog(@"PlugIn unhandled getPropertyDataWithAddress for %@",
|
||||
[ObjectStore
|
||||
StringFromPropertySelector:address.mSelector]);
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
- (void)setPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
|
||||
qualifierDataSize:(UInt32)qualifierDataSize
|
||||
qualifierData:(nonnull const void *)qualifierData
|
||||
dataSize:(UInt32)dataSize
|
||||
data:(nonnull const void *)data
|
||||
{
|
||||
DLog(@"PlugIn unhandled setPropertyDataWithAddress for %@",
|
||||
[ObjectStore StringFromPropertySelector:address.mSelector]);
|
||||
}
|
||||
|
||||
#pragma mark - MachClientDelegate
|
||||
|
||||
- (void)receivedFrameWithSize:(NSSize)size
|
||||
timestamp:(uint64_t)timestamp
|
||||
fpsNumerator:(uint32_t)fpsNumerator
|
||||
fpsDenominator:(uint32_t)fpsDenominator
|
||||
frameData:(NSData *)frameData
|
||||
{
|
||||
dispatch_sync(_stateQueue, ^{
|
||||
if (_state == PlugInStateWaitingForServer) {
|
||||
NSUserDefaults *defaults =
|
||||
[NSUserDefaults standardUserDefaults];
|
||||
[defaults setInteger:size.width
|
||||
forKey:kTestCardWidthKey];
|
||||
[defaults setInteger:size.height
|
||||
forKey:kTestCardHeightKey];
|
||||
[defaults setDouble:(double)fpsNumerator /
|
||||
(double)fpsDenominator
|
||||
forKey:kTestCardFPSKey];
|
||||
|
||||
dispatch_suspend(_machConnectTimer);
|
||||
[self.stream stopServingDefaultFrames];
|
||||
dispatch_resume(_timeoutTimer);
|
||||
_state = PlugInStateReceivingFrames;
|
||||
}
|
||||
});
|
||||
|
||||
// Add 5 more seconds onto the timeout timer
|
||||
dispatch_source_set_timer(
|
||||
_timeoutTimer,
|
||||
dispatch_time(DISPATCH_TIME_NOW, 5.0 * NSEC_PER_SEC),
|
||||
5.0 * NSEC_PER_SEC, (1ull * NSEC_PER_SEC) / 10);
|
||||
|
||||
[self.stream queueFrameWithSize:size
|
||||
timestamp:timestamp
|
||||
fpsNumerator:fpsNumerator
|
||||
fpsDenominator:fpsDenominator
|
||||
frameData:frameData];
|
||||
}
|
||||
|
||||
- (void)receivedStop
|
||||
{
|
||||
DLogFunc(@"Restarting connection");
|
||||
[self stopStream];
|
||||
[self startStream];
|
||||
}
|
||||
|
||||
@end
|
23
plugins/mac-virtualcam/src/dal-plugin/PlugInInterface.h
Normal file
23
plugins/mac-virtualcam/src/dal-plugin/PlugInInterface.h
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// PlugInInterface.h
|
||||
// obs-mac-virtualcam
|
||||
//
|
||||
// Created by John Boiles on 4/9/20.
|
||||
//
|
||||
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// obs-mac-virtualcam is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#import <CoreMediaIO/CMIOHardwarePlugIn.h>
|
||||
|
||||
// The static singleton of the plugin interface
|
||||
CMIOHardwarePlugInRef PlugInRef();
|
444
plugins/mac-virtualcam/src/dal-plugin/PlugInInterface.mm
Normal file
444
plugins/mac-virtualcam/src/dal-plugin/PlugInInterface.mm
Normal file
@ -0,0 +1,444 @@
|
||||
//
|
||||
// PlugInInterface.mm
|
||||
// obs-mac-virtualcam
|
||||
//
|
||||
// This file implements the CMIO DAL plugin interface
|
||||
//
|
||||
// Created by John Boiles on 4/9/20.
|
||||
//
|
||||
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// obs-mac-virtualcam is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#import "PlugInInterface.h"
|
||||
|
||||
#import <CoreFoundation/CFUUID.h>
|
||||
|
||||
#import "PlugIn.h"
|
||||
#import "Device.h"
|
||||
#import "Stream.h"
|
||||
#import "Logging.h"
|
||||
|
||||
#pragma mark Plug-In Operations
|
||||
|
||||
static UInt32 sRefCount = 0;
|
||||
|
||||
ULONG HardwarePlugIn_AddRef(CMIOHardwarePlugInRef self)
|
||||
{
|
||||
sRefCount += 1;
|
||||
DLogFunc(@"sRefCount now = %d", sRefCount);
|
||||
return sRefCount;
|
||||
}
|
||||
|
||||
ULONG HardwarePlugIn_Release(CMIOHardwarePlugInRef self)
|
||||
{
|
||||
sRefCount -= 1;
|
||||
DLogFunc(@"sRefCount now = %d", sRefCount);
|
||||
return sRefCount;
|
||||
}
|
||||
|
||||
HRESULT HardwarePlugIn_QueryInterface(CMIOHardwarePlugInRef self, REFIID uuid,
|
||||
LPVOID *interface)
|
||||
{
|
||||
DLogFunc(@"");
|
||||
|
||||
if (!interface) {
|
||||
DLogFunc(@"Received an empty interface");
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
// Set the returned interface to NULL in case the UUIDs don't match
|
||||
*interface = NULL;
|
||||
|
||||
// Create a CoreFoundation UUIDRef for the requested interface.
|
||||
CFUUIDRef cfUuid = CFUUIDCreateFromUUIDBytes(kCFAllocatorDefault, uuid);
|
||||
CFStringRef uuidString = CFUUIDCreateString(NULL, cfUuid);
|
||||
CFStringRef hardwarePluginUuid =
|
||||
CFUUIDCreateString(NULL, kCMIOHardwarePlugInInterfaceID);
|
||||
|
||||
if (CFEqual(uuidString, hardwarePluginUuid)) {
|
||||
// Return the interface;
|
||||
sRefCount += 1;
|
||||
*interface = PlugInRef();
|
||||
return kCMIOHardwareNoError;
|
||||
} else {
|
||||
DLogFunc(@"ERR Queried for some weird UUID %@", uuidString);
|
||||
}
|
||||
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
// I think this is deprecated, seems that HardwarePlugIn_InitializeWithObjectID gets called instead
|
||||
OSStatus HardwarePlugIn_Initialize(CMIOHardwarePlugInRef self)
|
||||
{
|
||||
DLogFunc(@"ERR self=%p", self);
|
||||
return kCMIOHardwareUnspecifiedError;
|
||||
}
|
||||
|
||||
OSStatus HardwarePlugIn_InitializeWithObjectID(CMIOHardwarePlugInRef self,
|
||||
CMIOObjectID objectID)
|
||||
{
|
||||
DLogFunc(@"self=%p", self);
|
||||
|
||||
OSStatus error = kCMIOHardwareNoError;
|
||||
|
||||
PlugIn *plugIn = [PlugIn SharedPlugIn];
|
||||
plugIn.objectId = objectID;
|
||||
[[ObjectStore SharedObjectStore] setObject:plugIn forObjectId:objectID];
|
||||
|
||||
Device *device = [[Device alloc] init];
|
||||
CMIOObjectID deviceId;
|
||||
error = CMIOObjectCreate(PlugInRef(), kCMIOObjectSystemObject,
|
||||
kCMIODeviceClassID, &deviceId);
|
||||
if (error != noErr) {
|
||||
DLog(@"CMIOObjectCreate Error %d", error);
|
||||
return error;
|
||||
}
|
||||
device.objectId = deviceId;
|
||||
device.pluginId = objectID;
|
||||
[[ObjectStore SharedObjectStore] setObject:device forObjectId:deviceId];
|
||||
|
||||
Stream *stream = [[Stream alloc] init];
|
||||
CMIOObjectID streamId;
|
||||
error = CMIOObjectCreate(PlugInRef(), deviceId, kCMIOStreamClassID,
|
||||
&streamId);
|
||||
if (error != noErr) {
|
||||
DLog(@"CMIOObjectCreate Error %d", error);
|
||||
return error;
|
||||
}
|
||||
stream.objectId = streamId;
|
||||
[[ObjectStore SharedObjectStore] setObject:stream forObjectId:streamId];
|
||||
device.streamId = streamId;
|
||||
plugIn.stream = stream;
|
||||
|
||||
// Tell the system about the Device
|
||||
error = CMIOObjectsPublishedAndDied(
|
||||
PlugInRef(), kCMIOObjectSystemObject, 1, &deviceId, 0, 0);
|
||||
if (error != kCMIOHardwareNoError) {
|
||||
DLog(@"CMIOObjectsPublishedAndDied plugin/device Error %d",
|
||||
error);
|
||||
return error;
|
||||
}
|
||||
|
||||
// Tell the system about the Stream
|
||||
error = CMIOObjectsPublishedAndDied(PlugInRef(), deviceId, 1, &streamId,
|
||||
0, 0);
|
||||
if (error != kCMIOHardwareNoError) {
|
||||
DLog(@"CMIOObjectsPublishedAndDied device/stream Error %d",
|
||||
error);
|
||||
return error;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
OSStatus HardwarePlugIn_Teardown(CMIOHardwarePlugInRef self)
|
||||
{
|
||||
DLogFunc(@"self=%p", self);
|
||||
|
||||
OSStatus error = kCMIOHardwareNoError;
|
||||
|
||||
PlugIn *plugIn = [PlugIn SharedPlugIn];
|
||||
[plugIn teardown];
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
#pragma mark CMIOObject Operations
|
||||
|
||||
void HardwarePlugIn_ObjectShow(CMIOHardwarePlugInRef self,
|
||||
CMIOObjectID objectID)
|
||||
{
|
||||
DLogFunc(@"self=%p", self);
|
||||
}
|
||||
|
||||
Boolean
|
||||
HardwarePlugIn_ObjectHasProperty(CMIOHardwarePlugInRef self,
|
||||
CMIOObjectID objectID,
|
||||
const CMIOObjectPropertyAddress *address)
|
||||
{
|
||||
|
||||
NSObject<CMIOObject> *object = [ObjectStore GetObjectWithId:objectID];
|
||||
|
||||
if (object == nil) {
|
||||
DLogFunc(@"ERR nil object");
|
||||
return false;
|
||||
}
|
||||
|
||||
Boolean answer = [object hasPropertyWithAddress:*address];
|
||||
|
||||
// Disabling Noisy logs
|
||||
// DLogFunc(@"%@(%d) %@ self=%p hasProperty=%d", NSStringFromClass([object class]), objectID, [ObjectStore StringFromPropertySelector:address->mSelector], self, answer);
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
OSStatus HardwarePlugIn_ObjectIsPropertySettable(
|
||||
CMIOHardwarePlugInRef self, CMIOObjectID objectID,
|
||||
const CMIOObjectPropertyAddress *address, Boolean *isSettable)
|
||||
{
|
||||
|
||||
NSObject<CMIOObject> *object = [ObjectStore GetObjectWithId:objectID];
|
||||
|
||||
if (object == nil) {
|
||||
DLogFunc(@"ERR nil object");
|
||||
return kCMIOHardwareBadObjectError;
|
||||
}
|
||||
|
||||
*isSettable = [object isPropertySettableWithAddress:*address];
|
||||
|
||||
DLogFunc(@"%@(%d) %@ self=%p settable=%d",
|
||||
NSStringFromClass([object class]), objectID,
|
||||
[ObjectStore StringFromPropertySelector:address->mSelector],
|
||||
self, *isSettable);
|
||||
|
||||
return kCMIOHardwareNoError;
|
||||
}
|
||||
|
||||
OSStatus HardwarePlugIn_ObjectGetPropertyDataSize(
|
||||
CMIOHardwarePlugInRef self, CMIOObjectID objectID,
|
||||
const CMIOObjectPropertyAddress *address, UInt32 qualifierDataSize,
|
||||
const void *qualifierData, UInt32 *dataSize)
|
||||
{
|
||||
|
||||
NSObject<CMIOObject> *object = [ObjectStore GetObjectWithId:objectID];
|
||||
|
||||
if (object == nil) {
|
||||
DLogFunc(@"ERR nil object");
|
||||
return kCMIOHardwareBadObjectError;
|
||||
}
|
||||
|
||||
*dataSize = [object getPropertyDataSizeWithAddress:*address
|
||||
qualifierDataSize:qualifierDataSize
|
||||
qualifierData:qualifierData];
|
||||
|
||||
// Disabling Noisy logs
|
||||
// DLogFunc(@"%@(%d) %@ self=%p size=%d", NSStringFromClass([object class]), objectID, [ObjectStore StringFromPropertySelector:address->mSelector], self, *dataSize);
|
||||
|
||||
return kCMIOHardwareNoError;
|
||||
}
|
||||
|
||||
OSStatus HardwarePlugIn_ObjectGetPropertyData(
|
||||
CMIOHardwarePlugInRef self, CMIOObjectID objectID,
|
||||
const CMIOObjectPropertyAddress *address, UInt32 qualifierDataSize,
|
||||
const void *qualifierData, UInt32 dataSize, UInt32 *dataUsed,
|
||||
void *data)
|
||||
{
|
||||
|
||||
NSObject<CMIOObject> *object = [ObjectStore GetObjectWithId:objectID];
|
||||
|
||||
if (object == nil) {
|
||||
DLogFunc(@"ERR nil object");
|
||||
return kCMIOHardwareBadObjectError;
|
||||
}
|
||||
|
||||
[object getPropertyDataWithAddress:*address
|
||||
qualifierDataSize:qualifierDataSize
|
||||
qualifierData:qualifierData
|
||||
dataSize:dataSize
|
||||
dataUsed:dataUsed
|
||||
data:data];
|
||||
|
||||
// Disabling Noisy logs
|
||||
// if ([ObjectStore IsBridgedTypeForSelector:address->mSelector]) {
|
||||
// id dataObj = (__bridge NSObject *)*static_cast<CFTypeRef*>(data);
|
||||
// DLogFunc(@"%@(%d) %@ self=%p data(id)=%@", NSStringFromClass([object class]), objectID, [ObjectStore StringFromPropertySelector:address->mSelector], self, dataObj);
|
||||
// } else {
|
||||
// UInt32 *dataInt = (UInt32 *)data;
|
||||
// DLogFunc(@"%@(%d) %@ self=%p data(int)=%d", NSStringFromClass([object class]), objectID, [ObjectStore StringFromPropertySelector:address->mSelector], self, *dataInt);
|
||||
// }
|
||||
|
||||
return kCMIOHardwareNoError;
|
||||
}
|
||||
|
||||
OSStatus HardwarePlugIn_ObjectSetPropertyData(
|
||||
CMIOHardwarePlugInRef self, CMIOObjectID objectID,
|
||||
const CMIOObjectPropertyAddress *address, UInt32 qualifierDataSize,
|
||||
const void *qualifierData, UInt32 dataSize, const void *data)
|
||||
{
|
||||
|
||||
NSObject<CMIOObject> *object = [ObjectStore GetObjectWithId:objectID];
|
||||
|
||||
if (object == nil) {
|
||||
DLogFunc(@"ERR nil object");
|
||||
return kCMIOHardwareBadObjectError;
|
||||
}
|
||||
|
||||
UInt32 *dataInt = (UInt32 *)data;
|
||||
DLogFunc(@"%@(%d) %@ self=%p data(int)=%d",
|
||||
NSStringFromClass([object class]), objectID,
|
||||
[ObjectStore StringFromPropertySelector:address->mSelector],
|
||||
self, *dataInt);
|
||||
|
||||
[object setPropertyDataWithAddress:*address
|
||||
qualifierDataSize:qualifierDataSize
|
||||
qualifierData:qualifierData
|
||||
dataSize:dataSize
|
||||
data:data];
|
||||
|
||||
return kCMIOHardwareNoError;
|
||||
}
|
||||
|
||||
#pragma mark CMIOStream Operations
|
||||
OSStatus HardwarePlugIn_StreamCopyBufferQueue(
|
||||
CMIOHardwarePlugInRef self, CMIOStreamID streamID,
|
||||
CMIODeviceStreamQueueAlteredProc queueAlteredProc,
|
||||
void *queueAlteredRefCon, CMSimpleQueueRef *queue)
|
||||
{
|
||||
|
||||
Stream *stream = (Stream *)[ObjectStore GetObjectWithId:streamID];
|
||||
|
||||
if (stream == nil) {
|
||||
DLogFunc(@"ERR nil object");
|
||||
return kCMIOHardwareBadObjectError;
|
||||
}
|
||||
|
||||
*queue = [stream copyBufferQueueWithAlteredProc:queueAlteredProc
|
||||
alteredRefCon:queueAlteredRefCon];
|
||||
|
||||
DLogFunc(@"%@ (id=%d) self=%p queue=%@", stream, streamID, self,
|
||||
(__bridge NSObject *)*queue);
|
||||
|
||||
return kCMIOHardwareNoError;
|
||||
}
|
||||
|
||||
#pragma mark CMIODevice Operations
|
||||
OSStatus HardwarePlugIn_DeviceStartStream(CMIOHardwarePlugInRef self,
|
||||
CMIODeviceID deviceID,
|
||||
CMIOStreamID streamID)
|
||||
{
|
||||
DLogFunc(@"self=%p device=%d stream=%d", self, deviceID, streamID);
|
||||
|
||||
Stream *stream = (Stream *)[ObjectStore GetObjectWithId:streamID];
|
||||
|
||||
if (stream == nil) {
|
||||
DLogFunc(@"ERR nil object");
|
||||
return kCMIOHardwareBadObjectError;
|
||||
}
|
||||
|
||||
[[PlugIn SharedPlugIn] startStream];
|
||||
|
||||
return kCMIOHardwareNoError;
|
||||
}
|
||||
|
||||
OSStatus HardwarePlugIn_DeviceSuspend(CMIOHardwarePlugInRef self,
|
||||
CMIODeviceID deviceID)
|
||||
{
|
||||
DLogFunc(@"self=%p", self);
|
||||
return kCMIOHardwareNoError;
|
||||
}
|
||||
|
||||
OSStatus HardwarePlugIn_DeviceResume(CMIOHardwarePlugInRef self,
|
||||
CMIODeviceID deviceID)
|
||||
{
|
||||
DLogFunc(@"self=%p", self);
|
||||
return kCMIOHardwareNoError;
|
||||
}
|
||||
|
||||
OSStatus HardwarePlugIn_DeviceStopStream(CMIOHardwarePlugInRef self,
|
||||
CMIODeviceID deviceID,
|
||||
CMIOStreamID streamID)
|
||||
{
|
||||
DLogFunc(@"self=%p device=%d stream=%d", self, deviceID, streamID);
|
||||
|
||||
Stream *stream = (Stream *)[ObjectStore GetObjectWithId:streamID];
|
||||
|
||||
if (stream == nil) {
|
||||
DLogFunc(@"ERR nil object");
|
||||
return kCMIOHardwareBadObjectError;
|
||||
}
|
||||
|
||||
[[PlugIn SharedPlugIn] stopStream];
|
||||
|
||||
return kCMIOHardwareNoError;
|
||||
}
|
||||
|
||||
OSStatus
|
||||
HardwarePlugIn_DeviceProcessAVCCommand(CMIOHardwarePlugInRef self,
|
||||
CMIODeviceID deviceID,
|
||||
CMIODeviceAVCCommand *ioAVCCommand)
|
||||
{
|
||||
DLogFunc(@"self=%p", self);
|
||||
return kCMIOHardwareNoError;
|
||||
}
|
||||
|
||||
OSStatus
|
||||
HardwarePlugIn_DeviceProcessRS422Command(CMIOHardwarePlugInRef self,
|
||||
CMIODeviceID deviceID,
|
||||
CMIODeviceRS422Command *ioRS422Command)
|
||||
{
|
||||
DLogFunc(@"self=%p", self);
|
||||
return kCMIOHardwareNoError;
|
||||
}
|
||||
|
||||
OSStatus HardwarePlugIn_StreamDeckPlay(CMIOHardwarePlugInRef self,
|
||||
CMIOStreamID streamID)
|
||||
{
|
||||
DLogFunc(@"self=%p", self);
|
||||
return kCMIOHardwareIllegalOperationError;
|
||||
}
|
||||
|
||||
OSStatus HardwarePlugIn_StreamDeckStop(CMIOHardwarePlugInRef self,
|
||||
CMIOStreamID streamID)
|
||||
{
|
||||
DLogFunc(@"self=%p", self);
|
||||
return kCMIOHardwareIllegalOperationError;
|
||||
}
|
||||
|
||||
OSStatus HardwarePlugIn_StreamDeckJog(CMIOHardwarePlugInRef self,
|
||||
CMIOStreamID streamID, SInt32 speed)
|
||||
{
|
||||
DLogFunc(@"self=%p", self);
|
||||
return kCMIOHardwareIllegalOperationError;
|
||||
}
|
||||
|
||||
OSStatus HardwarePlugIn_StreamDeckCueTo(CMIOHardwarePlugInRef self,
|
||||
CMIOStreamID streamID,
|
||||
Float64 requestedTimecode,
|
||||
Boolean playOnCue)
|
||||
{
|
||||
DLogFunc(@"self=%p", self);
|
||||
return kCMIOHardwareIllegalOperationError;
|
||||
}
|
||||
|
||||
static CMIOHardwarePlugInInterface sInterface = {
|
||||
// Padding for COM
|
||||
NULL,
|
||||
|
||||
// IUnknown Routines
|
||||
(HRESULT (*)(void *, CFUUIDBytes,
|
||||
void **))HardwarePlugIn_QueryInterface,
|
||||
(ULONG(*)(void *))HardwarePlugIn_AddRef,
|
||||
(ULONG(*)(void *))HardwarePlugIn_Release,
|
||||
|
||||
// DAL Plug-In Routines
|
||||
HardwarePlugIn_Initialize, HardwarePlugIn_InitializeWithObjectID,
|
||||
HardwarePlugIn_Teardown, HardwarePlugIn_ObjectShow,
|
||||
HardwarePlugIn_ObjectHasProperty,
|
||||
HardwarePlugIn_ObjectIsPropertySettable,
|
||||
HardwarePlugIn_ObjectGetPropertyDataSize,
|
||||
HardwarePlugIn_ObjectGetPropertyData,
|
||||
HardwarePlugIn_ObjectSetPropertyData, HardwarePlugIn_DeviceSuspend,
|
||||
HardwarePlugIn_DeviceResume, HardwarePlugIn_DeviceStartStream,
|
||||
HardwarePlugIn_DeviceStopStream, HardwarePlugIn_DeviceProcessAVCCommand,
|
||||
HardwarePlugIn_DeviceProcessRS422Command,
|
||||
HardwarePlugIn_StreamCopyBufferQueue, HardwarePlugIn_StreamDeckPlay,
|
||||
HardwarePlugIn_StreamDeckStop, HardwarePlugIn_StreamDeckJog,
|
||||
HardwarePlugIn_StreamDeckCueTo};
|
||||
|
||||
static CMIOHardwarePlugInInterface *sInterfacePtr = &sInterface;
|
||||
static CMIOHardwarePlugInRef sPlugInRef = &sInterfacePtr;
|
||||
|
||||
CMIOHardwarePlugInRef PlugInRef()
|
||||
{
|
||||
return sPlugInRef;
|
||||
}
|
37
plugins/mac-virtualcam/src/dal-plugin/PlugInMain.mm
Normal file
37
plugins/mac-virtualcam/src/dal-plugin/PlugInMain.mm
Normal file
@ -0,0 +1,37 @@
|
||||
//
|
||||
// PlugInMain.mm
|
||||
// obs-mac-virtualcam
|
||||
//
|
||||
// Created by John Boiles on 4/9/20.
|
||||
//
|
||||
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// obs-mac-virtualcam is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#import <CoreMediaIO/CMIOHardwarePlugin.h>
|
||||
|
||||
#import "PlugInInterface.h"
|
||||
#import "Logging.h"
|
||||
#import "Defines.h"
|
||||
|
||||
//! PlugInMain is the entrypoint for the plugin
|
||||
extern "C" {
|
||||
void *PlugInMain(CFAllocatorRef allocator, CFUUIDRef requestedTypeUUID)
|
||||
{
|
||||
DLogFunc(@"version=%@", PLUGIN_VERSION);
|
||||
if (!CFEqual(requestedTypeUUID, kCMIOHardwarePlugInTypeID)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return PlugInRef();
|
||||
}
|
||||
}
|
48
plugins/mac-virtualcam/src/dal-plugin/Stream.h
Normal file
48
plugins/mac-virtualcam/src/dal-plugin/Stream.h
Normal file
@ -0,0 +1,48 @@
|
||||
//
|
||||
// Stream.h
|
||||
// obs-mac-virtualcam
|
||||
//
|
||||
// Created by John Boiles on 4/10/20.
|
||||
//
|
||||
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// obs-mac-virtualcam is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "ObjectStore.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface Stream : NSObject <CMIOObject>
|
||||
|
||||
@property CMIOStreamID objectId;
|
||||
|
||||
- (instancetype _Nonnull)init;
|
||||
|
||||
- (CMSimpleQueueRef)copyBufferQueueWithAlteredProc:
|
||||
(CMIODeviceStreamQueueAlteredProc)alteredProc
|
||||
alteredRefCon:(void *)alteredRefCon;
|
||||
|
||||
- (void)startServingDefaultFrames;
|
||||
|
||||
- (void)stopServingDefaultFrames;
|
||||
|
||||
- (void)queueFrameWithSize:(NSSize)size
|
||||
timestamp:(uint64_t)timestamp
|
||||
fpsNumerator:(uint32_t)fpsNumerator
|
||||
fpsDenominator:(uint32_t)fpsDenominator
|
||||
frameData:(NSData *)frameData;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
571
plugins/mac-virtualcam/src/dal-plugin/Stream.mm
Normal file
571
plugins/mac-virtualcam/src/dal-plugin/Stream.mm
Normal file
@ -0,0 +1,571 @@
|
||||
//
|
||||
// Stream.mm
|
||||
// obs-mac-virtualcam
|
||||
//
|
||||
// Created by John Boiles on 4/10/20.
|
||||
//
|
||||
// obs-mac-virtualcam is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// obs-mac-virtualcam is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#import "Stream.h"
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <mach/mach_time.h>
|
||||
#include <CoreMediaIO/CMIOSampleBuffer.h>
|
||||
|
||||
#import "Logging.h"
|
||||
#import "CMSampleBufferUtils.h"
|
||||
#import "TestCard.h"
|
||||
#import "PlugIn.h"
|
||||
|
||||
@interface Stream () {
|
||||
CMSimpleQueueRef _queue;
|
||||
CFTypeRef _clock;
|
||||
NSImage *_testCardImage;
|
||||
dispatch_source_t _frameDispatchSource;
|
||||
NSSize _testCardSize;
|
||||
Float64 _fps;
|
||||
}
|
||||
|
||||
@property CMIODeviceStreamQueueAlteredProc alteredProc;
|
||||
@property void *alteredRefCon;
|
||||
@property (readonly) CMSimpleQueueRef queue;
|
||||
@property (readonly) CFTypeRef clock;
|
||||
@property UInt64 sequenceNumber;
|
||||
@property (readonly) NSImage *testCardImage;
|
||||
@property (readonly) NSSize testCardSize;
|
||||
@property (readonly) Float64 fps;
|
||||
|
||||
@end
|
||||
|
||||
@implementation Stream
|
||||
|
||||
#define DEFAULT_FPS 30.0
|
||||
#define DEFAULT_WIDTH 1280
|
||||
#define DEFAULT_HEIGHT 720
|
||||
|
||||
- (instancetype _Nonnull)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_frameDispatchSource = dispatch_source_create(
|
||||
DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
|
||||
dispatch_get_global_queue(
|
||||
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
|
||||
__weak __typeof(self) wself = self;
|
||||
dispatch_source_set_event_handler(_frameDispatchSource, ^{
|
||||
[wself fillFrame];
|
||||
});
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
DLog(@"Stream Dealloc");
|
||||
CMIOStreamClockInvalidate(_clock);
|
||||
CFRelease(_clock);
|
||||
_clock = NULL;
|
||||
CFRelease(_queue);
|
||||
_queue = NULL;
|
||||
dispatch_suspend(_frameDispatchSource);
|
||||
}
|
||||
|
||||
- (void)startServingDefaultFrames
|
||||
{
|
||||
DLogFunc(@"");
|
||||
_testCardImage = nil;
|
||||
_testCardSize = NSZeroSize;
|
||||
_fps = 0;
|
||||
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 0);
|
||||
uint64_t intervalTime = (int64_t)(NSEC_PER_SEC / self.fps);
|
||||
dispatch_source_set_timer(_frameDispatchSource, startTime, intervalTime,
|
||||
0);
|
||||
dispatch_resume(_frameDispatchSource);
|
||||
}
|
||||
|
||||
- (void)stopServingDefaultFrames
|
||||
{
|
||||
DLogFunc(@"");
|
||||
dispatch_suspend(_frameDispatchSource);
|
||||
}
|
||||
|
||||
- (CMSimpleQueueRef)queue
|
||||
{
|
||||
if (_queue == NULL) {
|
||||
// Allocate a one-second long queue, which we can use our FPS constant for.
|
||||
OSStatus err = CMSimpleQueueCreate(kCFAllocatorDefault,
|
||||
self.fps, &_queue);
|
||||
if (err != noErr) {
|
||||
DLog(@"Err %d in CMSimpleQueueCreate", err);
|
||||
}
|
||||
}
|
||||
return _queue;
|
||||
}
|
||||
|
||||
- (CFTypeRef)clock
|
||||
{
|
||||
if (_clock == NULL) {
|
||||
OSStatus err = CMIOStreamClockCreate(
|
||||
kCFAllocatorDefault,
|
||||
CFSTR("obs-mac-virtualcam::Stream::clock"),
|
||||
(__bridge void *)self, CMTimeMake(1, 10), 100, 10,
|
||||
&_clock);
|
||||
if (err != noErr) {
|
||||
DLog(@"Error %d from CMIOStreamClockCreate", err);
|
||||
}
|
||||
}
|
||||
return _clock;
|
||||
}
|
||||
|
||||
- (NSSize)testCardSize
|
||||
{
|
||||
if (NSEqualSizes(_testCardSize, NSZeroSize)) {
|
||||
NSUserDefaults *defaults =
|
||||
[NSUserDefaults standardUserDefaults];
|
||||
int width = [[defaults objectForKey:kTestCardWidthKey]
|
||||
integerValue];
|
||||
int height = [[defaults objectForKey:kTestCardHeightKey]
|
||||
integerValue];
|
||||
if (width == 0 || height == 0) {
|
||||
_testCardSize =
|
||||
NSMakeSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
} else {
|
||||
_testCardSize = NSMakeSize(width, height);
|
||||
}
|
||||
}
|
||||
return _testCardSize;
|
||||
}
|
||||
|
||||
- (Float64)fps
|
||||
{
|
||||
if (_fps == 0) {
|
||||
NSUserDefaults *defaults =
|
||||
[NSUserDefaults standardUserDefaults];
|
||||
double fps =
|
||||
[[defaults objectForKey:kTestCardFPSKey] doubleValue];
|
||||
if (fps == 0) {
|
||||
_fps = DEFAULT_FPS;
|
||||
} else {
|
||||
_fps = fps;
|
||||
}
|
||||
}
|
||||
return _fps;
|
||||
}
|
||||
|
||||
- (NSImage *)testCardImage
|
||||
{
|
||||
if (_testCardImage == nil) {
|
||||
NSString *bundlePath =
|
||||
[[NSBundle bundleForClass:[Stream class]] bundlePath];
|
||||
NSString *placeHolderPath = [bundlePath
|
||||
stringByAppendingString:
|
||||
@"/Contents/Resources/placeholder.png"];
|
||||
NSImage *placeholderImage = [[NSImage alloc]
|
||||
initWithContentsOfFile:placeHolderPath];
|
||||
|
||||
NSBitmapImageRep *rep = [[NSBitmapImageRep alloc]
|
||||
initWithBitmapDataPlanes:NULL
|
||||
pixelsWide:self.testCardSize.width
|
||||
pixelsHigh:self.testCardSize.height
|
||||
bitsPerSample:8
|
||||
samplesPerPixel:4
|
||||
hasAlpha:YES
|
||||
isPlanar:NO
|
||||
colorSpaceName:NSCalibratedRGBColorSpace
|
||||
bytesPerRow:0
|
||||
bitsPerPixel:0];
|
||||
rep.size = self.testCardSize;
|
||||
|
||||
float hScale =
|
||||
placeholderImage.size.width / self.testCardSize.width;
|
||||
float vScale =
|
||||
placeholderImage.size.height / self.testCardSize.height;
|
||||
|
||||
float scaling = fmax(hScale, vScale);
|
||||
|
||||
float newWidth = placeholderImage.size.width / scaling;
|
||||
float newHeight = placeholderImage.size.height / scaling;
|
||||
|
||||
float leftOffset = (self.testCardSize.width - newWidth) / 2;
|
||||
float topOffset = (self.testCardSize.height - newHeight) / 2;
|
||||
|
||||
[NSGraphicsContext saveGraphicsState];
|
||||
[NSGraphicsContext
|
||||
setCurrentContext:
|
||||
[NSGraphicsContext
|
||||
graphicsContextWithBitmapImageRep:rep]];
|
||||
|
||||
NSColor *backgroundColor = [NSColor blackColor];
|
||||
[backgroundColor set];
|
||||
NSRectFill(NSMakeRect(0, 0, self.testCardSize.width,
|
||||
self.testCardSize.height));
|
||||
|
||||
[placeholderImage drawInRect:NSMakeRect(leftOffset, topOffset,
|
||||
newWidth, newHeight)
|
||||
fromRect:NSZeroRect
|
||||
operation:NSCompositingOperationCopy
|
||||
fraction:1.0];
|
||||
[NSGraphicsContext restoreGraphicsState];
|
||||
|
||||
NSImage *testCardImage =
|
||||
[[NSImage alloc] initWithSize:self.testCardSize];
|
||||
[testCardImage addRepresentation:rep];
|
||||
|
||||
_testCardImage = testCardImage;
|
||||
}
|
||||
return _testCardImage;
|
||||
}
|
||||
|
||||
- (CMSimpleQueueRef)copyBufferQueueWithAlteredProc:
|
||||
(CMIODeviceStreamQueueAlteredProc)alteredProc
|
||||
alteredRefCon:(void *)alteredRefCon
|
||||
{
|
||||
self.alteredProc = alteredProc;
|
||||
self.alteredRefCon = alteredRefCon;
|
||||
|
||||
// Retain this since it's a copy operation
|
||||
CFRetain(self.queue);
|
||||
|
||||
return self.queue;
|
||||
}
|
||||
|
||||
- (CVPixelBufferRef)createPixelBufferWithTestAnimation
|
||||
{
|
||||
int width = self.testCardSize.width;
|
||||
int height = self.testCardSize.height;
|
||||
|
||||
NSDictionary *options = [NSDictionary
|
||||
dictionaryWithObjectsAndKeys:
|
||||
[NSNumber numberWithBool:YES],
|
||||
kCVPixelBufferCGImageCompatibilityKey,
|
||||
[NSNumber numberWithBool:YES],
|
||||
kCVPixelBufferCGBitmapContextCompatibilityKey, nil];
|
||||
CVPixelBufferRef pxbuffer = NULL;
|
||||
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, width,
|
||||
height, kCVPixelFormatType_32ARGB,
|
||||
(__bridge CFDictionaryRef)options,
|
||||
&pxbuffer);
|
||||
|
||||
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
|
||||
|
||||
CVPixelBufferLockBaseAddress(pxbuffer, 0);
|
||||
void *pxdata = CVPixelBufferGetBaseAddressOfPlane(pxbuffer, 0);
|
||||
NSParameterAssert(pxdata != NULL);
|
||||
|
||||
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
CGContextRef context = CGBitmapContextCreate(
|
||||
pxdata, width, height, 8,
|
||||
CVPixelBufferGetBytesPerRowOfPlane(pxbuffer, 0), rgbColorSpace,
|
||||
kCGImageAlphaPremultipliedFirst | kCGImageByteOrder32Big);
|
||||
NSParameterAssert(context);
|
||||
|
||||
NSGraphicsContext *nsContext = [NSGraphicsContext
|
||||
graphicsContextWithCGContext:context
|
||||
flipped:NO];
|
||||
[NSGraphicsContext setCurrentContext:nsContext];
|
||||
|
||||
NSRect rect = NSMakeRect(0, 0, self.testCardImage.size.width,
|
||||
self.testCardImage.size.height);
|
||||
CGImageRef image = [self.testCardImage CGImageForProposedRect:&rect
|
||||
context:nsContext
|
||||
hints:nil];
|
||||
CGContextDrawImage(context,
|
||||
CGRectMake(0, 0, CGImageGetWidth(image),
|
||||
CGImageGetHeight(image)),
|
||||
image);
|
||||
|
||||
// DrawDialWithFrame(
|
||||
// NSMakeRect(0, 0, width, height),
|
||||
// (int(self.fps) - self.sequenceNumber % int(self.fps)) * 360 /
|
||||
// int(self.fps));
|
||||
|
||||
CGContextRelease(context);
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
|
||||
|
||||
return pxbuffer;
|
||||
}
|
||||
|
||||
- (void)fillFrame
|
||||
{
|
||||
if (CMSimpleQueueGetFullness(self.queue) >= 1.0) {
|
||||
DLog(@"Queue is full, bailing out");
|
||||
return;
|
||||
}
|
||||
|
||||
CVPixelBufferRef pixelBuffer =
|
||||
[self createPixelBufferWithTestAnimation];
|
||||
|
||||
uint64_t hostTime = mach_absolute_time();
|
||||
CMSampleTimingInfo timingInfo =
|
||||
CMSampleTimingInfoForTimestamp(hostTime, self.fps, 1);
|
||||
|
||||
OSStatus err = CMIOStreamClockPostTimingEvent(
|
||||
timingInfo.presentationTimeStamp, hostTime, true, self.clock);
|
||||
if (err != noErr) {
|
||||
DLog(@"CMIOStreamClockPostTimingEvent err %d", err);
|
||||
}
|
||||
|
||||
CMFormatDescriptionRef format;
|
||||
CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault,
|
||||
pixelBuffer, &format);
|
||||
|
||||
self.sequenceNumber = CMIOGetNextSequenceNumber(self.sequenceNumber);
|
||||
|
||||
CMSampleBufferRef buffer;
|
||||
err = CMIOSampleBufferCreateForImageBuffer(
|
||||
kCFAllocatorDefault, pixelBuffer, format, &timingInfo,
|
||||
self.sequenceNumber, kCMIOSampleBufferNoDiscontinuities,
|
||||
&buffer);
|
||||
CFRelease(pixelBuffer);
|
||||
CFRelease(format);
|
||||
if (err != noErr) {
|
||||
DLog(@"CMIOSampleBufferCreateForImageBuffer err %d", err);
|
||||
}
|
||||
|
||||
CMSimpleQueueEnqueue(self.queue, buffer);
|
||||
|
||||
// Inform the clients that the queue has been altered
|
||||
if (self.alteredProc != NULL) {
|
||||
(self.alteredProc)(self.objectId, buffer, self.alteredRefCon);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)queueFrameWithSize:(NSSize)size
|
||||
timestamp:(uint64_t)timestamp
|
||||
fpsNumerator:(uint32_t)fpsNumerator
|
||||
fpsDenominator:(uint32_t)fpsDenominator
|
||||
frameData:(NSData *)frameData
|
||||
{
|
||||
if (CMSimpleQueueGetFullness(self.queue) >= 1.0) {
|
||||
DLog(@"Queue is full, bailing out");
|
||||
return;
|
||||
}
|
||||
OSStatus err = noErr;
|
||||
|
||||
CMSampleTimingInfo timingInfo = CMSampleTimingInfoForTimestamp(
|
||||
timestamp, fpsNumerator, fpsDenominator);
|
||||
|
||||
err = CMIOStreamClockPostTimingEvent(timingInfo.presentationTimeStamp,
|
||||
mach_absolute_time(), true,
|
||||
self.clock);
|
||||
if (err != noErr) {
|
||||
DLog(@"CMIOStreamClockPostTimingEvent err %d", err);
|
||||
}
|
||||
|
||||
self.sequenceNumber = CMIOGetNextSequenceNumber(self.sequenceNumber);
|
||||
|
||||
CMSampleBufferRef sampleBuffer;
|
||||
CMSampleBufferCreateFromData(size, timingInfo, self.sequenceNumber,
|
||||
frameData, &sampleBuffer);
|
||||
CMSimpleQueueEnqueue(self.queue, sampleBuffer);
|
||||
|
||||
// Inform the clients that the queue has been altered
|
||||
if (self.alteredProc != NULL) {
|
||||
(self.alteredProc)(self.objectId, sampleBuffer,
|
||||
self.alteredRefCon);
|
||||
}
|
||||
}
|
||||
|
||||
- (CMVideoFormatDescriptionRef)getFormatDescription
|
||||
{
|
||||
CMVideoFormatDescriptionRef formatDescription;
|
||||
OSStatus err = CMVideoFormatDescriptionCreate(
|
||||
kCFAllocatorDefault, kCMVideoCodecType_422YpCbCr8,
|
||||
self.testCardSize.width, self.testCardSize.height, NULL,
|
||||
&formatDescription);
|
||||
if (err != noErr) {
|
||||
DLog(@"Error %d from CMVideoFormatDescriptionCreate", err);
|
||||
}
|
||||
return formatDescription;
|
||||
}
|
||||
|
||||
#pragma mark - CMIOObject
|
||||
|
||||
- (UInt32)getPropertyDataSizeWithAddress:(CMIOObjectPropertyAddress)address
|
||||
qualifierDataSize:(UInt32)qualifierDataSize
|
||||
qualifierData:(nonnull const void *)qualifierData
|
||||
{
|
||||
switch (address.mSelector) {
|
||||
case kCMIOStreamPropertyInitialPresentationTimeStampForLinkedAndSyncedAudio:
|
||||
return sizeof(CMTime);
|
||||
case kCMIOStreamPropertyOutputBuffersNeededForThrottledPlayback:
|
||||
return sizeof(UInt32);
|
||||
case kCMIOObjectPropertyName:
|
||||
return sizeof(CFStringRef);
|
||||
case kCMIOObjectPropertyManufacturer:
|
||||
return sizeof(CFStringRef);
|
||||
case kCMIOObjectPropertyElementName:
|
||||
return sizeof(CFStringRef);
|
||||
case kCMIOObjectPropertyElementCategoryName:
|
||||
return sizeof(CFStringRef);
|
||||
case kCMIOObjectPropertyElementNumberName:
|
||||
return sizeof(CFStringRef);
|
||||
case kCMIOStreamPropertyDirection:
|
||||
return sizeof(UInt32);
|
||||
case kCMIOStreamPropertyTerminalType:
|
||||
return sizeof(UInt32);
|
||||
case kCMIOStreamPropertyStartingChannel:
|
||||
return sizeof(UInt32);
|
||||
case kCMIOStreamPropertyLatency:
|
||||
return sizeof(UInt32);
|
||||
case kCMIOStreamPropertyFormatDescriptions:
|
||||
return sizeof(CFArrayRef);
|
||||
case kCMIOStreamPropertyFormatDescription:
|
||||
return sizeof(CMFormatDescriptionRef);
|
||||
case kCMIOStreamPropertyFrameRateRanges:
|
||||
return sizeof(AudioValueRange);
|
||||
case kCMIOStreamPropertyFrameRate:
|
||||
case kCMIOStreamPropertyFrameRates:
|
||||
return sizeof(Float64);
|
||||
case kCMIOStreamPropertyMinimumFrameRate:
|
||||
return sizeof(Float64);
|
||||
case kCMIOStreamPropertyClock:
|
||||
return sizeof(CFTypeRef);
|
||||
default:
|
||||
DLog(@"Stream unhandled getPropertyDataSizeWithAddress for %@",
|
||||
[ObjectStore
|
||||
StringFromPropertySelector:address.mSelector]);
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
- (void)getPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
|
||||
qualifierDataSize:(UInt32)qualifierDataSize
|
||||
qualifierData:(nonnull const void *)qualifierData
|
||||
dataSize:(UInt32)dataSize
|
||||
dataUsed:(nonnull UInt32 *)dataUsed
|
||||
data:(nonnull void *)data
|
||||
{
|
||||
switch (address.mSelector) {
|
||||
case kCMIOObjectPropertyName:
|
||||
*static_cast<CFStringRef *>(data) = CFSTR("OBS Virtual Camera");
|
||||
*dataUsed = sizeof(CFStringRef);
|
||||
break;
|
||||
case kCMIOObjectPropertyElementName:
|
||||
*static_cast<CFStringRef *>(data) =
|
||||
CFSTR("OBS Virtual Camera Stream Element");
|
||||
*dataUsed = sizeof(CFStringRef);
|
||||
break;
|
||||
case kCMIOObjectPropertyManufacturer:
|
||||
case kCMIOObjectPropertyElementCategoryName:
|
||||
case kCMIOObjectPropertyElementNumberName:
|
||||
case kCMIOStreamPropertyTerminalType:
|
||||
case kCMIOStreamPropertyStartingChannel:
|
||||
case kCMIOStreamPropertyLatency:
|
||||
case kCMIOStreamPropertyInitialPresentationTimeStampForLinkedAndSyncedAudio:
|
||||
case kCMIOStreamPropertyOutputBuffersNeededForThrottledPlayback:
|
||||
break;
|
||||
case kCMIOStreamPropertyDirection:
|
||||
*static_cast<UInt32 *>(data) = 1;
|
||||
*dataUsed = sizeof(UInt32);
|
||||
break;
|
||||
case kCMIOStreamPropertyFormatDescriptions:
|
||||
*static_cast<CFArrayRef *>(
|
||||
data) = (__bridge_retained CFArrayRef)[NSArray
|
||||
arrayWithObject:(__bridge_transfer NSObject *)
|
||||
[self getFormatDescription]];
|
||||
*dataUsed = sizeof(CFArrayRef);
|
||||
break;
|
||||
case kCMIOStreamPropertyFormatDescription:
|
||||
*static_cast<CMVideoFormatDescriptionRef *>(data) =
|
||||
[self getFormatDescription];
|
||||
*dataUsed = sizeof(CMVideoFormatDescriptionRef);
|
||||
break;
|
||||
case kCMIOStreamPropertyFrameRateRanges:
|
||||
AudioValueRange range;
|
||||
range.mMinimum = self.fps;
|
||||
range.mMaximum = self.fps;
|
||||
*static_cast<AudioValueRange *>(data) = range;
|
||||
*dataUsed = sizeof(AudioValueRange);
|
||||
break;
|
||||
case kCMIOStreamPropertyFrameRate:
|
||||
case kCMIOStreamPropertyFrameRates:
|
||||
*static_cast<Float64 *>(data) = self.fps;
|
||||
*dataUsed = sizeof(Float64);
|
||||
break;
|
||||
case kCMIOStreamPropertyMinimumFrameRate:
|
||||
*static_cast<Float64 *>(data) = self.fps;
|
||||
*dataUsed = sizeof(Float64);
|
||||
break;
|
||||
case kCMIOStreamPropertyClock:
|
||||
*static_cast<CFTypeRef *>(data) = self.clock;
|
||||
// This one was incredibly tricky and cost me many hours to find. It seems that DAL expects
|
||||
// the clock to be retained when returned. It's unclear why, and that seems inconsistent
|
||||
// with other properties that don't have the same behavior. But this is what Apple's sample
|
||||
// code does.
|
||||
// https://github.com/lvsti/CoreMediaIO-DAL-Example/blob/0392cb/Sources/Extras/CoreMediaIO/DeviceAbstractionLayer/Devices/DP/Properties/CMIO_DP_Property_Clock.cpp#L75
|
||||
CFRetain(*static_cast<CFTypeRef *>(data));
|
||||
*dataUsed = sizeof(CFTypeRef);
|
||||
break;
|
||||
default:
|
||||
DLog(@"Stream unhandled getPropertyDataWithAddress for %@",
|
||||
[ObjectStore
|
||||
StringFromPropertySelector:address.mSelector]);
|
||||
*dataUsed = 0;
|
||||
};
|
||||
}
|
||||
|
||||
- (BOOL)hasPropertyWithAddress:(CMIOObjectPropertyAddress)address
|
||||
{
|
||||
switch (address.mSelector) {
|
||||
case kCMIOObjectPropertyName:
|
||||
case kCMIOObjectPropertyElementName:
|
||||
case kCMIOStreamPropertyFormatDescriptions:
|
||||
case kCMIOStreamPropertyFormatDescription:
|
||||
case kCMIOStreamPropertyFrameRateRanges:
|
||||
case kCMIOStreamPropertyFrameRate:
|
||||
case kCMIOStreamPropertyFrameRates:
|
||||
case kCMIOStreamPropertyMinimumFrameRate:
|
||||
case kCMIOStreamPropertyClock:
|
||||
return true;
|
||||
case kCMIOObjectPropertyManufacturer:
|
||||
case kCMIOObjectPropertyElementCategoryName:
|
||||
case kCMIOObjectPropertyElementNumberName:
|
||||
case kCMIOStreamPropertyDirection:
|
||||
case kCMIOStreamPropertyTerminalType:
|
||||
case kCMIOStreamPropertyStartingChannel:
|
||||
case kCMIOStreamPropertyLatency:
|
||||
case kCMIOStreamPropertyInitialPresentationTimeStampForLinkedAndSyncedAudio:
|
||||
case kCMIOStreamPropertyOutputBuffersNeededForThrottledPlayback:
|
||||
DLog(@"TODO: %@",
|
||||
[ObjectStore
|
||||
StringFromPropertySelector:address.mSelector]);
|
||||
return false;
|
||||
default:
|
||||
DLog(@"Stream unhandled hasPropertyWithAddress for %@",
|
||||
[ObjectStore
|
||||
StringFromPropertySelector:address.mSelector]);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
- (BOOL)isPropertySettableWithAddress:(CMIOObjectPropertyAddress)address
|
||||
{
|
||||
DLog(@"Stream unhandled isPropertySettableWithAddress for %@",
|
||||
[ObjectStore StringFromPropertySelector:address.mSelector]);
|
||||
return false;
|
||||
}
|
||||
|
||||
- (void)setPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
|
||||
qualifierDataSize:(UInt32)qualifierDataSize
|
||||
qualifierData:(nonnull const void *)qualifierData
|
||||
dataSize:(UInt32)dataSize
|
||||
data:(nonnull const void *)data
|
||||
{
|
||||
DLog(@"Stream unhandled setPropertyDataWithAddress for %@",
|
||||
[ObjectStore StringFromPropertySelector:address.mSelector]);
|
||||
}
|
||||
|
||||
@end
|
14
plugins/mac-virtualcam/src/dal-plugin/TestCard.h
Normal file
14
plugins/mac-virtualcam/src/dal-plugin/TestCard.h
Normal file
@ -0,0 +1,14 @@
|
||||
//
|
||||
// TestCard.h
|
||||
// dal-plugin
|
||||
//
|
||||
// Created by John Boiles on 5/8/20.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
|
||||
void DrawTestCardWithFrame(CGContextRef context, NSRect frame);
|
||||
void DrawDialWithFrame(NSRect frame, CGFloat rotation);
|
||||
|
||||
NSImage *ImageOfTestCardWithSize(NSSize imageSize);
|
1452
plugins/mac-virtualcam/src/dal-plugin/TestCard.mm
Normal file
1452
plugins/mac-virtualcam/src/dal-plugin/TestCard.mm
Normal file
File diff suppressed because it is too large
Load Diff
BIN
plugins/mac-virtualcam/src/dal-plugin/placeholder.png
Normal file
BIN
plugins/mac-virtualcam/src/dal-plugin/placeholder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 307 KiB |
59
plugins/mac-virtualcam/src/obs-plugin/CMakeLists.txt
Normal file
59
plugins/mac-virtualcam/src/obs-plugin/CMakeLists.txt
Normal file
@ -0,0 +1,59 @@
|
||||
project(mac-virtualcam)
|
||||
|
||||
find_library(AVFOUNDATION AVFoundation)
|
||||
find_library(APPKIT AppKit)
|
||||
find_library(COCOA Cocoa)
|
||||
find_library(COREFOUNDATION CoreFoundation)
|
||||
find_library(COREMEDIA CoreMedia)
|
||||
find_library(COREVIDEO CoreVideo)
|
||||
find_library(COCOA Cocoa)
|
||||
find_library(COREMEDIAIO CoreMediaIO)
|
||||
find_library(IOSURFACE IOSurface)
|
||||
find_library(IOKIT IOKit)
|
||||
|
||||
include_directories(${AVFOUNDATION}
|
||||
${APPKIT}
|
||||
${COCOA}
|
||||
${COREFOUNDATION}
|
||||
${COREMEDIA}
|
||||
${COREVIDEO}
|
||||
${COREMEDIAIO}
|
||||
${COCOA}
|
||||
${IOSURFACE}
|
||||
"${CMAKE_SOURCE_DIR}/UI/obs-frontend-api"
|
||||
../common)
|
||||
|
||||
set(mac-virtualcam_HEADERS
|
||||
Defines.h
|
||||
MachServer.h
|
||||
../common/MachProtocol.h)
|
||||
|
||||
set(mac-virtualcam_SOURCES
|
||||
plugin-main.mm
|
||||
MachServer.mm)
|
||||
|
||||
add_library(mac-virtualcam MODULE
|
||||
${mac-virtualcam_SOURCES}
|
||||
${mac-virtualcam_HEADERS})
|
||||
|
||||
target_link_libraries(mac-virtualcam
|
||||
libobs
|
||||
obs-frontend-api
|
||||
Qt5::Core
|
||||
Qt5::Widgets
|
||||
${AVFOUNDATION}
|
||||
${APPKIT}
|
||||
${COCOA}
|
||||
${COREFOUNDATION}
|
||||
${COREMEDIA}
|
||||
${COREVIDEO}
|
||||
${COREMEDIAIO}
|
||||
${IOSURFACE}
|
||||
${IOKIT})
|
||||
|
||||
set_target_properties(mac-virtualcam PROPERTIES
|
||||
FOLDER "plugins"
|
||||
COMPILE_FLAGS "-std=gnu++14 -stdlib=libc++ -fobjc-arc -fobjc-weak"
|
||||
)
|
||||
|
||||
install_obs_plugin_with_data(mac-virtualcam data)
|
24
plugins/mac-virtualcam/src/obs-plugin/Defines.h
Normal file
24
plugins/mac-virtualcam/src/obs-plugin/Defines.h
Normal 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__)
|
29
plugins/mac-virtualcam/src/obs-plugin/MachServer.h
Normal file
29
plugins/mac-virtualcam/src/obs-plugin/MachServer.h
Normal file
@ -0,0 +1,29 @@
|
||||
//
|
||||
// MachServer.h
|
||||
// obs-mac-virtualcam
|
||||
//
|
||||
// Created by John Boiles on 5/5/20.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MachServer : NSObject
|
||||
|
||||
- (void)run;
|
||||
|
||||
/*!
|
||||
Will eventually be used for sending frames to all connected clients
|
||||
*/
|
||||
- (void)sendFrameWithSize:(NSSize)size
|
||||
timestamp:(uint64_t)timestamp
|
||||
fpsNumerator:(uint32_t)fpsNumerator
|
||||
fpsDenominator:(uint32_t)fpsDenominator
|
||||
frameBytes:(uint8_t *)frameBytes;
|
||||
|
||||
- (void)stop;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
178
plugins/mac-virtualcam/src/obs-plugin/MachServer.mm
Normal file
178
plugins/mac-virtualcam/src/obs-plugin/MachServer.mm
Normal file
@ -0,0 +1,178 @@
|
||||
//
|
||||
// MachServer.m
|
||||
// mac-virtualcam
|
||||
//
|
||||
// Created by John Boiles on 5/5/20.
|
||||
//
|
||||
|
||||
#import "MachServer.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <obs-module.h>
|
||||
#include "MachProtocol.h"
|
||||
#include "Defines.h"
|
||||
|
||||
@interface MachServer () <NSPortDelegate>
|
||||
@property NSPort *port;
|
||||
@property NSMutableSet *clientPorts;
|
||||
@property NSRunLoop *runLoop;
|
||||
@end
|
||||
|
||||
@implementation MachServer
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
self.clientPorts = [[NSMutableSet alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
blog(LOG_DEBUG, "tearing down MachServer");
|
||||
[self.runLoop removePort:self.port forMode:NSDefaultRunLoopMode];
|
||||
[self.port invalidate];
|
||||
self.port.delegate = nil;
|
||||
}
|
||||
|
||||
- (void)run
|
||||
{
|
||||
if (self.port != nil) {
|
||||
blog(LOG_DEBUG, "mach server already running!");
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a bummer this is deprecated. The replacement, NSXPCConnection, seems to require
|
||||
// an assistant process that lives inside the .app bundle. This would be more modern, but adds
|
||||
// complexity and I think makes it impossible to just run the `obs` binary from the commandline.
|
||||
// So let's stick with NSMachBootstrapServer at least until it fully goes away.
|
||||
// At that point we can decide between NSXPCConnection and using the CoreFoundation versions of
|
||||
// these APIs (which are, interestingly, not deprecated)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
self.port = [[NSMachBootstrapServer sharedInstance]
|
||||
servicePortWithName:@MACH_SERVICE_NAME];
|
||||
#pragma clang diagnostic pop
|
||||
if (self.port == nil) {
|
||||
// This probably means another instance is running.
|
||||
blog(LOG_ERROR, "Unable to open mach server port.");
|
||||
return;
|
||||
}
|
||||
|
||||
self.port.delegate = self;
|
||||
|
||||
self.runLoop = [NSRunLoop currentRunLoop];
|
||||
[self.runLoop addPort:self.port forMode:NSDefaultRunLoopMode];
|
||||
|
||||
blog(LOG_DEBUG, "mach server running!");
|
||||
}
|
||||
|
||||
- (void)handlePortMessage:(NSPortMessage *)message
|
||||
{
|
||||
switch (message.msgid) {
|
||||
case MachMsgIdConnect:
|
||||
if (message.sendPort != nil) {
|
||||
blog(LOG_DEBUG,
|
||||
"mach server received connect message from port %d!",
|
||||
((NSMachPort *)message.sendPort).machPort);
|
||||
[self.clientPorts addObject:message.sendPort];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
blog(LOG_ERROR, "Unexpected mach message ID %u",
|
||||
(unsigned)message.msgid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)sendMessageToClientsWithMsgId:(uint32_t)msgId
|
||||
components:(nullable NSArray *)components
|
||||
{
|
||||
if ([self.clientPorts count] <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableSet *removedPorts = [NSMutableSet set];
|
||||
|
||||
for (NSPort *port in self.clientPorts) {
|
||||
@try {
|
||||
NSPortMessage *message = [[NSPortMessage alloc]
|
||||
initWithSendPort:port
|
||||
receivePort:nil
|
||||
components:components];
|
||||
message.msgid = msgId;
|
||||
if (![message
|
||||
sendBeforeDate:
|
||||
[NSDate dateWithTimeIntervalSinceNow:
|
||||
1.0]]) {
|
||||
blog(LOG_DEBUG,
|
||||
"failed to send message to %d, removing it from the clients!",
|
||||
((NSMachPort *)port).machPort);
|
||||
[removedPorts addObject:port];
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
blog(LOG_DEBUG,
|
||||
"failed to send message (exception) to %d, removing it from the clients!",
|
||||
((NSMachPort *)port).machPort);
|
||||
[removedPorts addObject:port];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove dead ports if necessary
|
||||
[self.clientPorts minusSet:removedPorts];
|
||||
}
|
||||
|
||||
- (void)sendFrameWithSize:(NSSize)size
|
||||
timestamp:(uint64_t)timestamp
|
||||
fpsNumerator:(uint32_t)fpsNumerator
|
||||
fpsDenominator:(uint32_t)fpsDenominator
|
||||
frameBytes:(uint8_t *)frameBytes
|
||||
{
|
||||
if ([self.clientPorts count] <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@autoreleasepool {
|
||||
CGFloat width = size.width;
|
||||
NSData *widthData = [NSData dataWithBytes:&width
|
||||
length:sizeof(width)];
|
||||
CGFloat height = size.height;
|
||||
NSData *heightData = [NSData dataWithBytes:&height
|
||||
length:sizeof(height)];
|
||||
NSData *timestampData = [NSData
|
||||
dataWithBytes:×tamp
|
||||
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
|
@ -0,0 +1,5 @@
|
||||
UnsupportedResolution_Title="Unsupported resolution"
|
||||
UnsupportedResolution_Main="Your output resolution not supported. Please use one of the following:"
|
||||
VirtualCamera_Start="Start Virtual Camera"
|
||||
VirtualCamera_Stop="Stop Virtual Camera"
|
||||
Plugin_Name="macOS Virtual Webcam"
|
208
plugins/mac-virtualcam/src/obs-plugin/plugin-main.mm
Normal file
208
plugins/mac-virtualcam/src/obs-plugin/plugin-main.mm
Normal file
@ -0,0 +1,208 @@
|
||||
#include <obs-module.h>
|
||||
#include <obs.hpp>
|
||||
#include <pthread.h>
|
||||
#include <QMainWindow.h>
|
||||
#include <QAction.h>
|
||||
#include <obs-frontend-api.h>
|
||||
#include <obs.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <AppKit/AppKit.h>
|
||||
#include "MachServer.h"
|
||||
#include "Defines.h"
|
||||
|
||||
OBS_DECLARE_MODULE()
|
||||
OBS_MODULE_USE_DEFAULT_LOCALE("mac-virtualcam", "en-US")
|
||||
MODULE_EXPORT const char *obs_module_description(void)
|
||||
{
|
||||
return "macOS virtual webcam output";
|
||||
}
|
||||
|
||||
obs_output_t *outputRef;
|
||||
obs_video_info videoInfo;
|
||||
static MachServer *sMachServer;
|
||||
|
||||
static bool check_dal_plugin()
|
||||
{
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
|
||||
NSString *dalPluginDestinationPath =
|
||||
@"/Library/CoreMediaIO/Plug-Ins/DAL/";
|
||||
NSString *dalPluginFileName = [dalPluginDestinationPath
|
||||
stringByAppendingString:@"obs-mac-virtualcam.plugin"];
|
||||
|
||||
BOOL dalPluginInstalled =
|
||||
[fileManager fileExistsAtPath:dalPluginFileName];
|
||||
BOOL dalPluginUpdateNeeded = NO;
|
||||
|
||||
if (dalPluginInstalled) {
|
||||
NSString *dalPluginPlistPath = [dalPluginFileName
|
||||
stringByAppendingString:@"/Contents/Info.plist"];
|
||||
NSDictionary *dalPluginInfoPlist = [NSDictionary
|
||||
dictionaryWithContentsOfURL:
|
||||
[NSURL fileURLWithPath:dalPluginPlistPath]
|
||||
error:nil];
|
||||
NSString *dalPluginVersion = [dalPluginInfoPlist
|
||||
valueForKey:@"CFBundleShortVersionString"];
|
||||
const char *obsVersion = obs_get_version_string();
|
||||
|
||||
if (![dalPluginVersion isEqualToString:@(obsVersion)]) {
|
||||
dalPluginUpdateNeeded = YES;
|
||||
}
|
||||
} else {
|
||||
dalPluginUpdateNeeded = YES;
|
||||
}
|
||||
|
||||
if (dalPluginUpdateNeeded) {
|
||||
NSString *dalPluginSourcePath;
|
||||
NSRunningApplication *app =
|
||||
[NSRunningApplication currentApplication];
|
||||
|
||||
if ([app bundleIdentifier] != nil) {
|
||||
NSURL *bundleURL = [app bundleURL];
|
||||
NSString *pluginPath =
|
||||
@"Contents/Resources/data/obs-mac-virtualcam.plugin";
|
||||
|
||||
NSURL *pluginUrl = [bundleURL
|
||||
URLByAppendingPathComponent:pluginPath];
|
||||
dalPluginSourcePath = [pluginUrl path];
|
||||
} else {
|
||||
dalPluginSourcePath = [[[[app executableURL]
|
||||
URLByAppendingPathComponent:
|
||||
@"../data/obs-mac-virtualcam.plugin"]
|
||||
path]
|
||||
stringByReplacingOccurrencesOfString:@"obs/"
|
||||
withString:@""];
|
||||
}
|
||||
|
||||
if ([fileManager fileExistsAtPath:dalPluginSourcePath]) {
|
||||
NSString *copyCmd = [NSString
|
||||
stringWithFormat:
|
||||
@"do shell script \"cp -R '%@' '%@'\" with administrator privileges",
|
||||
dalPluginSourcePath,
|
||||
dalPluginDestinationPath];
|
||||
|
||||
NSDictionary *errorDict;
|
||||
NSAppleEventDescriptor *returnDescriptor = NULL;
|
||||
NSAppleScript *scriptObject =
|
||||
[[NSAppleScript alloc] initWithSource:copyCmd];
|
||||
returnDescriptor =
|
||||
[scriptObject executeAndReturnError:&errorDict];
|
||||
if (errorDict != nil) {
|
||||
const char *errorMessage = [[errorDict
|
||||
objectForKey:@"NSAppleScriptErrorMessage"]
|
||||
UTF8String];
|
||||
blog(LOG_INFO,
|
||||
"[macOS] VirtualCam DAL Plugin Installation status: %s",
|
||||
errorMessage);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
blog(LOG_INFO,
|
||||
"[macOS] VirtualCam DAL Plugin not shipped with OBS");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char *virtualcam_output_get_name(void *type_data)
|
||||
{
|
||||
(void)type_data;
|
||||
return obs_module_text("macOS Virtual Webcam");
|
||||
}
|
||||
|
||||
// This is a dummy pointer so we have something to return from virtualcam_output_create
|
||||
static void *data = &data;
|
||||
|
||||
static void *virtualcam_output_create(obs_data_t *settings,
|
||||
obs_output_t *output)
|
||||
{
|
||||
outputRef = output;
|
||||
|
||||
blog(LOG_DEBUG, "output_create");
|
||||
sMachServer = [[MachServer alloc] init];
|
||||
return data;
|
||||
}
|
||||
|
||||
static void virtualcam_output_destroy(void *data)
|
||||
{
|
||||
blog(LOG_DEBUG, "output_destroy");
|
||||
sMachServer = nil;
|
||||
}
|
||||
|
||||
static bool virtualcam_output_start(void *data)
|
||||
{
|
||||
bool hasDalPlugin = check_dal_plugin();
|
||||
|
||||
if (!hasDalPlugin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
blog(LOG_DEBUG, "output_start");
|
||||
|
||||
[sMachServer run];
|
||||
|
||||
obs_get_video_info(&videoInfo);
|
||||
|
||||
struct video_scale_info conversion = {};
|
||||
conversion.format = VIDEO_FORMAT_UYVY;
|
||||
conversion.width = videoInfo.output_width;
|
||||
conversion.height = videoInfo.output_height;
|
||||
obs_output_set_video_conversion(outputRef, &conversion);
|
||||
if (!obs_output_begin_data_capture(outputRef, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void virtualcam_output_stop(void *data, uint64_t ts)
|
||||
{
|
||||
blog(LOG_DEBUG, "output_stop");
|
||||
obs_output_end_data_capture(outputRef);
|
||||
[sMachServer stop];
|
||||
}
|
||||
|
||||
static void virtualcam_output_raw_video(void *data, struct video_data *frame)
|
||||
{
|
||||
uint8_t *outData = frame->data[0];
|
||||
if (frame->linesize[0] != (videoInfo.output_width * 2)) {
|
||||
blog(LOG_ERROR,
|
||||
"unexpected frame->linesize (expected:%d actual:%d)",
|
||||
(videoInfo.output_width * 2), frame->linesize[0]);
|
||||
}
|
||||
|
||||
CGFloat width = videoInfo.output_width;
|
||||
CGFloat height = videoInfo.output_height;
|
||||
|
||||
[sMachServer sendFrameWithSize:NSMakeSize(width, height)
|
||||
timestamp:frame->timestamp
|
||||
fpsNumerator:videoInfo.fps_num
|
||||
fpsDenominator:videoInfo.fps_den
|
||||
frameBytes:outData];
|
||||
}
|
||||
|
||||
struct obs_output_info virtualcam_output_info = {
|
||||
.id = "virtualcam_output",
|
||||
.flags = OBS_OUTPUT_VIDEO,
|
||||
.get_name = virtualcam_output_get_name,
|
||||
.create = virtualcam_output_create,
|
||||
.destroy = virtualcam_output_destroy,
|
||||
.start = virtualcam_output_start,
|
||||
.stop = virtualcam_output_stop,
|
||||
.raw_video = virtualcam_output_raw_video,
|
||||
};
|
||||
|
||||
bool obs_module_load(void)
|
||||
{
|
||||
blog(LOG_INFO, "version=%s", PLUGIN_VERSION);
|
||||
|
||||
obs_register_output(&virtualcam_output_info);
|
||||
|
||||
obs_data_t *obs_settings = obs_data_create();
|
||||
obs_data_set_bool(obs_settings, "vcamEnabled", true);
|
||||
obs_apply_private_data(obs_settings);
|
||||
obs_data_release(obs_settings);
|
||||
|
||||
return true;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user