From c657e9538169b822448364e0dd47c510231285ea Mon Sep 17 00:00:00 2001 From: jcm Date: Thu, 21 Dec 2023 15:53:57 -0700 Subject: [PATCH] create virtualcam --- CaptureSample/CaptureEngine.swift | 3 +- CaptureSample/CaptureSample.entitlements | 4 +- CaptureSample/CaptureSampleApp.swift | 32 ++ CaptureSample/ScreenRecorder.swift | 10 +- README.md | 1 + Record.xcodeproj/project.pbxproj | 161 ++++++++- RecordCameraExtension/Info.plist | 13 + .../RecordCameraExtension.entitlements | 12 + .../RecordCameraExtensionProvider.swift | 315 ++++++++++++++++++ RecordCameraExtension/main.swift | 15 + 10 files changed, 559 insertions(+), 7 deletions(-) create mode 100644 RecordCameraExtension/Info.plist create mode 100644 RecordCameraExtension/RecordCameraExtension.entitlements create mode 100644 RecordCameraExtension/RecordCameraExtensionProvider.swift create mode 100644 RecordCameraExtension/main.swift diff --git a/CaptureSample/CaptureEngine.swift b/CaptureSample/CaptureEngine.swift index 8a91db3..edf48da 100644 --- a/CaptureSample/CaptureEngine.swift +++ b/CaptureSample/CaptureEngine.swift @@ -10,6 +10,7 @@ import AVFAudio import ScreenCaptureKit import OSLog import VideoToolbox +import com_jcm_Record_RecordCameraExtension /// A structure that contains the video data to render. struct CapturedFrame { @@ -140,7 +141,7 @@ class CaptureEngineStreamOutput: NSObject, SCStreamOutput, SCStreamDelegate { /// We assume that we don't want to perform lots of work on these queues, so they /// can be maximally available to handle new frames as they're delivered by SCK. /// Therefore, immediately dispatch to the frame handler queue. - + self.frameHandlerQueue.schedule { guard sampleBuffer.isValid else { self.logger.notice("ScreenCaptureKit emitted an invalid frame; skipping it. Timestamp: \(sampleBuffer.presentationTimeStamp.seconds, privacy: .public)") diff --git a/CaptureSample/CaptureSample.entitlements b/CaptureSample/CaptureSample.entitlements index a1e1364..07c80a8 100644 --- a/CaptureSample/CaptureSample.entitlements +++ b/CaptureSample/CaptureSample.entitlements @@ -7,7 +7,9 @@ com.apple.security.app-sandbox com.apple.security.application-groups - + + $(TeamIdentifierPrefix)com.jcm.Record + com.apple.security.files.user-selected.read-write diff --git a/CaptureSample/CaptureSampleApp.swift b/CaptureSample/CaptureSampleApp.swift index 4e8dfb3..c6d8a78 100644 --- a/CaptureSample/CaptureSampleApp.swift +++ b/CaptureSample/CaptureSampleApp.swift @@ -6,6 +6,7 @@ The entry point into this app. */ import SwiftUI import OSLog +import SystemExtensions let startupTime = Date() @@ -22,8 +23,12 @@ struct CaptureSampleApp: App { @State var selectedPreset: OptionsStorable! var logger = Logger.application + + var extensionActivated = false @StateObject var screenRecorder = ScreenRecorder() + + var requestDelegate = CameraExtensionRequestDelegate() @State private var currentLog: TextDocument? @@ -110,3 +115,30 @@ struct CaptureSampleApp: App { } } } + +class CameraExtensionRequestDelegate: NSObject, OSSystemExtensionRequestDelegate { + func request(_ request: OSSystemExtensionRequest, actionForReplacingExtension existing: OSSystemExtensionProperties, withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction { + return .replace + } + + func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) { + Logger.application.info("Camera extension requires user approval.") + } + + func request(_ request: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) { + switch result { + case .completed: + Logger.application.info("Camera extension installation is complete.") + case .willCompleteAfterReboot: + Logger.application.info("Camera extension installation will complete after reboot.") + default: + Logger.application.info("poop.") + } + } + + func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) { + Logger.application.error("Camera extension installation failed with error \(error, privacy: .public)") + } + + +} diff --git a/CaptureSample/ScreenRecorder.swift b/CaptureSample/ScreenRecorder.swift index c924212..33de113 100644 --- a/CaptureSample/ScreenRecorder.swift +++ b/CaptureSample/ScreenRecorder.swift @@ -26,6 +26,8 @@ class ScreenRecorder: ObservableObject { private var extensionActivated = false + var requestDelegate = CameraExtensionRequestDelegate() + //MARK: event tap //private var providerSource: RecordVirtualCamProviderSource! @@ -546,15 +548,15 @@ class ScreenRecorder: ObservableObject { if self.showsEncodePreview { self.updateEncodePreview() } - /*if !self.extensionActivated { - let identifier = "com.example.apple-samplecode.CustomCamera.CameraExtension" + if !self.extensionActivated { + let identifier = "com.jcm.Record.RecordCameraExtension" // Submit an activation request. let activationRequest = OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier: identifier, queue: .main) - activationRequest.delegate = self + activationRequest.delegate = self.requestDelegate OSSystemExtensionManager.shared.submitRequest(activationRequest) - }*/ + } } /// - Tag: UpdateFilter diff --git a/README.md b/README.md index 081d173..15d4b5b 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Record is in active development and uses rolling releases. Download the most rec # Roadmap +* menu bar mode / run in background * audio settings / audio metering / audio only * advanced capture options (include some apps, not others) * compositing? diff --git a/Record.xcodeproj/project.pbxproj b/Record.xcodeproj/project.pbxproj index b5c4d3c..0df58e8 100644 --- a/Record.xcodeproj/project.pbxproj +++ b/Record.xcodeproj/project.pbxproj @@ -30,6 +30,9 @@ 0D4875402A81A20800F6D832 /* Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D48753F2A81A20800F6D832 /* Enums.swift */; }; 0D52D73F2AB9283F0091AD97 /* EventTap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D52D73E2AB9283F0091AD97 /* EventTap.swift */; }; 0D5C529C2AD60B8E00D11280 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D5C529B2AD60B8E00D11280 /* Logging.swift */; }; + 0D6933012B34B1850019368E /* RecordCameraExtensionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D6933002B34B1850019368E /* RecordCameraExtensionProvider.swift */; }; + 0D6933032B34B1850019368E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D6933022B34B1850019368E /* main.swift */; }; + 0D6933082B34B1850019368E /* com.jcm.Record.RecordCameraExtension.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 0D6932FE2B34B1850019368E /* com.jcm.Record.RecordCameraExtension.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 0DE5C82F2A95EDC60054AC23 /* PickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE5C82E2A95EDC60054AC23 /* PickerView.swift */; }; C470F0812811C5CB00D29309 /* ScreenRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C470F0802811C5CB00D29309 /* ScreenRecorder.swift */; }; C471DFFB2809F440001D24C9 /* PowerMeter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C471DFF92809F440001D24C9 /* PowerMeter.swift */; }; @@ -44,6 +47,16 @@ C4EB90D428108656006A595C /* ConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EB90D328108656006A595C /* ConfigurationView.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 0D6933062B34B1850019368E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C4B0DA9F276BA4460015082A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0D6932FD2B34B1850019368E; + remoteInfo = RecordCameraExtension; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 0DBF97E42AEB29CF00D21A33 /* Embed System Extensions */ = { isa = PBXCopyFilesBuildPhase; @@ -51,6 +64,7 @@ dstPath = "$(SYSTEM_EXTENSIONS_FOLDER_PATH)"; dstSubfolderSpec = 16; files = ( + 0D6933082B34B1850019368E /* com.jcm.Record.RecordCameraExtension.systemextension in Embed System Extensions */, ); name = "Embed System Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -82,6 +96,11 @@ 0D52D73E2AB9283F0091AD97 /* EventTap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTap.swift; sourceTree = ""; }; 0D5C529B2AD60B8E00D11280 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; 0D64B0E62B1699E200A8CAFE /* Record-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Record-Info.plist"; sourceTree = ""; }; + 0D6932FE2B34B1850019368E /* com.jcm.Record.RecordCameraExtension.systemextension */ = {isa = PBXFileReference; explicitFileType = "wrapper.system-extension"; includeInIndex = 0; path = com.jcm.Record.RecordCameraExtension.systemextension; sourceTree = BUILT_PRODUCTS_DIR; }; + 0D6933002B34B1850019368E /* RecordCameraExtensionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordCameraExtensionProvider.swift; sourceTree = ""; }; + 0D6933022B34B1850019368E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + 0D6933042B34B1850019368E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0D6933052B34B1850019368E /* RecordCameraExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RecordCameraExtension.entitlements; sourceTree = ""; }; 0DE5C82E2A95EDC60054AC23 /* PickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerView.swift; sourceTree = ""; }; 0DF11FDD2A6ECBA500B45306 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.markdown; }; 7C6C99F1D4B6E3EBA3A7B7DF /* LICENSE.txt */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; @@ -101,6 +120,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 0D6932FB2B34B1850019368E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; C4B0DAA4276BA4460015082A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -147,6 +173,17 @@ path = "Test Pattern"; sourceTree = ""; }; + 0D6932FF2B34B1850019368E /* RecordCameraExtension */ = { + isa = PBXGroup; + children = ( + 0D6933002B34B1850019368E /* RecordCameraExtensionProvider.swift */, + 0D6933022B34B1850019368E /* main.swift */, + 0D6933042B34B1850019368E /* Info.plist */, + 0D6933052B34B1850019368E /* RecordCameraExtension.entitlements */, + ); + path = RecordCameraExtension; + sourceTree = ""; + }; 58C6EBE29CE5FC0542EA3228 /* LICENSE */ = { isa = PBXGroup; children = ( @@ -176,6 +213,7 @@ 0D164AAE2A996C6E003F2F7E /* objective-c-xcode.yml */, 0DF11FDD2A6ECBA500B45306 /* README.md */, C4B0DAA9276BA4460015082A /* CaptureSample */, + 0D6932FF2B34B1850019368E /* RecordCameraExtension */, C4B0DAA8276BA4460015082A /* Products */, CFC39354E2BF335FE5D2CDFE /* Configuration */, 58C6EBE29CE5FC0542EA3228 /* LICENSE */, @@ -186,6 +224,7 @@ isa = PBXGroup; children = ( C4B0DAA7276BA4460015082A /* Record.app */, + 0D6932FE2B34B1850019368E /* com.jcm.Record.RecordCameraExtension.systemextension */, ); name = Products; sourceTree = ""; @@ -232,6 +271,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 0D6932FD2B34B1850019368E /* RecordCameraExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0D69330B2B34B1850019368E /* Build configuration list for PBXNativeTarget "RecordCameraExtension" */; + buildPhases = ( + 0D6932FA2B34B1850019368E /* Sources */, + 0D6932FB2B34B1850019368E /* Frameworks */, + 0D6932FC2B34B1850019368E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RecordCameraExtension; + productName = RecordCameraExtension; + productReference = 0D6932FE2B34B1850019368E /* com.jcm.Record.RecordCameraExtension.systemextension */; + productType = "com.apple.product-type.system-extension"; + }; C4B0DAA6276BA4460015082A /* Record */ = { isa = PBXNativeTarget; buildConfigurationList = C4B0DAB6276BA4480015082A /* Build configuration list for PBXNativeTarget "Record" */; @@ -244,6 +300,7 @@ buildRules = ( ); dependencies = ( + 0D6933072B34B1850019368E /* PBXTargetDependency */, ); name = Record; packageProductDependencies = ( @@ -260,10 +317,13 @@ attributes = { BuildIndependentTargetsInParallel = 1; DefaultBuildSystemTypeForWorkspace = Latest; - LastSwiftUpdateCheck = 1500; + LastSwiftUpdateCheck = 1510; LastUpgradeCheck = 1500; ORGANIZATIONNAME = jcm; TargetAttributes = { + 0D6932FD2B34B1850019368E = { + CreatedOnToolsVersion = 15.1; + }; C4B0DAA6276BA4460015082A = { CreatedOnToolsVersion = 13.3; LastSwiftMigration = 1330; @@ -286,11 +346,19 @@ projectRoot = ""; targets = ( C4B0DAA6276BA4460015082A /* Record */, + 0D6932FD2B34B1850019368E /* RecordCameraExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 0D6932FC2B34B1850019368E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; C4B0DAA5276BA4460015082A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -304,6 +372,15 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 0D6932FA2B34B1850019368E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0D6933032B34B1850019368E /* main.swift in Sources */, + 0D6933012B34B1850019368E /* RecordCameraExtensionProvider.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C4B0DAA3276BA4460015082A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -345,7 +422,80 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 0D6933072B34B1850019368E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0D6932FD2B34B1850019368E /* RecordCameraExtension */; + targetProxy = 0D6933062B34B1850019368E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ + 0D6933092B34B1850019368E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = RecordCameraExtension/RecordCameraExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6E383BMF5Y; + ENABLE_HARDENED_RUNTIME = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = RecordCameraExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = RecordCameraExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 jcm. All rights reserved."; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jcm.Record.RecordCameraExtension; + PRODUCT_NAME = "$(inherited)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 0D69330A2B34B1850019368E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = RecordCameraExtension/RecordCameraExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6E383BMF5Y; + ENABLE_HARDENED_RUNTIME = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = RecordCameraExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = RecordCameraExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 jcm. All rights reserved."; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jcm.Record.RecordCameraExtension; + PRODUCT_NAME = "$(inherited)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; C4B0DAB4276BA4480015082A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -541,6 +691,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 0D69330B2B34B1850019368E /* Build configuration list for PBXNativeTarget "RecordCameraExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0D6933092B34B1850019368E /* Debug */, + 0D69330A2B34B1850019368E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; C4B0DAA2276BA4460015082A /* Build configuration list for PBXProject "Record" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/RecordCameraExtension/Info.plist b/RecordCameraExtension/Info.plist new file mode 100644 index 0000000..199e949 --- /dev/null +++ b/RecordCameraExtension/Info.plist @@ -0,0 +1,13 @@ + + + + + CMIOExtension + + CMIOExtensionMachServiceName + $(TeamIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER) + + NSSystemExtensionUsageDescription + + + diff --git a/RecordCameraExtension/RecordCameraExtension.entitlements b/RecordCameraExtension/RecordCameraExtension.entitlements new file mode 100644 index 0000000..db677c9 --- /dev/null +++ b/RecordCameraExtension/RecordCameraExtension.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + $(TeamIdentifierPrefix)com.jcm.Record + + + diff --git a/RecordCameraExtension/RecordCameraExtensionProvider.swift b/RecordCameraExtension/RecordCameraExtensionProvider.swift new file mode 100644 index 0000000..c897550 --- /dev/null +++ b/RecordCameraExtension/RecordCameraExtensionProvider.swift @@ -0,0 +1,315 @@ +// +// RecordCameraExtensionProvider.swift +// RecordCameraExtension +// +// Created by John Moody on 12/21/23. +// Copyright © 2023 jcm. All rights reserved. +// + +import Foundation +import CoreMediaIO +import IOKit.audio +import os.log + +let kWhiteStripeHeight: Int = 10 +let kFrameRate: Int = 60 + +// MARK: - + +class RecordCameraExtensionDeviceSource: NSObject, CMIOExtensionDeviceSource { + + private(set) var device: CMIOExtensionDevice! + + private var _streamSource: RecordCameraExtensionStreamSource! + + private var _streamingCounter: 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 _whiteStripeStartRow: UInt32 = 0 + + private var _whiteStripeIsAscending: Bool = false + + init(localizedName: String) { + + super.init() + let deviceID = UUID(uuidString: "7626645E-4425-469E-9D8B-97E0FA59AC75")! // replace this with your device UUID + self.device = CMIOExtensionDevice(localizedName: localizedName, deviceID: deviceID, legacyDeviceID: nil, source: self) + + let dims = CMVideoDimensions(width: 1920, height: 1080) + CMVideoFormatDescriptionCreate(allocator: kCFAllocatorDefault, codecType: kCVPixelFormatType_32BGRA, width: dims.width, height: dims.height, extensions: nil, formatDescriptionOut: &_videoDescription) + + let pixelBufferAttributes: NSDictionary = [ + kCVPixelBufferWidthKey: dims.width, + kCVPixelBufferHeightKey: dims.height, + kCVPixelBufferPixelFormatTypeKey: _videoDescription.mediaSubType, + kCVPixelBufferIOSurfacePropertiesKey: [:] as NSDictionary + ] + CVPixelBufferPoolCreate(kCFAllocatorDefault, nil, pixelBufferAttributes, &_bufferPool) + + let videoStreamFormat = CMIOExtensionStreamFormat.init(formatDescription: _videoDescription, maxFrameDuration: CMTime(value: 1, timescale: Int32(kFrameRate)), minFrameDuration: CMTime(value: 1, timescale: Int32(kFrameRate)), validFrameDurations: nil) + _bufferAuxAttributes = [kCVPixelBufferPoolAllocationThresholdKey: 5] + + let videoID = UUID(uuidString: "A8D7B8AA-65AD-4D21-9C42-66480DBFA8E1")! // replace this with your video UUID + _streamSource = RecordCameraExtensionStreamSource(localizedName: "RecordCameraExtension.Video", streamID: videoID, streamFormat: videoStreamFormat, device: device) + do { + try device.addStream(_streamSource.stream) + } catch let error { + fatalError("Failed to add stream: \(error.localizedDescription)") + } + } + + var availableProperties: Set { + + return [.deviceTransportType, .deviceModel] + } + + func deviceProperties(forProperties properties: Set) throws -> CMIOExtensionDeviceProperties { + + let deviceProperties = CMIOExtensionDeviceProperties(dictionary: [:]) + if properties.contains(.deviceTransportType) { + deviceProperties.transportType = kIOAudioDeviceTransportTypeVirtual + } + if properties.contains(.deviceModel) { + deviceProperties.model = "RecordCameraExtension Model" + } + + return deviceProperties + } + + func setDeviceProperties(_ deviceProperties: CMIOExtensionDeviceProperties) throws { + + // Handle settable properties here. + } + + func startStreaming() { + + guard let _ = _bufferPool else { + return + } + + _streamingCounter += 1 + + _timer = DispatchSource.makeTimerSource(flags: .strict, queue: _timerQueue) + _timer!.schedule(deadline: .now(), repeating: 1.0 / Double(kFrameRate), leeway: .seconds(0)) + + _timer!.setEventHandler { + + var err: OSStatus = 0 + let now = CMClockGetTime(CMClockGetHostTimeClock()) + + var pixelBuffer: CVPixelBuffer? + err = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, self._bufferPool, self._bufferAuxAttributes, &pixelBuffer) + if err != 0 { + os_log(.error, "out of pixel buffers \(err)") + } + + if let pixelBuffer = pixelBuffer { + + CVPixelBufferLockBaseAddress(pixelBuffer, []) + + var bufferPtr = CVPixelBufferGetBaseAddress(pixelBuffer)! + let width = CVPixelBufferGetWidth(pixelBuffer) + let height = CVPixelBufferGetHeight(pixelBuffer) + let rowBytes = CVPixelBufferGetBytesPerRow(pixelBuffer) + memset(bufferPtr, 0, rowBytes * height) + + let whiteStripeStartRow = self._whiteStripeStartRow + if self._whiteStripeIsAscending { + self._whiteStripeStartRow = whiteStripeStartRow - 1 + self._whiteStripeIsAscending = self._whiteStripeStartRow > 0 + } + else { + self._whiteStripeStartRow = whiteStripeStartRow + 1 + self._whiteStripeIsAscending = self._whiteStripeStartRow >= (height - kWhiteStripeHeight) + } + bufferPtr += rowBytes * Int(whiteStripeStartRow) + for _ in 0.. 1 { + _streamingCounter -= 1 + } + else { + _streamingCounter = 0 + if let timer = _timer { + timer.cancel() + _timer = nil + } + } + } + + func consumeBuffer(_ client: CMIOExtensionClient) { + self._streamS + } +} + +// MARK: - + +class RecordCameraExtensionStreamSource: 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 { + + return [.streamActiveFormatIndex, .streamFrameDuration] + } + + func streamProperties(forProperties properties: Set) throws -> CMIOExtensionStreamProperties { + + let streamProperties = CMIOExtensionStreamProperties(dictionary: [:]) + if properties.contains(.streamActiveFormatIndex) { + streamProperties.activeFormatIndex = 0 + } + if properties.contains(.streamFrameDuration) { + let frameDuration = CMTime(value: 1, timescale: Int32(kFrameRate)) + streamProperties.frameDuration = frameDuration + } + + return streamProperties + } + + func setStreamProperties(_ streamProperties: CMIOExtensionStreamProperties) throws { + + if let activeFormatIndex = streamProperties.activeFormatIndex { + self.activeFormatIndex = activeFormatIndex + } + } + + func authorizedToStartStream(for client: CMIOExtensionClient) -> Bool { + + // An opportunity to inspect the client info and decide if it should be allowed to start the stream. + return true + } + + func startStream() throws { + + guard let deviceSource = device.source as? RecordCameraExtensionDeviceSource else { + fatalError("Unexpected source type \(String(describing: device.source))") + } + deviceSource.startStreaming() + } + + func stopStream() throws { + + guard let deviceSource = device.source as? RecordCameraExtensionDeviceSource else { + fatalError("Unexpected source type \(String(describing: device.source))") + } + deviceSource.stopStreaming() + } +} + +// MARK: - + +class RecordCameraExtensionProviderSource: NSObject, CMIOExtensionProviderSource { + + private(set) var provider: CMIOExtensionProvider! + + private var deviceSource: RecordCameraExtensionDeviceSource! + + // CMIOExtensionProviderSource protocol methods (all are required) + + init(clientQueue: DispatchQueue?) { + + super.init() + + provider = CMIOExtensionProvider(source: self, clientQueue: clientQueue) + deviceSource = RecordCameraExtensionDeviceSource(localizedName: "RecordCameraExtension (Swift)") + + do { + try provider.addDevice(deviceSource.device) + } catch let error { + fatalError("Failed to add device: \(error.localizedDescription)") + } + } + + func connect(to client: CMIOExtensionClient) throws { + + // Handle client connect + } + + func disconnect(from client: CMIOExtensionClient) { + + // Handle client disconnect + } + + var availableProperties: Set { + + // See full list of CMIOExtensionProperty choices in CMIOExtensionProperties.h + return [.providerManufacturer] + } + + func providerProperties(forProperties properties: Set) throws -> CMIOExtensionProviderProperties { + + let providerProperties = CMIOExtensionProviderProperties(dictionary: [:]) + if properties.contains(.providerManufacturer) { + providerProperties.manufacturer = "RecordCameraExtension Manufacturer" + } + return providerProperties + } + + func setProviderProperties(_ providerProperties: CMIOExtensionProviderProperties) throws { + + // Handle settable properties here. + } +} diff --git a/RecordCameraExtension/main.swift b/RecordCameraExtension/main.swift new file mode 100644 index 0000000..bb0be39 --- /dev/null +++ b/RecordCameraExtension/main.swift @@ -0,0 +1,15 @@ +// +// main.swift +// RecordCameraExtension +// +// Created by John Moody on 12/21/23. +// Copyright © 2023 jcm. All rights reserved. +// + +import Foundation +import CoreMediaIO + +let providerSource = RecordCameraExtensionProviderSource(clientQueue: nil) +CMIOExtensionProvider.startService(provider: providerSource.provider) + +CFRunLoopRun()