Browse Source

mac-virtualcam: Add macOS camera extension project

Co-authored-by: PatTheMav <[email protected]>
gxalpha 2 years ago
parent
commit
5c6e471a56

+ 50 - 0
plugins/mac-virtualcam/src/camera-extension/CMakeLists.txt

@@ -0,0 +1,50 @@
+foreach(_uuid IN ITEMS VIRTUALCAM_DEVICE_UUID VIRTUALCAM_SOURCE_UUID VIRTUALCAM_SINK_UUID)
+  set(VALID_UUID FALSE)
+  if(NOT ${_uuid})
+    message(AUTHOR_WARNING "macOS Camera Extension UUID '${_uuid}' is not set, but required for extension.")
+    return()
+  else()
+    check_uuid(${${_uuid}} VALID_UUID)
+
+    if(NOT VALID_UUID)
+      message(AUTHOR_WARNING "macos Camera Extension UUID '${_uuid}' is not a valid UUID.")
+      return()
+    endif()
+  endif()
+endforeach()
+
+project(mac-camera-extension LANGUAGES Swift)
+
+set(_ORIG_DEPLOYMENT_TARGET ${CMAKE_OSX_DEPLOYMENT_TARGET})
+
+set(CMAKE_OSX_DEPLOYMENT_TARGET 13.0)
+
+add_executable(mac-camera-extension)
+add_executable(OBS:mac-camera-extension ALIAS mac-camera-extension)
+
+set(_placeholder_location "${CMAKE_CURRENT_SOURCE_DIR}/../common/data/placeholder.png")
+target_sources(
+  mac-camera-extension PRIVATE main.swift OBSCameraDeviceSource.swift OBSCameraProviderSource.swift
+                               OBSCameraStreamSink.swift OBSCameraStreamSource.swift "${_placeholder_location}")
+
+set_property(SOURCE "${_placeholder_location}" PROPERTY MACOSX_PACKAGE_LOCATION "Resources")
+source_group("Resources" FILES "${_placeholder_location}")
+
+set_target_properties(
+  mac-camera-extension
+  PROPERTIES BUNDLE_EXTENSION systemextension
+             RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/../../"
+             MACOSX_BUNDLE ON
+             MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/Info.plist.in"
+             XCODE_PRODUCT_TYPE com.apple.product-type.system-extension
+             XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/entitlements.plist"
+             XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES
+             XCODE_ATTRIBUTE_CODE_SIGN_INJECT_BASE_ENTITLEMENTS NO
+             XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS "--timestamp"
+             XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET 13.0
+             XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.obsproject.obs-studio.mac-camera-extension"
+             XCODE_ATTRIBUTE_SWIFT_VERSION 5
+             XCODE_ATTRIBUTE_SKIP_INSTALL YES)
+
+set_target_properties_obs(mac-camera-extension PROPERTIES FOLDER plugins
+                                                          OUTPUT_NAME "com.obsproject.obs-studio.mac-camera-extension")

+ 288 - 0
plugins/mac-virtualcam/src/camera-extension/OBSCameraDeviceSource.swift

@@ -0,0 +1,288 @@
+//
+//  OBSCameraDeviceSource.swift
+//  camera-extension
+//
+//  Created by Sebastian Beckmann on 2022-09-30.
+//  Changed by Patrick Heyer on 2022-10-16.
+//
+
+import AppKit
+import CoreMediaIO
+import Foundation
+import IOKit.audio
+import os.log
+
+let OBSCameraFrameRate: Int = 60
+
+class OBSCameraDeviceSource: NSObject, CMIOExtensionDeviceSource {
+    private(set) var device: CMIOExtensionDevice!
+
+    private var _streamSource: OBSCameraStreamSource!
+    private var _streamSink: OBSCameraStreamSink!
+
+    private var _streamingCounter: UInt32 = 0
+    private var _streamingSinkCounter: UInt32 = 0
+
+    private var _timer: DispatchSourceTimer?
+
+    private let _timerQueue = DispatchQueue(
+        label: "timerQueue",
+        qos: .userInteractive,
+        attributes: [],
+        autoreleaseFrequency: .workItem,
+        target: .global(qos: .userInteractive)
+    )
+
+    private var _videoDescription: CMFormatDescription!
+    private var _bufferPool: CVPixelBufferPool!
+    private var _bufferAuxAttributes: NSDictionary!
+
+    private var _placeholderImage: NSImage!
+
+    init(localizedName: String, deviceUUID: UUID, sourceUUID: UUID, sinkUUID: UUID) {
+        super.init()
+
+        self.device = CMIOExtensionDevice(
+            localizedName: localizedName,
+            deviceID: deviceUUID,
+            legacyDeviceID: nil,
+            source: self
+        )
+
+        let dimensions = CMVideoDimensions(width: 1920, height: 1080)
+        CMVideoFormatDescriptionCreate(
+            allocator: kCFAllocatorDefault,
+            codecType: kCVPixelFormatType_32ARGB,
+            width: dimensions.width,
+            height: dimensions.height,
+            extensions: nil,
+            formatDescriptionOut: &_videoDescription
+        )
+
+        let pixelBufferAttributes: NSDictionary = [
+            kCVPixelBufferWidthKey: dimensions.width,
+            kCVPixelBufferHeightKey: dimensions.height,
+            kCVPixelBufferPixelFormatTypeKey: _videoDescription.mediaSubType,
+            kCVPixelBufferIOSurfacePropertiesKey: [:],
+        ]
+
+        CVPixelBufferPoolCreate(kCFAllocatorDefault, nil, pixelBufferAttributes, &_bufferPool)
+
+        let videoStreamFormat = CMIOExtensionStreamFormat.init(
+            formatDescription: _videoDescription,
+            maxFrameDuration: CMTime(value: 1, timescale: Int32(OBSCameraFrameRate)),
+            minFrameDuration: CMTime(value: 1, timescale: Int32(OBSCameraFrameRate)),
+            validFrameDurations: nil
+        )
+        _bufferAuxAttributes = [kCVPixelBufferPoolAllocationThresholdKey: 5]
+
+        _streamSource = OBSCameraStreamSource(
+            localizedName: "OBS Camera Extension Stream Source",
+            streamID: sourceUUID,
+            streamFormat: videoStreamFormat,
+            device: device
+        )
+
+        _streamSink = OBSCameraStreamSink(
+            localizedName: "OBS Camera Extension Stream Sink",
+            streamID: sinkUUID,
+            streamFormat: videoStreamFormat,
+            device: device
+        )
+
+        do {
+            try device.addStream(_streamSource.stream)
+            try device.addStream(_streamSink.stream)
+        } catch let error {
+            fatalError("Failed to add stream: \(error.localizedDescription)")
+        }
+
+        let placeholderURL = Bundle.main.url(forResource: "placeholder", withExtension: "png")
+        if let placeholderURL = placeholderURL {
+            if let image = NSImage(contentsOf: placeholderURL) {
+                _placeholderImage = image
+            } else {
+                fatalError("Unable to create NSImage from placeholder image in bundle resources")
+            }
+        } else {
+            fatalError("Unable to find placeholder image in bundle resources")
+        }
+    }
+
+    var availableProperties: Set<CMIOExtensionProperty> {
+        return [.deviceTransportType, .deviceModel]
+    }
+
+    func deviceProperties(forProperties properties: Set<CMIOExtensionProperty>) throws
+        -> CMIOExtensionDeviceProperties
+    {
+        let deviceProperties = CMIOExtensionDeviceProperties(dictionary: [:])
+        if properties.contains(.deviceTransportType) {
+            deviceProperties.transportType = kIOAudioDeviceTransportTypeVirtual
+        }
+        if properties.contains(.deviceModel) {
+            deviceProperties.model = "OBS Camera Extension"
+        }
+
+        return deviceProperties
+    }
+
+    func setDeviceProperties(_ deviceProperties: CMIOExtensionDeviceProperties) throws {
+    }
+
+    func startStreaming() {
+        guard let _ = _bufferPool else {
+            return
+        }
+
+        _streamingCounter += 1
+
+        _timer = DispatchSource.makeTimerSource(flags: .strict, queue: _timerQueue)
+
+        _timer!.schedule(
+            deadline: .now(),
+            repeating: Double(1 / OBSCameraFrameRate),
+            leeway: .seconds(0)
+        )
+
+        _timer!.setEventHandler {
+            if self.sinkStarted {
+                return
+            }
+
+            var error: CVReturn = noErr
+            var pixelBuffer: CVPixelBuffer?
+            error = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(
+                kCFAllocatorDefault,
+                self._bufferPool,
+                self._bufferAuxAttributes,
+                &pixelBuffer
+            )
+
+            if error == kCVReturnPoolAllocationFailed {
+                os_log(.error, "no available PixelBuffers in PixelBufferPool: \(error)")
+            }
+
+            if let pixelBuffer = pixelBuffer {
+                CVPixelBufferLockBaseAddress(pixelBuffer, [])
+
+                let bufferPointer = CVPixelBufferGetBaseAddress(pixelBuffer)!
+
+                let width = CVPixelBufferGetWidth(pixelBuffer)
+                let height = CVPixelBufferGetHeight(pixelBuffer)
+                let rowBytes = CVPixelBufferGetBytesPerRow(pixelBuffer)
+
+                let cgContext = CGContext(
+                    data: bufferPointer,
+                    width: width,
+                    height: height,
+                    bitsPerComponent: 8,
+                    bytesPerRow: rowBytes,
+                    space: CGColorSpaceCreateDeviceRGB(),
+                    bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue
+                )!
+                let graphicsContext = NSGraphicsContext(cgContext: cgContext, flipped: false)
+
+                NSGraphicsContext.saveGraphicsState()
+                NSGraphicsContext.current = graphicsContext
+                self._placeholderImage.draw(in: CGRect(x: 0, y: 0, width: width, height: height))
+                NSGraphicsContext.restoreGraphicsState()
+
+                CVPixelBufferUnlockBaseAddress(pixelBuffer, [])
+
+                var sampleBuffer: CMSampleBuffer!
+                var timingInfo = CMSampleTimingInfo()
+
+                timingInfo.presentationTimeStamp = CMClockGetTime(CMClockGetHostTimeClock())
+
+                error = CMSampleBufferCreateForImageBuffer(
+                    allocator: kCFAllocatorDefault,
+                    imageBuffer: pixelBuffer,
+                    dataReady: true,
+                    makeDataReadyCallback: nil,
+                    refcon: nil,
+                    formatDescription: self._videoDescription,
+                    sampleTiming: &timingInfo,
+                    sampleBufferOut: &sampleBuffer
+                )
+
+                if error == noErr {
+                    self._streamSource.stream.send(
+                        sampleBuffer,
+                        discontinuity: [],
+                        hostTimeInNanoseconds: UInt64(
+                            timingInfo.presentationTimeStamp.seconds * Double(NSEC_PER_SEC)
+                        )
+                    )
+                }
+            }
+        }
+        _timer!.setCancelHandler {}
+
+        _timer!.resume()
+    }
+
+    func stopStreaming() {
+        if _streamingCounter > 1 {
+            _streamingCounter -= 1
+        } else {
+            _streamingCounter = 0
+
+            if let timer = _timer {
+                timer.cancel()
+                _timer = nil
+            }
+        }
+    }
+
+    var sinkStarted = false
+    var lastTimingInfo = CMSampleTimingInfo()
+
+    func consumeBuffer(_ client: CMIOExtensionClient) {
+        if !sinkStarted {
+            return
+        }
+
+        self._streamSink.stream.consumeSampleBuffer(from: client) {
+            sampleBuffer, sequenceNumber, discontinuity, hasMoreSampleBuffers, error in
+            if sampleBuffer != nil {
+                self.lastTimingInfo.presentationTimeStamp = CMClockGetTime(
+                    CMClockGetHostTimeClock())
+                let output: CMIOExtensionScheduledOutput = CMIOExtensionScheduledOutput(
+                    sequenceNumber: sequenceNumber,
+                    hostTimeInNanoseconds: UInt64(
+                        self.lastTimingInfo.presentationTimeStamp.seconds * Double(NSEC_PER_SEC)
+                    )
+                )
+
+                if self._streamingCounter > 0 {
+                    self._streamSource.stream.send(
+                        sampleBuffer!,
+                        discontinuity: [],
+                        hostTimeInNanoseconds: UInt64(
+                            sampleBuffer!.presentationTimeStamp.seconds * Double(NSEC_PER_SEC)
+                        )
+                    )
+                }
+
+                self._streamSink.stream.notifyScheduledOutputChanged(output)
+            }
+            self.consumeBuffer(client)
+        }
+    }
+
+    func startStreamingSink(client: CMIOExtensionClient) {
+        _streamingSinkCounter += 1
+        self.sinkStarted = true
+        consumeBuffer(client)
+    }
+
+    func stopStreamingSink() {
+        self.sinkStarted = false
+        if _streamingCounter > 1 {
+            _streamingSinkCounter -= 1
+        } else {
+            _streamingSinkCounter = 0
+        }
+    }
+}

+ 61 - 0
plugins/mac-virtualcam/src/camera-extension/OBSCameraProviderSource.swift

@@ -0,0 +1,61 @@
+//
+//  OBSCameraProviderSource.swift
+//  camera-extension
+//
+//  Created by Sebastian Beckmann on 2022-09-30.
+//  Changed by Patrick Heyer on 2022-10-16.
+//
+
+import CoreMediaIO
+import Foundation
+
+class OBSCameraProviderSource: NSObject, CMIOExtensionProviderSource {
+    private(set) var provider: CMIOExtensionProvider!
+
+    private var deviceSource: OBSCameraDeviceSource!
+
+    init(clientQueue: DispatchQueue?, deviceUUID: UUID, sourceUUID: UUID, sinkUUID: UUID) {
+        super.init()
+
+        provider = CMIOExtensionProvider(source: self, clientQueue: clientQueue)
+        deviceSource = OBSCameraDeviceSource(
+            localizedName: "OBS Virtual Camera",
+            deviceUUID: deviceUUID,
+            sourceUUID: sourceUUID,
+            sinkUUID: sinkUUID)
+        do {
+            try provider.addDevice(deviceSource.device)
+        } catch let error {
+            fatalError("Failed to add device \(error.localizedDescription)")
+        }
+    }
+
+    func connect(to client: CMIOExtensionClient) throws {
+    }
+
+    func disconnect(from client: CMIOExtensionClient) {
+    }
+
+    var availableProperties: Set<CMIOExtensionProperty> {
+        return [.providerName, .providerManufacturer]
+    }
+
+    func providerProperties(forProperties properties: Set<CMIOExtensionProperty>) throws
+        -> CMIOExtensionProviderProperties
+    {
+        let providerProperties = CMIOExtensionProviderProperties(dictionary: [:])
+
+        if properties.contains(.providerName) {
+            providerProperties.name = "OBS Camera Extension Provider"
+        }
+
+        if properties.contains(.providerManufacturer) {
+            providerProperties.manufacturer = "OBS Project"
+        }
+
+        return providerProperties
+    }
+
+    func setProviderProperties(_ providerProperties: CMIOExtensionProviderProperties) throws {
+    }
+}

+ 111 - 0
plugins/mac-virtualcam/src/camera-extension/OBSCameraStreamSink.swift

@@ -0,0 +1,111 @@
+//
+//  OBSCameraStreamSink.swift
+//  camera-extension
+//
+//  Created by Sebastian Beckmann on 2022-09-30.
+//  Changed by Patrick Heyer on 2022-10-16.
+//
+
+import CoreMediaIO
+import Foundation
+import os.log
+
+class OBSCameraStreamSink: NSObject, CMIOExtensionStreamSource {
+    private(set) var stream: CMIOExtensionStream!
+
+    let device: CMIOExtensionDevice
+
+    private let _streamFormat: CMIOExtensionStreamFormat
+
+    init(
+        localizedName: String, streamID: UUID, streamFormat: CMIOExtensionStreamFormat,
+        device: CMIOExtensionDevice
+    ) {
+        self.device = device
+        self._streamFormat = streamFormat
+        super.init()
+        self.stream = CMIOExtensionStream(
+            localizedName: localizedName,
+            streamID: streamID,
+            direction: .sink,
+            clockType: .hostTime,
+            source: self
+        )
+    }
+
+    var formats: [CMIOExtensionStreamFormat] {
+        return [_streamFormat]
+    }
+
+    var activeFormatIndex: Int = 0 {
+        didSet {
+            if activeFormatIndex >= 1 {
+                os_log(.error, "Invalid index")
+            }
+        }
+    }
+
+    var availableProperties: Set<CMIOExtensionProperty> {
+        return [
+            .streamActiveFormatIndex,
+            .streamFrameDuration,
+            .streamSinkBufferQueueSize,
+            .streamSinkBuffersRequiredForStartup,
+            .streamSinkBufferUnderrunCount,
+            .streamSinkEndOfData,
+        ]
+    }
+
+    var client: CMIOExtensionClient?
+
+    func streamProperties(forProperties properties: Set<CMIOExtensionProperty>) throws
+        -> CMIOExtensionStreamProperties
+    {
+        let streamProperties = CMIOExtensionStreamProperties(dictionary: [:])
+
+        if properties.contains(.streamActiveFormatIndex) {
+            streamProperties.activeFormatIndex = 0
+        }
+        if properties.contains(.streamFrameDuration) {
+            let frameDuration = CMTime(value: 1, timescale: Int32(OBSCameraFrameRate))
+            streamProperties.frameDuration = frameDuration
+        }
+        if properties.contains(.streamSinkBufferQueueSize) {
+            streamProperties.sinkBufferQueueSize = 1
+        }
+        if properties.contains(.streamSinkBuffersRequiredForStartup) {
+            streamProperties.sinkBuffersRequiredForStartup = 1
+        }
+
+        return streamProperties
+    }
+
+    func setStreamProperties(_ streamProperties: CMIOExtensionStreamProperties) throws {
+        if let activeFormatIndex = streamProperties.activeFormatIndex {
+            self.activeFormatIndex = activeFormatIndex
+        }
+    }
+
+    func authorizedToStartStream(for client: CMIOExtensionClient) -> Bool {
+        self.client = client
+        return true
+    }
+
+    func startStream() throws {
+        guard let deviceSource = device.source as? OBSCameraDeviceSource else {
+            fatalError("Unexpected source type \(String(describing: device.source))")
+        }
+
+        if let client = client {
+            deviceSource.startStreamingSink(client: client)
+        }
+    }
+
+    func stopStream() throws {
+        guard let deviceSource = device.source as? OBSCameraDeviceSource else {
+            fatalError("Unexpected source type \(String(describing: device.source))")
+        }
+
+        deviceSource.stopStreamingSink()
+    }
+}

+ 92 - 0
plugins/mac-virtualcam/src/camera-extension/OBSCameraStreamSource.swift

@@ -0,0 +1,92 @@
+//
+//  OBSCameraStreamSource.swift
+//  camera-extension
+//
+//  Created by Sebastian Beckmann on 2022-09-30.
+//  Changed by Patrick Heyer on 2022-10-16.
+//
+
+import CoreMediaIO
+import Foundation
+import os.log
+
+class OBSCameraStreamSource: NSObject, CMIOExtensionStreamSource {
+    private(set) var stream: CMIOExtensionStream!
+
+    let device: CMIOExtensionDevice
+
+    private let _streamFormat: CMIOExtensionStreamFormat
+
+    init(
+        localizedName: String, streamID: UUID, streamFormat: CMIOExtensionStreamFormat,
+        device: CMIOExtensionDevice
+    ) {
+        self.device = device
+        self._streamFormat = streamFormat
+        super.init()
+        self.stream = CMIOExtensionStream(
+            localizedName: localizedName,
+            streamID: streamID,
+            direction: .source,
+            clockType: .hostTime,
+            source: self
+        )
+    }
+
+    var formats: [CMIOExtensionStreamFormat] {
+        return [_streamFormat]
+    }
+
+    var activeFormatIndex: Int = 0 {
+        didSet {
+            if activeFormatIndex >= 1 {
+                os_log(.error, "Invalid index")
+            }
+        }
+    }
+
+    var availableProperties: Set<CMIOExtensionProperty> {
+        return [.streamActiveFormatIndex, .streamFrameDuration]
+    }
+
+    func streamProperties(forProperties properties: Set<CMIOExtensionProperty>) throws
+        -> CMIOExtensionStreamProperties
+    {
+        let streamProperties = CMIOExtensionStreamProperties(dictionary: [:])
+
+        if properties.contains(.streamActiveFormatIndex) {
+            streamProperties.activeFormatIndex = 0
+        }
+        if properties.contains(.streamFrameDuration) {
+            let frameDuration = CMTime(value: 1, timescale: Int32(OBSCameraFrameRate))
+            streamProperties.frameDuration = frameDuration
+        }
+
+        return streamProperties
+    }
+
+    func setStreamProperties(_ streamProperties: CMIOExtensionStreamProperties) throws {
+        if let activeFormatIndex = streamProperties.activeFormatIndex {
+            self.activeFormatIndex = activeFormatIndex
+        }
+    }
+
+    func authorizedToStartStream(for client: CMIOExtensionClient) -> Bool {
+        return true
+    }
+
+    func startStream() throws {
+        guard let deviceSource = device.source as? OBSCameraDeviceSource else {
+            fatalError("Unexpected source type \(String(describing: device.source))")
+        }
+
+        deviceSource.startStreaming()
+    }
+
+    func stopStream() throws {
+        guard let deviceSource = device.source as? OBSCameraDeviceSource else {
+            fatalError("Unexpcted source type \(String(describing: device.source))")
+        }
+        deviceSource.stopStreaming()
+    }
+}

+ 41 - 0
plugins/mac-virtualcam/src/camera-extension/cmake/macos/Info.plist.in

@@ -0,0 +1,41 @@
+<?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>CFBundleName</key>
+    <string>com.obsproject.obs-studio.mac-camera-extension</string>
+    <key>CFBundleIdentifier</key>
+    <string>com.obsproject.obs-studio.mac-camera-extension</string>
+    <key>CFBundleVersion</key>
+    <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
+    <key>CFBundleShortVersionString</key>
+    <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
+    <key>CFBundleExecutable</key>
+    <string>com.obsproject.obs-studio.mac-camera-extension</string>
+    <key>CFBundleInfoDictionaryVersion</key>
+    <string>6.0</string>
+    <key>CFBundlePackageType</key>
+    <string>SYSX</string>
+    <key>CFBundleSupportedPlatforms</key>
+    <array>
+        <string>MacOSX</string>
+    </array>
+    <key>LSMinimumSystemVersion</key>
+    <string>13.0</string>
+    <key>NSHumanReadableCopyright</key>
+    <string>(c) 2022-${CURRENT_YEAR} Sebastian Beckmann, Patrick Heyer</string>
+    <key>CMIOExtension</key>
+    <dict>
+        <key>CMIOExtensionMachServiceName</key>
+        <string>$(TeamIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+    </dict>
+    <key>NSSystemExtensionUsageDescription</key>
+    <string>This Camera Extension enables virtual camera functionality in OBS Studio.</string>
+    <key>OBSCameraDeviceUUID</key>
+    <string>${VIRTUALCAM_DEVICE_UUID}</string>
+    <key>OBSCameraSourceUUID</key>
+    <string>${VIRTUALCAM_SOURCE_UUID}</string>
+    <key>OBSCameraSinkUUID</key>
+    <string>${VIRTUALCAM_SINK_UUID}</string>
+</dict>
+</plist>

+ 12 - 0
plugins/mac-virtualcam/src/camera-extension/cmake/macos/entitlements.plist

@@ -0,0 +1,12 @@
+<!--?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>com.apple.security.app-sandbox</key>
+        <true/>
+        <key>com.apple.security.application-groups</key>
+        <array>
+            <string>$(TeamIdentifierPrefix)com.obsproject.obs-studio</string>
+        </array>
+    </dict>
+</plist>

+ 32 - 0
plugins/mac-virtualcam/src/camera-extension/main.swift

@@ -0,0 +1,32 @@
+//
+//  main.swift
+//  camera-extension
+//
+//  Created by Sebastian Beckmann on 2022-09-30.
+//  Changed by Patrick Heyer on 2022-10-16.
+//
+
+import CoreMediaIO
+import Foundation
+import os.log
+
+let OBSCameraDeviceUUID = Bundle.main.object(forInfoDictionaryKey: "OBSCameraDeviceUUID") as? String
+let OBSCameraSourceUUID = Bundle.main.object(forInfoDictionaryKey: "OBSCameraSourceUUID") as? String
+let OBSCameraSinkUUID = Bundle.main.object(forInfoDictionaryKey: "OBSCameraSinkUUID") as? String
+
+guard let OBSCameraDeviceUUID = OBSCameraDeviceUUID, let OBSCameraSourceUUID = OBSCameraSourceUUID,
+    let OBSCameraSinkUUID = OBSCameraSinkUUID
+else {
+    fatalError("Unable to retrieve Camera Extension UUIDs from Info.plist.")
+}
+
+guard let deviceUUID = UUID(uuidString: OBSCameraDeviceUUID), let sourceUUID = UUID(uuidString: OBSCameraSourceUUID),
+    let sinkUUID = UUID(uuidString: OBSCameraSinkUUID)
+else {
+    fatalError("Unable to generate Camera Extension UUIDs from Info.plist values.")
+}
+
+let providerSource = OBSCameraProviderSource(
+    clientQueue: nil, deviceUUID: deviceUUID, sourceUUID: sourceUUID, sinkUUID: sinkUUID)
+CMIOExtensionProvider.startService(provider: providerSource.provider)
+CFRunLoopRun()