Browse Source

Add virtualcam plugin to OBS codebase

Co-authored-by: lvsti <[email protected]>
Co-authored-by: Sebastian Beckmann <[email protected]>
Co-authored-by: Stefan Huber <[email protected]>
Co-authored-by: Ryohei Ikegami <[email protected]>
Co-authored-by: Colin Dean <[email protected]>
Co-authored-by: Wolfgang Ladermann <[email protected]>
Co-authored-by: Simon Eves <[email protected]>
Co-authored-by: Colin Nelson <[email protected]>
Co-authored-by: Yoshimasa Niwa <[email protected]>
Co-authored-by: Michael Karliner <[email protected]>
Co-authored-by: Jason Grout <[email protected]>
Co-authored-by: Alfredo Inostroza <[email protected]>
Co-authored-by: Daniel Kennett <[email protected]>
Co-authored-by: Gary Ewan Park <[email protected]>
Co-authored-by: José Carlos Cieni Júnior <[email protected]>
John Boiles 5 years ago
parent
commit
2700db9ff9
34 changed files with 4682 additions and 0 deletions
  1. 3 0
      .github/workflows/main.yml
  2. 6 0
      CI/full-build-macos.sh
  3. 1 0
      plugins/CMakeLists.txt
  4. 2 0
      plugins/mac-virtualcam/CMakeLists.txt
  5. 0 0
      plugins/mac-virtualcam/data/locale/en-US.ini
  6. 17 0
      plugins/mac-virtualcam/src/common/MachProtocol.h
  7. 22 0
      plugins/mac-virtualcam/src/dal-plugin/CMSampleBufferUtils.h
  8. 187 0
      plugins/mac-virtualcam/src/dal-plugin/CMSampleBufferUtils.mm
  9. 108 0
      plugins/mac-virtualcam/src/dal-plugin/CMakeLists.txt
  10. 21 0
      plugins/mac-virtualcam/src/dal-plugin/Defines.h
  11. 34 0
      plugins/mac-virtualcam/src/dal-plugin/Device.h
  12. 295 0
      plugins/mac-virtualcam/src/dal-plugin/Device.mm
  13. 40 0
      plugins/mac-virtualcam/src/dal-plugin/Info.plist
  14. 32 0
      plugins/mac-virtualcam/src/dal-plugin/Logging.h
  15. 33 0
      plugins/mac-virtualcam/src/dal-plugin/MachClient.h
  16. 140 0
      plugins/mac-virtualcam/src/dal-plugin/MachClient.mm
  17. 62 0
      plugins/mac-virtualcam/src/dal-plugin/ObjectStore.h
  18. 281 0
      plugins/mac-virtualcam/src/dal-plugin/ObjectStore.mm
  19. 51 0
      plugins/mac-virtualcam/src/dal-plugin/PlugIn.h
  20. 255 0
      plugins/mac-virtualcam/src/dal-plugin/PlugIn.mm
  21. 23 0
      plugins/mac-virtualcam/src/dal-plugin/PlugInInterface.h
  22. 444 0
      plugins/mac-virtualcam/src/dal-plugin/PlugInInterface.mm
  23. 37 0
      plugins/mac-virtualcam/src/dal-plugin/PlugInMain.mm
  24. 48 0
      plugins/mac-virtualcam/src/dal-plugin/Stream.h
  25. 571 0
      plugins/mac-virtualcam/src/dal-plugin/Stream.mm
  26. 14 0
      plugins/mac-virtualcam/src/dal-plugin/TestCard.h
  27. 1452 0
      plugins/mac-virtualcam/src/dal-plugin/TestCard.mm
  28. BIN
      plugins/mac-virtualcam/src/dal-plugin/placeholder.png
  29. 59 0
      plugins/mac-virtualcam/src/obs-plugin/CMakeLists.txt
  30. 24 0
      plugins/mac-virtualcam/src/obs-plugin/Defines.h
  31. 29 0
      plugins/mac-virtualcam/src/obs-plugin/MachServer.h
  32. 178 0
      plugins/mac-virtualcam/src/obs-plugin/MachServer.mm
  33. 5 0
      plugins/mac-virtualcam/src/obs-plugin/data/locale/en-US.ini
  34. 208 0
      plugins/mac-virtualcam/src/obs-plugin/plugin-main.mm

+ 3 - 0
.github/workflows/main.yml

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

+ 6 - 0
CI/full-build-macos.sh

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

+ 1 - 0
plugins/CMakeLists.txt

@@ -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 - 0
plugins/mac-virtualcam/CMakeLists.txt

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

+ 0 - 0
plugins/mac-virtualcam/data/locale/en-US.ini


+ 17 - 0
plugins/mac-virtualcam/src/common/MachProtocol.h

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/CMSampleBufferUtils.h

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/CMSampleBufferUtils.mm

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/CMakeLists.txt

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/Defines.h

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/Device.h

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/Device.mm

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/Info.plist

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/Logging.h

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/MachClient.h

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/MachClient.mm

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

+ 62 - 0
plugins/mac-virtualcam/src/dal-plugin/ObjectStore.h

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/ObjectStore.mm

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/PlugIn.h

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/PlugIn.mm

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/PlugInInterface.h

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/PlugInInterface.mm

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/PlugInMain.mm

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/Stream.h

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/Stream.mm

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/TestCard.h

@@ -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 - 0
plugins/mac-virtualcam/src/dal-plugin/TestCard.mm

@@ -0,0 +1,1452 @@
+//
+//  TestCard.m
+//  dal-plugin
+//
+//  Created by John Boiles  on 5/8/20.
+//
+
+#import "TestCard.h"
+
+#import <AppKit/AppKit.h>
+#import "Defines.h"
+
+// This code was generated by Trial version of PaintCode, therefore cannot be used for commercial purposes.
+// http://www.paintcodeapp.com
+
+void DrawTestCardWithFrame(CGContextRef context, NSRect frame)
+{
+	if (context == NULL) {
+		context = NSGraphicsContext.currentContext.CGContext;
+	}
+
+	BOOL showText = (frame.size.width >= 1280 && frame.size.height >= 720);
+
+	CGFloat centerWidth = floor(frame.size.width * 0.70840 - 0.25) -
+			      floor(frame.size.width * 0.29199 - 0.25);
+	NSRect center = NSMakeRect(
+		NSMinX(frame) + floor(frame.size.width * 0.29199 - 0.25) + 0.75,
+		NSMinY(frame) + floor((frame.size.height - centerWidth) / 2),
+		centerWidth, centerWidth);
+	NSString *versionText = PLUGIN_VERSION;
+
+	// Paste in PaintCode code below
+
+	//// Color Declarations
+	NSColor *fillColor = [NSColor colorWithRed:0.125
+					     green:0.176
+					      blue:0.435
+					     alpha:1];
+	NSColor *fillColor2 = [NSColor colorWithRed:0.086
+					      green:0.141
+					       blue:0.345
+					      alpha:1];
+	NSColor *fillColor3 = [NSColor colorWithRed:0.047
+					      green:0.086
+					       blue:0.2
+					      alpha:1];
+	NSColor *strokeColor = [NSColor colorWithRed:1 green:1 blue:1 alpha:1];
+	NSColor *fillColor4 = [NSColor colorWithRed:0
+					      green:0
+					       blue:0
+					      alpha:0.62];
+	NSColor *fillColor5 = [NSColor colorWithRed:0.188
+					      green:0.18
+					       blue:0.192
+					      alpha:1];
+	NSColor *fillColor6 = [NSColor colorWithRed:0.769
+					      green:0.761
+					       blue:0.769
+					      alpha:1];
+	NSColor *textForeground = [NSColor colorWithRed:1
+						  green:1
+						   blue:1
+						  alpha:1];
+	NSColor *fillColor7 = [NSColor colorWithRed:1 green:1 blue:1 alpha:1];
+	NSColor *fillColor8 = [NSColor colorWithRed:0
+					      green:0
+					       blue:0.753
+					      alpha:1];
+	NSColor *fillColor9 = [NSColor colorWithRed:0
+					      green:0.753
+					       blue:0
+					      alpha:1];
+	NSColor *fillColor10 = [NSColor colorWithRed:0.753
+					       green:0
+						blue:0
+					       alpha:1];
+
+	//// Subframes
+	NSRect background = NSMakeRect(NSMinX(frame), NSMinY(frame),
+				       frame.size.width, frame.size.height);
+	// NSRect center = NSMakeRect(NSMinX(frame) + floor(frame.size.width * 0.29199 - 0.25) + 0.75, NSMinY(frame) + floor(frame.size.height * 0.12917 + 0.5), floor(frame.size.width * 0.70840 - 0.25) - floor(frame.size.width * 0.29199 - 0.25), floor(frame.size.height * 0.86944 + 0.5) - floor(frame.size.height * 0.12917 + 0.5));
+	NSRect regularText = NSMakeRect(
+		NSMinX(frame) +
+			floor((frame.size.width - 274.23) * 0.04061 - 0.34) +
+			0.84,
+		NSMinY(frame) +
+			floor((frame.size.height - 352.53) * 0.42711 - 0.45) +
+			0.95,
+		274.23, 352.53);
+	NSRect rGB = NSMakeRect(
+		NSMinX(frame), NSMinY(frame) - 0.98,
+		floor((frame.size.width) * 0.03223 + 0.24) + 0.26,
+		floor((frame.size.height + 0.98) * 0.02185 - 1.25) + 1.75);
+	NSRect topRight = NSMakeRect(NSMinX(frame) + frame.size.width - 93.46,
+				     NSMinY(frame) + 0.04, 93.42, 93.52);
+	NSRect bottomLeft = NSMakeRect(
+		NSMinX(frame), NSMinY(frame) + frame.size.height - 93.55, 93.42,
+		93.51);
+
+	//// Background
+	{
+		//// Rectangle Drawing
+		NSBezierPath *rectanglePath = [NSBezierPath
+			bezierPathWithRect:
+				NSMakeRect(
+					NSMinX(background) +
+						floor(background.size.width *
+							      0.00000 +
+						      0.5),
+					NSMinY(background) +
+						floor(background.size.height *
+							      0.00000 +
+						      0.5),
+					floor(background.size.width * 1.00000 +
+					      0.5) -
+						floor(background.size.width *
+							      0.00000 +
+						      0.5),
+					floor(background.size.height * 0.24874 +
+					      0.41) -
+						floor(background.size.height *
+							      0.00000 +
+						      0.5) +
+						0.09)];
+		[fillColor setFill];
+		[rectanglePath fill];
+
+		//// Rectangle 2 Drawing
+		NSBezierPath *rectangle2Path = [NSBezierPath
+			bezierPathWithRect:
+				NSMakeRect(
+					NSMinX(background) +
+						floor(background.size.width *
+							      0.00000 +
+						      0.5),
+					NSMinY(background) +
+						floor(background.size.height *
+							      0.24874 +
+						      0.41) +
+						0.09,
+					floor(background.size.width * 1.00000 +
+					      0.5) -
+						floor(background.size.width *
+							      0.00000 +
+						      0.5),
+					floor(background.size.height * 1.00000 +
+					      0.5) -
+						floor(background.size.height *
+							      0.24874 +
+						      0.41) -
+						0.09)];
+		[fillColor setFill];
+		[rectangle2Path fill];
+
+		//// Bezier Drawing
+		NSBezierPath *bezierPath = [NSBezierPath bezierPath];
+		[bezierPath
+			moveToPoint:NSMakePoint(
+					    NSMinX(background) +
+						    1.00000 * background.size
+								      .width,
+					    NSMinY(background) +
+						    0.49645 * background.size
+								      .height)];
+		[bezierPath
+			 curveToPoint:NSMakePoint(
+					      NSMinX(background) +
+						      0.71753 * background.size
+									.width,
+					      NSMinY(background) +
+						      0.62275 * background.size
+									.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(background) +
+						      1.00000 * background.size
+									.width,
+					      NSMinY(background) +
+						      0.48916 * background.size
+									.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(background) +
+						      0.71753 * background.size
+									.width,
+					      NSMinY(background) +
+						      0.62275 *
+							      background.size
+								      .height)];
+		[bezierPath
+			 curveToPoint:NSMakePoint(
+					      NSMinX(background) +
+						      0.00000 * background.size
+									.width,
+					      NSMinY(background) +
+						      0.28446 * background.size
+									.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(background) +
+						      0.71753 * background.size
+									.width,
+					      NSMinY(background) +
+						      0.62275 * background.size
+									.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(background) +
+						      0.15141 * background.size
+									.width,
+					      NSMinY(background) +
+						      0.36204 *
+							      background.size
+								      .height)];
+		[bezierPath
+			lineToPoint:NSMakePoint(
+					    NSMinX(background) +
+						    0.00000 * background.size
+								      .width,
+					    NSMinY(background) +
+						    0.24823 * background.size
+								      .height)];
+		[bezierPath
+			lineToPoint:NSMakePoint(
+					    NSMinX(background) +
+						    1.00000 * background.size
+								      .width,
+					    NSMinY(background) +
+						    0.24823 * background.size
+								      .height)];
+		[bezierPath
+			lineToPoint:NSMakePoint(
+					    NSMinX(background) +
+						    1.00000 * background.size
+								      .width,
+					    NSMinY(background) +
+						    0.49645 * background.size
+								      .height)];
+		[bezierPath closePath];
+		[fillColor2 setFill];
+		[bezierPath fill];
+
+		//// Bezier 2 Drawing
+		NSBezierPath *bezier2Path = [NSBezierPath bezierPath];
+		[bezier2Path
+			moveToPoint:NSMakePoint(
+					    NSMinX(background) +
+						    1.00000 * background.size
+								      .width,
+					    NSMinY(background) +
+						    0.49645 * background.size
+								      .height)];
+		[bezier2Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(background) +
+						    1.00000 * background.size
+								      .width,
+					    NSMinY(background) +
+						    1.00000 * background.size
+								      .height)];
+		[bezier2Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(background) +
+						    0.00000 * background.size
+								      .width,
+					    NSMinY(background) +
+						    1.00000 * background.size
+								      .height)];
+		[bezier2Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(background) +
+						    0.00000 * background.size
+								      .width,
+					    NSMinY(background) +
+						    0.78019 * background.size
+								      .height)];
+		[bezier2Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(background) +
+						    1.00000 * background.size
+								      .width,
+					    NSMinY(background) +
+						    0.49645 * background.size
+								      .height)];
+		[bezier2Path closePath];
+		[fillColor3 setFill];
+		[bezier2Path fill];
+	}
+
+	//// Center
+	{
+		//// Oval Drawing
+		NSBezierPath *ovalPath = [NSBezierPath
+			bezierPathWithOvalInRect:
+				NSMakeRect(NSMinX(center) +
+						   floor(center.size.width *
+								 0.00000 +
+							 0.5),
+					   NSMinY(center) +
+						   floor(center.size.height *
+								 0.00000 +
+							 0.5),
+					   floor(center.size.width * 1.00000 +
+						 0.5) -
+						   floor(center.size.width *
+								 0.00000 +
+							 0.5),
+					   floor(center.size.height * 1.00000 +
+						 0.5) -
+						   floor(center.size.height *
+								 0.00000 +
+							 0.5))];
+		[fillColor4 setFill];
+		[ovalPath fill];
+		[strokeColor setStroke];
+		ovalPath.lineWidth = 2;
+		[ovalPath stroke];
+
+		//// Rectangle 3 Drawing
+		NSBezierPath *rectangle3Path = [NSBezierPath
+			bezierPathWithRect:
+				NSMakeRect(NSMinX(center) +
+						   floor(center.size.width *
+								 0.15572 +
+							 0.5),
+					   NSMinY(center) +
+						   floor(center.size.height *
+								 0.15572 +
+							 0.5),
+					   floor(center.size.width * 0.84428 +
+						 0.5) -
+						   floor(center.size.width *
+								 0.15572 +
+							 0.5),
+					   floor(center.size.height * 0.84428 +
+						 0.5) -
+						   floor(center.size.height *
+								 0.15572 +
+							 0.5))];
+		[strokeColor setStroke];
+		rectangle3Path.lineWidth = 2;
+		[rectangle3Path stroke];
+
+		//// Oval 2 Drawing
+		NSBezierPath *oval2Path = [NSBezierPath
+			bezierPathWithOvalInRect:
+				NSMakeRect(NSMinX(center) +
+						   floor(center.size.width *
+								 0.37715 +
+							 0.48) +
+						   0.02,
+					   NSMinY(center) +
+						   floor(center.size.height *
+								 0.37715 +
+							 0.48) +
+						   0.02,
+					   floor(center.size.width * 0.62285 -
+						 0.48) -
+						   floor(center.size.width *
+								 0.37715 +
+							 0.48) +
+						   0.96,
+					   floor(center.size.height * 0.62285 -
+						 0.48) -
+						   floor(center.size.height *
+								 0.37715 +
+							 0.48) +
+						   0.96)];
+		[fillColor5 setFill];
+		[oval2Path fill];
+		[strokeColor setStroke];
+		oval2Path.lineWidth = 3.47;
+		[oval2Path stroke];
+
+		//// Bezier 3 Drawing
+		NSBezierPath *bezier3Path = [NSBezierPath bezierPath];
+		[bezier3Path
+			moveToPoint:NSMakePoint(
+					    NSMinX(center) +
+						    0.43774 * center.size.width,
+					    NSMinY(center) +
+						    0.43569 *
+							    center.size.height)];
+		[bezier3Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(center) +
+						    0.43777 * center.size.width,
+					    NSMinY(center) +
+						    0.43553 *
+							    center.size.height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.47053 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.39283 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.44169 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.41708 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.45373 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.40139 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.46134 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.40126 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.46749 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.39584 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.46401 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.39809 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(center) +
+						    0.46106 * center.size.width,
+					    NSMinY(center) +
+						    0.40157 *
+							    center.size.height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.44863 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.44638 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.45011 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.41373 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.44551 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.43032 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.49915 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.48885 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.45226 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.46965 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.47476 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.48919 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.54568 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.46244 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.51803 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.48969 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.53644 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.47885 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(center) +
+						    0.54557 * center.size.width,
+					    NSMinY(center) +
+						    0.46243 *
+							    center.size.height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.59607 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.48985 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.56578 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.46314 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.58447 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.47329 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.60657 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.51871 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.60175 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.49825 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.60626 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.50827 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(center) +
+						    0.60649 * center.size.width,
+					    NSMinY(center) +
+						    0.51837 *
+							    center.size.height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.58067 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.48533 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.60272 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.50431 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.59340 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.49239 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(center) +
+						    0.58087 * center.size.width,
+					    NSMinY(center) +
+						    0.48544 *
+							    center.size.height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.50993 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.50579 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.55566 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.47147 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.52390 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.48058 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.50541 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.51670 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.50801 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.50924 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.50649 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.51291 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(center) +
+						    0.50550 * center.size.width,
+					    NSMinY(center) +
+						    0.51637 *
+							    center.size.height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.50977 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.55699 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.50161 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.52995 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.50314 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.54451 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(center) +
+						    0.50940 * center.size.width,
+					    NSMinY(center) +
+						    0.55691 *
+							    center.size.height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.46769 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.58611 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.50026 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.57211 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.48510 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.58273 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.42781 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.58115 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.45419 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.58894 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.44020 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.58679 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.46195 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.58102 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.43890 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.58437 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.45094 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.58490 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(center) +
+						    0.46219 * center.size.width,
+					    NSMinY(center) +
+						    0.58094 *
+							    center.size.height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.49448 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.54921 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.47719 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.57578 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.48905 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.56412 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.48799 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.50104 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.50027 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.53367 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.49801 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.51472 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(center) +
+						    0.48819 * center.size.width,
+					    NSMinY(center) +
+						    0.50132 *
+							    center.size.height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.45603 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.48003 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.48055 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.49041 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.46906 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.48280 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.44359 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.47857 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.45180 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.47927 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.44769 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.47895 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			 curveToPoint:NSMakePoint(
+					      NSMinX(center) +
+						      0.43794 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.43581 *
+							      center.size.height)
+			controlPoint1:NSMakePoint(
+					      NSMinX(center) +
+						      0.43705 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.46540 *
+							      center.size.height)
+			controlPoint2:NSMakePoint(
+					      NSMinX(center) +
+						      0.43465 *
+							      center.size.width,
+					      NSMinY(center) +
+						      0.45011 *
+							      center.size
+								      .height)];
+		[bezier3Path
+			lineToPoint:NSMakePoint(
+					    NSMinX(center) +
+						    0.43774 * center.size.width,
+					    NSMinY(center) +
+						    0.43569 *
+							    center.size.height)];
+		[bezier3Path closePath];
+		[fillColor6 setFill];
+		[bezier3Path fill];
+	}
+
+	if (showText) {
+		//// MirroredText
+		{
+			[NSGraphicsContext saveGraphicsState];
+			CGContextTranslateCTM(
+				context,
+				NSMinX(frame) + 0.96057 * frame.size.width,
+				NSMinY(frame) + 0.42824 * frame.size.height);
+			CGContextScaleCTM(context, -1, 1);
+
+			//// Label Drawing
+			NSRect labelRect =
+				NSMakeRect(-0.15, -30.85, 264.59, 40);
+			NSMutableParagraphStyle *labelStyle =
+				[[NSMutableParagraphStyle alloc] init];
+			labelStyle.alignment = NSTextAlignmentLeft;
+			NSDictionary *labelFontAttributes = @{
+				NSFontAttributeName:
+					[NSFont fontWithName:@"Helvetica-Bold"
+							size:32],
+				NSForegroundColorAttributeName: textForeground,
+				NSParagraphStyleAttributeName: labelStyle
+			};
+
+			[@"OBS Virtual Cam "
+				    drawInRect:NSOffsetRect(labelRect, 0, 0)
+				withAttributes:labelFontAttributes];
+
+			//// Label 2 Drawing
+			NSRect label2Rect = NSMakeRect(-0.15, 7.75, 264.68, 40);
+			NSMutableParagraphStyle *label2Style =
+				[[NSMutableParagraphStyle alloc] init];
+			label2Style.alignment = NSTextAlignmentLeft;
+			NSDictionary *label2FontAttributes = @{
+				NSFontAttributeName:
+					[NSFont fontWithName:@"Helvetica-Bold"
+							size:32],
+				NSForegroundColorAttributeName: textForeground,
+				NSParagraphStyleAttributeName: label2Style
+			};
+
+			[@"is inactive."
+				    drawInRect:NSOffsetRect(label2Rect, 0, 0)
+				withAttributes:label2FontAttributes];
+
+			//// Label 3 Drawing
+			NSRect label3Rect =
+				NSMakeRect(-0.15, 84.95, 245.51, 39);
+			NSMutableParagraphStyle *label3Style =
+				[[NSMutableParagraphStyle alloc] init];
+			label3Style.alignment = NSTextAlignmentLeft;
+			NSDictionary *label3FontAttributes = @{
+				NSFontAttributeName:
+					[NSFont fontWithName:@"Helvetica-Bold"
+							size:32],
+				NSForegroundColorAttributeName: textForeground,
+				NSParagraphStyleAttributeName: label3Style
+			};
+
+			[@"Choose Tools > "
+				    drawInRect:NSOffsetRect(label3Rect, 0, 0)
+				withAttributes:label3FontAttributes];
+
+			//// Label 4 Drawing
+			NSRect label4Rect =
+				NSMakeRect(-0.15, 123.55, 269.53, 39);
+			NSMutableParagraphStyle *label4Style =
+				[[NSMutableParagraphStyle alloc] init];
+			label4Style.alignment = NSTextAlignmentLeft;
+			NSDictionary *label4FontAttributes = @{
+				NSFontAttributeName:
+					[NSFont fontWithName:@"Helvetica-Bold"
+							size:32],
+				NSForegroundColorAttributeName: textForeground,
+				NSParagraphStyleAttributeName: label4Style
+			};
+
+			[@"Start Virtual "
+				    drawInRect:NSOffsetRect(label4Rect, 0, 0)
+				withAttributes:label4FontAttributes];
+
+			//// Label 5 Drawing
+			NSRect label5Rect =
+				NSMakeRect(-0.15, 162.15, 126.45, 39);
+			NSMutableParagraphStyle *label5Style =
+				[[NSMutableParagraphStyle alloc] init];
+			label5Style.alignment = NSTextAlignmentLeft;
+			NSDictionary *label5FontAttributes = @{
+				NSFontAttributeName:
+					[NSFont fontWithName:@"Helvetica-Bold"
+							size:32],
+				NSForegroundColorAttributeName: textForeground,
+				NSParagraphStyleAttributeName: label5Style
+			};
+
+			[@"Camera." drawInRect:NSOffsetRect(label5Rect, 0, 0)
+				withAttributes:label5FontAttributes];
+
+			//// Label 6 Drawing
+			NSRect label6Rect =
+				NSMakeRect(-0.15, -152.38, 296.53, 81);
+			NSMutableParagraphStyle *label6Style =
+				[[NSMutableParagraphStyle alloc] init];
+			label6Style.alignment = NSTextAlignmentLeft;
+			NSDictionary *label6FontAttributes = @{
+				NSFontAttributeName:
+					[NSFont fontWithName:@"Helvetica-Bold"
+							size:66],
+				NSForegroundColorAttributeName: textForeground,
+				NSParagraphStyleAttributeName: label6Style
+			};
+
+			[@"Mirrored" drawInRect:NSOffsetRect(label6Rect, 0, 0)
+				 withAttributes:label6FontAttributes];
+
+			[NSGraphicsContext restoreGraphicsState];
+		}
+
+		//// RegularText
+		{
+			//// Label 7 Drawing
+			NSRect label7Rect = NSMakeRect(
+				NSMinX(regularText) + 4.7,
+				NSMinY(regularText) + 121.53, 264.59, 40);
+			NSMutableParagraphStyle *label7Style =
+				[[NSMutableParagraphStyle alloc] init];
+			label7Style.alignment = NSTextAlignmentLeft;
+			NSDictionary *label7FontAttributes = @{
+				NSFontAttributeName:
+					[NSFont fontWithName:@"Helvetica-Bold"
+							size:32],
+				NSForegroundColorAttributeName: textForeground,
+				NSParagraphStyleAttributeName: label7Style
+			};
+
+			[@"OBS Virtual Cam "
+				    drawInRect:NSOffsetRect(label7Rect, 0, 0)
+				withAttributes:label7FontAttributes];
+
+			//// Label 8 Drawing
+			NSRect label8Rect = NSMakeRect(
+				NSMinX(regularText) + 4.7,
+				NSMinY(regularText) + 160.13, 269.46, 39);
+			NSMutableParagraphStyle *label8Style =
+				[[NSMutableParagraphStyle alloc] init];
+			label8Style.alignment = NSTextAlignmentLeft;
+			NSDictionary *label8FontAttributes = @{
+				NSFontAttributeName:
+					[NSFont fontWithName:@"Helvetica-Bold"
+							size:32],
+				NSForegroundColorAttributeName: textForeground,
+				NSParagraphStyleAttributeName: label8Style
+			};
+
+			[@"is inactive."
+				    drawInRect:NSOffsetRect(label8Rect, 0, 0)
+				withAttributes:label8FontAttributes];
+
+			//// Label 9 Drawing
+			NSRect label9Rect = NSMakeRect(
+				NSMinX(regularText) + 4.7,
+				NSMinY(regularText) + 236.33, 245.51, 39);
+			NSMutableParagraphStyle *label9Style =
+				[[NSMutableParagraphStyle alloc] init];
+			label9Style.alignment = NSTextAlignmentLeft;
+			NSDictionary *label9FontAttributes = @{
+				NSFontAttributeName:
+					[NSFont fontWithName:@"Helvetica-Bold"
+							size:32],
+				NSForegroundColorAttributeName: textForeground,
+				NSParagraphStyleAttributeName: label9Style
+			};
+
+			[@"Choose Tools > "
+				    drawInRect:NSOffsetRect(label9Rect, 0, 0)
+				withAttributes:label9FontAttributes];
+
+			//// Label 10 Drawing
+			NSRect label10Rect = NSMakeRect(
+				NSMinX(regularText) + 4.7,
+				NSMinY(regularText) + 274.93, 269.53, 39);
+			NSMutableParagraphStyle *label10Style =
+				[[NSMutableParagraphStyle alloc] init];
+			label10Style.alignment = NSTextAlignmentLeft;
+			NSDictionary *label10FontAttributes = @{
+				NSFontAttributeName:
+					[NSFont fontWithName:@"Helvetica-Bold"
+							size:32],
+				NSForegroundColorAttributeName: textForeground,
+				NSParagraphStyleAttributeName: label10Style
+			};
+
+			[@"Start Virtual "
+				    drawInRect:NSOffsetRect(label10Rect, 0, 0)
+				withAttributes:label10FontAttributes];
+
+			//// Label 11 Drawing
+			NSRect label11Rect = NSMakeRect(
+				NSMinX(regularText) + 4.7,
+				NSMinY(regularText) + 313.53, 126.45, 39);
+			NSMutableParagraphStyle *label11Style =
+				[[NSMutableParagraphStyle alloc] init];
+			label11Style.alignment = NSTextAlignmentLeft;
+			NSDictionary *label11FontAttributes = @{
+				NSFontAttributeName:
+					[NSFont fontWithName:@"Helvetica-Bold"
+							size:32],
+				NSForegroundColorAttributeName: textForeground,
+				NSParagraphStyleAttributeName: label11Style
+			};
+
+			[@"Camera." drawInRect:NSOffsetRect(label11Rect, 0, 0)
+				withAttributes:label11FontAttributes];
+
+			//// Label 12 Drawing
+			NSRect label12Rect = NSMakeRect(NSMinX(regularText),
+							NSMinY(regularText),
+							248.31, 81);
+			NSMutableParagraphStyle *label12Style =
+				[[NSMutableParagraphStyle alloc] init];
+			label12Style.alignment = NSTextAlignmentLeft;
+			NSDictionary *label12FontAttributes = @{
+				NSFontAttributeName:
+					[NSFont fontWithName:@"Helvetica-Bold"
+							size:66],
+				NSForegroundColorAttributeName: textForeground,
+				NSParagraphStyleAttributeName: label12Style
+			};
+
+			[@"Regular" drawInRect:NSOffsetRect(label12Rect, 0, 0)
+				withAttributes:label12FontAttributes];
+		}
+	}
+
+	//// RGB
+	{
+		//// Blue Drawing
+		NSBezierPath *bluePath = [NSBezierPath
+			bezierPathWithRect:
+				NSMakeRect(
+					NSMinX(rGB) +
+						floor(rGB.size.width * 0.67475 -
+						      0.34) +
+						0.84,
+					NSMinY(rGB) + floor(rGB.size.height *
+								    0.00000 +
+							    0.5),
+					floor(rGB.size.width * 1.00000 + 0.24) -
+						floor(rGB.size.width * 0.67475 -
+						      0.34) -
+						0.58,
+					floor(rGB.size.height * 1.00000 - 0.25) -
+						floor(rGB.size.height * 0.00000 +
+						      0.5) +
+						0.75)];
+		[fillColor8 setFill];
+		[bluePath fill];
+
+		//// Green Drawing
+		NSBezierPath *greenPath = [NSBezierPath
+			bezierPathWithRect:
+				NSMakeRect(
+					NSMinX(rGB) +
+						floor(rGB.size.width * 0.32525 +
+						      0.08) +
+						0.42,
+					NSMinY(rGB) + floor(rGB.size.height *
+								    0.00000 +
+							    0.5),
+					floor(rGB.size.width * 0.67475 - 0.34) -
+						floor(rGB.size.width * 0.32525 +
+						      0.08) +
+						0.42,
+					floor(rGB.size.height * 1.00000 - 0.25) -
+						floor(rGB.size.height * 0.00000 +
+						      0.5) +
+						0.75)];
+		[fillColor9 setFill];
+		[greenPath fill];
+
+		//// Red Drawing
+		NSBezierPath *redPath = [NSBezierPath
+			bezierPathWithRect:
+				NSMakeRect(
+					NSMinX(rGB) +
+						floor(rGB.size.width * 0.00000 +
+						      0.5),
+					NSMinY(rGB) + floor(rGB.size.height *
+								    0.00000 +
+							    0.5),
+					floor(rGB.size.width * 0.32525 + 0.08) -
+						floor(rGB.size.width * 0.00000 +
+						      0.5) +
+						0.42,
+					floor(rGB.size.height * 1.00000 - 0.25) -
+						floor(rGB.size.height * 0.00000 +
+						      0.5) +
+						0.75)];
+		[fillColor10 setFill];
+		[redPath fill];
+	}
+
+	//// TopRight
+	{
+		//// Bezier 7 Drawing
+		NSBezierPath *bezier7Path = [NSBezierPath bezierPath];
+		[bezier7Path moveToPoint:NSMakePoint(NSMinX(topRight) + 31.28,
+						     NSMinY(topRight) + 54.38)];
+		[bezier7Path
+			 curveToPoint:NSMakePoint(NSMinX(topRight) + 23.82,
+						  NSMinY(topRight) + 61.91)
+			controlPoint1:NSMakePoint(NSMinX(topRight) + 26.23,
+						  NSMinY(topRight) + 55.91)
+			controlPoint2:NSMakePoint(NSMinX(topRight) + 25.34,
+						  NSMinY(topRight) + 56.81)];
+		[bezier7Path lineToPoint:NSMakePoint(NSMinX(topRight),
+						     NSMinY(topRight) + 61.91)];
+		[bezier7Path lineToPoint:NSMakePoint(NSMinX(topRight) + 30.91,
+						     NSMinY(topRight))];
+		[bezier7Path lineToPoint:NSMakePoint(NSMinX(topRight) + 31.28,
+						     NSMinY(topRight) + 0.08)];
+		[bezier7Path lineToPoint:NSMakePoint(NSMinX(topRight) + 31.28,
+						     NSMinY(topRight) + 54.38)];
+		[bezier7Path closePath];
+		[fillColor7 setFill];
+		[bezier7Path fill];
+
+		//// Bezier 8 Drawing
+		NSBezierPath *bezier8Path = [NSBezierPath bezierPath];
+		[bezier8Path moveToPoint:NSMakePoint(NSMinX(topRight) + 93.42,
+						     NSMinY(topRight) + 62.55)];
+		[bezier8Path lineToPoint:NSMakePoint(NSMinX(topRight) + 31.61,
+						     NSMinY(topRight) + 93.52)];
+		[bezier8Path lineToPoint:NSMakePoint(NSMinX(topRight) + 31.61,
+						     NSMinY(topRight) + 69.52)];
+		[bezier8Path
+			 curveToPoint:NSMakePoint(NSMinX(topRight) + 38.98,
+						  NSMinY(topRight) + 62.1)
+			controlPoint1:NSMakePoint(NSMinX(topRight) + 35.92,
+						  NSMinY(topRight) + 68.97)
+			controlPoint2:NSMakePoint(NSMinX(topRight) + 38.42,
+						  NSMinY(topRight) + 66.47)];
+		[bezier8Path lineToPoint:NSMakePoint(NSMinX(topRight) + 93.28,
+						     NSMinY(topRight) + 62.1)];
+		[bezier8Path lineToPoint:NSMakePoint(NSMinX(topRight) + 93.42,
+						     NSMinY(topRight) + 62.55)];
+		[bezier8Path closePath];
+		[fillColor7 setFill];
+		[bezier8Path fill];
+
+		//// Bezier 9 Drawing
+		NSBezierPath *bezier9Path = [NSBezierPath bezierPath];
+		[bezier9Path moveToPoint:NSMakePoint(NSMinX(topRight) + 31.54,
+						     NSMinY(topRight) + 65.21)];
+		[bezier9Path
+			 curveToPoint:NSMakePoint(NSMinX(topRight) + 28.38,
+						  NSMinY(topRight) + 62.07)
+			controlPoint1:NSMakePoint(NSMinX(topRight) + 29.66,
+						  NSMinY(topRight) + 64.99)
+			controlPoint2:NSMakePoint(NSMinX(topRight) + 28.39,
+						  NSMinY(topRight) + 64.01)];
+		[bezier9Path lineToPoint:NSMakePoint(NSMinX(topRight) + 28.38,
+						     NSMinY(topRight) + 62.05)];
+		[bezier9Path
+			 curveToPoint:NSMakePoint(NSMinX(topRight) + 31.52,
+						  NSMinY(topRight) + 59)
+			controlPoint1:NSMakePoint(NSMinX(topRight) + 28.41,
+						  NSMinY(topRight) + 60.34)
+			controlPoint2:NSMakePoint(NSMinX(topRight) + 29.82,
+						  NSMinY(topRight) + 58.97)];
+		[bezier9Path
+			 curveToPoint:NSMakePoint(NSMinX(topRight) + 34.56,
+						  NSMinY(topRight) + 61.94)
+			controlPoint1:NSMakePoint(NSMinX(topRight) + 33.15,
+						  NSMinY(topRight) + 59.03)
+			controlPoint2:NSMakePoint(NSMinX(topRight) + 34.47,
+						  NSMinY(topRight) + 60.31)];
+		[bezier9Path
+			 curveToPoint:NSMakePoint(NSMinX(topRight) + 31.54,
+						  NSMinY(topRight) + 65.21)
+			controlPoint1:NSMakePoint(NSMinX(topRight) + 34.65,
+						  NSMinY(topRight) + 63.82)
+			controlPoint2:NSMakePoint(NSMinX(topRight) + 33.42,
+						  NSMinY(topRight) + 64.86)];
+		[bezier9Path closePath];
+		[fillColor7 setFill];
+		[bezier9Path fill];
+	}
+
+	//// BottomLeft
+	{
+		//// Bezier 4 Drawing
+		NSBezierPath *bezier4Path = [NSBezierPath bezierPath];
+		[bezier4Path
+			moveToPoint:NSMakePoint(NSMinX(bottomLeft) + 62.14,
+						NSMinY(bottomLeft) + 39.13)];
+		[bezier4Path
+			 curveToPoint:NSMakePoint(NSMinX(bottomLeft) + 69.6,
+						  NSMinY(bottomLeft) + 31.6)
+			controlPoint1:NSMakePoint(NSMinX(bottomLeft) + 67.18,
+						  NSMinY(bottomLeft) + 37.6)
+			controlPoint2:NSMakePoint(NSMinX(bottomLeft) + 68.08,
+						  NSMinY(bottomLeft) + 36.71)];
+		[bezier4Path
+			lineToPoint:NSMakePoint(NSMinX(bottomLeft) + 93.42,
+						NSMinY(bottomLeft) + 31.6)];
+		[bezier4Path
+			lineToPoint:NSMakePoint(NSMinX(bottomLeft) + 62.51,
+						NSMinY(bottomLeft) + 93.51)];
+		[bezier4Path
+			lineToPoint:NSMakePoint(NSMinX(bottomLeft) + 62.14,
+						NSMinY(bottomLeft) + 93.43)];
+		[bezier4Path
+			lineToPoint:NSMakePoint(NSMinX(bottomLeft) + 62.14,
+						NSMinY(bottomLeft) + 39.13)];
+		[bezier4Path closePath];
+		[fillColor7 setFill];
+		[bezier4Path fill];
+
+		//// Bezier 5 Drawing
+		NSBezierPath *bezier5Path = [NSBezierPath bezierPath];
+		[bezier5Path
+			moveToPoint:NSMakePoint(NSMinX(bottomLeft),
+						NSMinY(bottomLeft) + 30.96)];
+		[bezier5Path lineToPoint:NSMakePoint(NSMinX(bottomLeft) + 61.81,
+						     NSMinY(bottomLeft))];
+		[bezier5Path
+			lineToPoint:NSMakePoint(NSMinX(bottomLeft) + 61.81,
+						NSMinY(bottomLeft) + 24.02)];
+		[bezier5Path
+			 curveToPoint:NSMakePoint(NSMinX(bottomLeft) + 54.44,
+						  NSMinY(bottomLeft) + 31.44)
+			controlPoint1:NSMakePoint(NSMinX(bottomLeft) + 57.49,
+						  NSMinY(bottomLeft) + 24.57)
+			controlPoint2:NSMakePoint(NSMinX(bottomLeft) + 54.99,
+						  NSMinY(bottomLeft) + 27.07)];
+		[bezier5Path
+			lineToPoint:NSMakePoint(NSMinX(bottomLeft) + 0.14,
+						NSMinY(bottomLeft) + 31.44)];
+		[bezier5Path
+			lineToPoint:NSMakePoint(NSMinX(bottomLeft),
+						NSMinY(bottomLeft) + 30.96)];
+		[bezier5Path closePath];
+		[fillColor7 setFill];
+		[bezier5Path fill];
+
+		//// Bezier 6 Drawing
+		NSBezierPath *bezier6Path = [NSBezierPath bezierPath];
+		[bezier6Path
+			moveToPoint:NSMakePoint(NSMinX(bottomLeft) + 61.88,
+						NSMinY(bottomLeft) + 28.3)];
+		[bezier6Path
+			 curveToPoint:NSMakePoint(NSMinX(bottomLeft) + 65.04,
+						  NSMinY(bottomLeft) + 31.45)
+			controlPoint1:NSMakePoint(NSMinX(bottomLeft) + 63.75,
+						  NSMinY(bottomLeft) + 28.53)
+			controlPoint2:NSMakePoint(NSMinX(bottomLeft) + 65.03,
+						  NSMinY(bottomLeft) + 29.51)];
+		[bezier6Path
+			lineToPoint:NSMakePoint(NSMinX(bottomLeft) + 65.04,
+						NSMinY(bottomLeft) + 31.47)];
+		[bezier6Path
+			 curveToPoint:NSMakePoint(NSMinX(bottomLeft) + 61.89,
+						  NSMinY(bottomLeft) + 34.52)
+			controlPoint1:NSMakePoint(NSMinX(bottomLeft) + 65.01,
+						  NSMinY(bottomLeft) + 33.18)
+			controlPoint2:NSMakePoint(NSMinX(bottomLeft) + 63.6,
+						  NSMinY(bottomLeft) + 34.55)];
+		[bezier6Path
+			 curveToPoint:NSMakePoint(NSMinX(bottomLeft) + 58.86,
+						  NSMinY(bottomLeft) + 31.58)
+			controlPoint1:NSMakePoint(NSMinX(bottomLeft) + 60.27,
+						  NSMinY(bottomLeft) + 34.49)
+			controlPoint2:NSMakePoint(NSMinX(bottomLeft) + 58.95,
+						  NSMinY(bottomLeft) + 33.21)];
+		[bezier6Path
+			 curveToPoint:NSMakePoint(NSMinX(bottomLeft) + 61.88,
+						  NSMinY(bottomLeft) + 28.3)
+			controlPoint1:NSMakePoint(NSMinX(bottomLeft) + 58.77,
+						  NSMinY(bottomLeft) + 29.7)
+			controlPoint2:NSMakePoint(NSMinX(bottomLeft) + 60,
+						  NSMinY(bottomLeft) + 28.66)];
+		[bezier6Path closePath];
+		[fillColor7 setFill];
+		[bezier6Path fill];
+	}
+
+	//// Text Drawing
+	NSRect textRect = NSMakeRect(
+		NSMinX(frame) + floor(frame.size.width * 0.42422 + 0.5),
+		NSMinY(frame) + frame.size.height - 20,
+		floor(frame.size.width * 0.57656 + 0.5) -
+			floor(frame.size.width * 0.42422 + 0.5),
+		21);
+	NSMutableParagraphStyle *textStyle =
+		[[NSMutableParagraphStyle alloc] init];
+	textStyle.alignment = NSTextAlignmentCenter;
+	NSDictionary *textFontAttributes = @{
+		NSFontAttributeName: [NSFont systemFontOfSize:14],
+		NSForegroundColorAttributeName: fillColor6,
+		NSParagraphStyleAttributeName: textStyle
+	};
+
+	CGFloat textTextHeight =
+		[versionText
+			boundingRectWithSize:textRect.size
+				     options:NSStringDrawingUsesLineFragmentOrigin
+				  attributes:textFontAttributes]
+			.size.height;
+	NSRect textTextRect = NSMakeRect(
+		NSMinX(textRect),
+		NSMinY(textRect) + (textRect.size.height - textTextHeight) / 2,
+		textRect.size.width, textTextHeight);
+	[NSGraphicsContext saveGraphicsState];
+	NSRectClip(textRect);
+	[versionText drawInRect:NSOffsetRect(textTextRect, 0, -0.5)
+		 withAttributes:textFontAttributes];
+	[NSGraphicsContext restoreGraphicsState];
+}
+
+void DrawDialWithFrame(NSRect frame, CGFloat rotation)
+{
+	//// General Declarations
+	CGContextRef context = NSGraphicsContext.currentContext.CGContext;
+
+	//// Oval 3 Drawing
+	NSBezierPath *oval3Path = [NSBezierPath
+		bezierPathWithOvalInRect:NSMakeRect(
+						 NSMinX(frame) +
+							 frame.size.width - 133,
+						 NSMinY(frame) + 30, 98, 98)];
+	[NSColor.grayColor setFill];
+	[oval3Path fill];
+
+	//// Bezier 10 Drawing
+	[NSGraphicsContext saveGraphicsState];
+	CGContextTranslateCTM(context, NSMaxX(frame) - 83.5,
+			      NSMinY(frame) + 79.5);
+	CGContextRotateCTM(context, rotation * M_PI / 180);
+
+	NSBezierPath *bezier10Path = [NSBezierPath bezierPath];
+	[bezier10Path moveToPoint:NSMakePoint(-0, -0)];
+	[bezier10Path lineToPoint:NSMakePoint(-0, 48)];
+	[NSColor.blackColor setStroke];
+	bezier10Path.lineWidth = 2;
+	[bezier10Path stroke];
+
+	[NSGraphicsContext restoreGraphicsState];
+}
+
+NSImage *ImageOfTestCardWithSize(NSSize imageSize)
+{
+	return [NSImage imageWithSize:imageSize
+			      flipped:YES
+		       drawingHandler:^(__unused NSRect dstRect) {
+			       DrawTestCardWithFrame(
+				       nil, NSMakeRect(0, 0, imageSize.width,
+						       imageSize.height));
+			       return YES;
+		       }];
+}

BIN
plugins/mac-virtualcam/src/dal-plugin/placeholder.png


+ 59 - 0
plugins/mac-virtualcam/src/obs-plugin/CMakeLists.txt

@@ -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 - 0
plugins/mac-virtualcam/src/obs-plugin/Defines.h

@@ -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 - 0
plugins/mac-virtualcam/src/obs-plugin/MachServer.h

@@ -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 - 0
plugins/mac-virtualcam/src/obs-plugin/MachServer.mm

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

+ 5 - 0
plugins/mac-virtualcam/src/obs-plugin/data/locale/en-US.ini

@@ -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 - 0
plugins/mac-virtualcam/src/obs-plugin/plugin-main.mm

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