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()