diff --git a/Capturer.xcodeproj/project.pbxproj b/Capturer.xcodeproj/project.pbxproj index 24ae784..be5e6c6 100644 --- a/Capturer.xcodeproj/project.pbxproj +++ b/Capturer.xcodeproj/project.pbxproj @@ -400,7 +400,6 @@ 4B2D586A25346C730080E958 /* Frameworks */, 4B2D586B25346C730080E958 /* Resources */, 4B2D5D8625347B7C0080E958 /* Embed Frameworks */, - D04BD3091FCA399F86FC6D03 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -439,8 +438,9 @@ 4B2D585425346C400080E958 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1200; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1620; TargetAttributes = { 4B2D585C25346C410080E958 = { CreatedOnToolsVersion = 12.0; @@ -564,23 +564,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - D04BD3091FCA399F86FC6D03 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-CapturerDemoSwiftUI/Pods-CapturerDemoSwiftUI-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-CapturerDemoSwiftUI/Pods-CapturerDemoSwiftUI-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-CapturerDemoSwiftUI/Pods-CapturerDemoSwiftUI-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -706,6 +689,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -770,6 +754,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -795,25 +780,28 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = NO; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = Capturer/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = app.muukii.ComponentCamera; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -823,24 +811,27 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = NO; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = Capturer/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = app.muukii.ComponentCamera; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -860,14 +851,14 @@ "$(PROJECT_DIR)", ); INFOPLIST_FILE = ComponentCameraDemoSwiftUI/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = app.muukii.ComponentCameraDemoSwiftUI; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -887,14 +878,14 @@ "$(PROJECT_DIR)", ); INFOPLIST_FILE = ComponentCameraDemoSwiftUI/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = app.muukii.ComponentCameraDemoSwiftUI; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -912,14 +903,14 @@ "$(PROJECT_DIR)", ); INFOPLIST_FILE = ComponentCameraDemoUIKit/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = app.muukii.ComponentCameraDemoUIKit; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -937,14 +928,14 @@ "$(PROJECT_DIR)", ); INFOPLIST_FILE = ComponentCameraDemoUIKit/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = app.muukii.ComponentCameraDemoUIKit; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; }; name = Release; diff --git a/Capturer.xcodeproj/xcshareddata/xcschemes/ComponentCameraDemoSwiftUI.xcscheme b/Capturer.xcodeproj/xcshareddata/xcschemes/ComponentCameraDemoSwiftUI.xcscheme index b2d282c..af35e90 100644 --- a/Capturer.xcodeproj/xcshareddata/xcschemes/ComponentCameraDemoSwiftUI.xcscheme +++ b/Capturer.xcodeproj/xcshareddata/xcschemes/ComponentCameraDemoSwiftUI.xcscheme @@ -1,6 +1,6 @@ Void - ) { - var instance = Self() - modify(&instance) - self = instance + public var sessionPreset: AVCaptureSession.Preset = .photo + + public init() { + + } + + public init( + modify: (inout Self) -> Void + ) { + var instance = Self() + modify(&instance) + self = instance + } } - } - public struct State: Equatable { + public struct State: Equatable { - } + } - public struct Handlers { + public struct Handlers { - public var didChangeState: (State) -> Void = { _ in } + public var didChangeState: (State) -> Void = { _ in } - public init() {} - } + public init() {} + } - public private(set) var state: State = .init() { - didSet { - if oldValue != state { - handlers.didChangeState(state) + public private(set) var state: State = .init() { + didSet { + if oldValue != state { + handlers.didChangeState(state) + } } } - } - public var handlers: Handlers = .init() + public var handlers: Handlers = .init() - public let session: AVCaptureSession + private let session: AVCaptureSession = .init() + private var inputNode: InputNodeType? + private var outputNodes: [OutputNodeType] = [] - private var inputNode: InputNodeType? - private var outputNodes: [OutputNodeType] = [] + public init( + configuration: Configuration + ) { + guard ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] != "1" else { return } + _ = self.session - public init( - configuration: Configuration - ) { - session = .init() - guard ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] != "1" else { return } + assert(Utils.checkIfCanUseCameraAccordingToPrivacySensitiveData() == true) - assert(Utils.checkIfCanUseCameraAccordingToPrivacySensitiveData() == true) + self.session.performConfiguration { + $0.sessionPreset = configuration.sessionPreset + $0.automaticallyConfiguresCaptureDeviceForWideColor = true + } - session.performConfiguration { - $0.sessionPreset = configuration.sessionPreset - $0.automaticallyConfiguresCaptureDeviceForWideColor = true } - } + public func start() { + Log.debug(.capture, "Session started") - public func start() { + self.session.startRunning() + } - Log.debug(.capture, "Session started") + public func stop() { - session.startRunning() - } + Log.debug(.capture, "Session stopped") + self.session.stopRunning() + } - public func stop() { + public func batchAttaching( + input newInputNode: InputNodeType, + outputs newOutputNodes: [OutputNodeType] + ) { + self.session.performConfiguration { session in - Log.debug(.capture, "Session stopped") + if let currentInputNode = self.inputNode { + currentInputNode.tearDown(sessionInConfiguring: session) + } - session.stopRunning() - } + self.inputNode = newInputNode - public func batchAttaching( - input newInputNode: InputNodeType, - outputs newOutputNodes: [OutputNodeType] - ) { + newInputNode.setUp(sessionInConfiguring: session) - session.performConfiguration { session in + self.outputNodes.append(contentsOf: newOutputNodes) - if let currentInputNode = inputNode { - currentInputNode.tearDown(sessionInConfiguring: session) + session.performConfiguration { session in + newOutputNodes.forEach { + $0.setUp(sessionInConfiguring: session) + } + } } - inputNode = newInputNode + } + + /** + Attaches an input with replacing current input. + */ + public func attach(input newInputNode: some DeviceInputNodeType) { - newInputNode.setUp(sessionInConfiguring: session) + Log.debug(.capture, "Attach input \(newInputNode)") - outputNodes.append(contentsOf: newOutputNodes) + self.session.performConfiguration { session in - session.performConfiguration { session in - newOutputNodes.forEach { - $0.setUp(sessionInConfiguring: session) + if let currentNode = self.inputNode { + currentNode.tearDown(sessionInConfiguring: session) } + + self.inputNode = newInputNode + + newInputNode.setUp(sessionInConfiguring: session) } } - } + public func attach(output component: some OutputNodeType) { - /** - Attaches an input with replacing current input. - */ - public func attach(input newInputNode: some DeviceInputNodeType) { + Log.debug(.capture, "Attach output \(component)") - Log.debug(.capture, "Attach input \(newInputNode)") + outputNodes.append(component) - session.performConfiguration { session in - if let currentNode = inputNode { - currentNode.tearDown(sessionInConfiguring: session) + self.session.performConfiguration { + component.setUp(sessionInConfiguring: $0) } + } - inputNode = newInputNode + public func removeCurrentInput() { - newInputNode.setUp(sessionInConfiguring: session) + guard let currentInput = inputNode else { + return + } + + inputNode = nil + + self.session.performConfiguration { + currentInput.tearDown(sessionInConfiguring: $0) + } } - } + // upon migrating to ios 18.4, we can place this in the isolated deinit and remove the wrapper calling it + fileprivate func isolatedDeinitAction() { + session.performConfiguration { [inputNode] in + inputNode?.tearDown(sessionInConfiguring: $0) + } - public func attach(output component: some OutputNodeType) { + session.performConfiguration { [outputNodes] session in + outputNodes.forEach { output in + output.tearDown(sessionInConfiguring: session) + } + } + } - Log.debug(.capture, "Attach output \(component)") + deinit { - outputNodes.append(component) + Log.debug(.capture, "\(self) deinitializes") - session.performConfiguration { - component.setUp(sessionInConfiguring: $0) } } - public func removeCurrentInput() { + // Boilerplate wrapping: + public init( + configuration: Configuration + ) { + self.wrappedCaptureBody = .init(configuration: configuration) + } + + public func start() async { + await wrappedCaptureBody.start() + } - guard let currentInput = inputNode else { - return - } + public func stop() async { + await wrappedCaptureBody.stop() + } - inputNode = nil + public func batchAttaching( + input newInputNode: InputNodeType, + outputs newOutputNodes: [OutputNodeType] + ) async { + await wrappedCaptureBody.batchAttaching(input: newInputNode, outputs: newOutputNodes) + } - session.performConfiguration { - currentInput.tearDown(sessionInConfiguring: $0) - } + public func removeCurrentInput() async { + await wrappedCaptureBody.removeCurrentInput() } - deinit { + public func attach(output component: some OutputNodeType) async { + await wrappedCaptureBody.attach(output: component) + } - Log.debug(.capture, "\(self) deinitializes") + public func attach(input newInputNode: some DeviceInputNodeType) async { + await wrappedCaptureBody.attach(input: newInputNode) + } - session.performConfiguration { [inputNode] in - inputNode?.tearDown(sessionInConfiguring: $0) - } + private let wrappedCaptureBody: CaptureBody - session.performConfiguration { [outputNodes] session in - outputNodes.forEach { output in - output.tearDown(sessionInConfiguring: session) - } + deinit { + Task { [wrappedCaptureBody] in + await wrappedCaptureBody.isolatedDeinitAction() } - } - } extension AVCaptureSession { diff --git a/Capturer/Basic/EventBus.swift b/Capturer/Basic/EventBus.swift index 0a21a9f..387057b 100644 --- a/Capturer/Basic/EventBus.swift +++ b/Capturer/Basic/EventBus.swift @@ -1,7 +1,8 @@ import Foundation +import os -public final class EventBusCancellable: Hashable { +public final class EventBusCancellable: Hashable, @unchecked Sendable { public static func == (lhs: EventBusCancellable, rhs: EventBusCancellable) -> Bool { lhs === rhs @@ -11,7 +12,6 @@ public final class EventBusCancellable: Hashable { ObjectIdentifier(self).hash(into: &hasher) } - private var isAutoCancelEnabled: Bool = false private let _onCancel: (EventBusCancellable) -> Void @@ -22,51 +22,42 @@ public final class EventBusCancellable: Hashable { public func cancel() { _onCancel(self) } - - /// Enables auto cancellation on deinitialization. - /// [non-atomic] - public func enableAutoCancel() { - isAutoCancelEnabled = true - } - - deinit { - if isAutoCancelEnabled { - cancel() - } - } } /// [non-atomic] -public final class EventBus { - - public typealias Handler = (Element) -> Void +public actor EventBus { + public typealias Handler = @Sendable (Element) -> Void public var hasTargets: Bool = false public init() { - } - private var targets: ContiguousArray<(EventBusCancellable, Handler)> = .init() { + private var targets: ContiguousArray<(cancellable: EventBusCancellable, handler: Handler)> = .init() { didSet { hasTargets = targets.isEmpty == false } } + private func removeTarget(matchingCancellable: EventBusCancellable) { + self.targets.removeAll { $0.cancellable == matchingCancellable } + } + public func addHandler(_ handler: @escaping Handler) -> EventBusCancellable { let cancellable = EventBusCancellable { [weak self] cancellable in guard let self = self else { return } - self.targets.removeAll { $0.0 == cancellable } + Task { + await self.removeTarget(matchingCancellable: cancellable) + } } - targets.append((cancellable, handler)) return cancellable } public func emit(element: Element) { for target in targets { - target.1(element) + target.handler(element) } } diff --git a/Capturer/Basic/Inputs/CameraInput.swift b/Capturer/Basic/Inputs/CameraInput.swift index fb220c1..2d46389 100644 --- a/Capturer/Basic/Inputs/CameraInput.swift +++ b/Capturer/Basic/Inputs/CameraInput.swift @@ -1,7 +1,7 @@ -import AVFoundation +@preconcurrency import AVFoundation -public final class CameraInput: _StatefulObjectBase, DeviceInputNodeType { +public final class CameraInput: _StatefulObjectBase, DeviceInputNodeType, @unchecked Sendable { public var device: AVCaptureDevice { captureDeviceInput.device @@ -15,6 +15,7 @@ public final class CameraInput: _StatefulObjectBase, DeviceInputNodeType { } public func setUp(sessionInConfiguring: AVCaptureSession) { + // Are those actually thread safe ? sessionInConfiguring.addInput(captureDeviceInput) } diff --git a/Capturer/Basic/Inputs/InputNodeType.swift b/Capturer/Basic/Inputs/InputNodeType.swift index e77294c..71a17ff 100644 --- a/Capturer/Basic/Inputs/InputNodeType.swift +++ b/Capturer/Basic/Inputs/InputNodeType.swift @@ -2,7 +2,7 @@ import Foundation import AVFoundation -public protocol InputNodeType: AnyObject { +public protocol InputNodeType: Sendable { func setUp(sessionInConfiguring: AVCaptureSession) func tearDown(sessionInConfiguring: AVCaptureSession) } diff --git a/Capturer/Basic/Outputs/AnyCVPixelBufferOutput.swift b/Capturer/Basic/Outputs/AnyCVPixelBufferOutput.swift index 23aeb5c..cff2890 100644 --- a/Capturer/Basic/Outputs/AnyCVPixelBufferOutput.swift +++ b/Capturer/Basic/Outputs/AnyCVPixelBufferOutput.swift @@ -1,36 +1,52 @@ import Foundation import AVFoundation +@preconcurrency import CoreMedia -public final class AnyCVPixelBufferOutput: _StatefulObjectBase, PixelBufferOutputNodeType { +extension CVBuffer: @retroactive @unchecked Sendable {} + +public class AnyCVPixelBufferOutput: PixelBufferOutputNodeType, @unchecked Sendable { public let pixelBufferBus: EventBus = .init() private let upstream: VideoDataOutput - private var cancellable: EventBusCancellable? + private var cancellable: EventBusCancellable? = nil - public init( + public init( upstream: VideoDataOutput, filter: Filter - ) { - self.upstream = upstream - - if (filter is NoPixelBufferModifier) == false { - cancellable = upstream.sampleBufferBus.addHandler { [pixelBufferBus] buffer in - let pixelBuffer = CMSampleBufferGetImageBuffer(buffer)! - let new = filter.perform(pixelBuffer: pixelBuffer) - pixelBufferBus.emit(element: new) - } - } else { - cancellable = upstream.sampleBufferBus.addHandler { [pixelBufferBus] buffer in - let pixelBuffer = CMSampleBufferGetImageBuffer(buffer)! - pixelBufferBus.emit(element: pixelBuffer) - } + ) { + self.upstream = upstream + + if (filter is NoPixelBufferModifier) == false { + Task { + self.cancellable = await upstream.sampleBufferBus.addHandler { [pixelBufferBus] buffer in + let pixelBuffer = CMSampleBufferGetImageBuffer(buffer)! + let new = filter.perform(pixelBuffer: pixelBuffer) + Task { + await pixelBufferBus.emit(element: new) + } + } + } + } else { + Task { + cancellable = await upstream.sampleBufferBus.addHandler { [pixelBufferBus] buffer in + let pixelBuffer = CMSampleBufferGetImageBuffer(buffer)! + Task { + await pixelBufferBus.emit(element: pixelBuffer) + } + } + } + } } - } - public convenience init( + private func setCancellables(_ cancellables: EventBusCancellable?) { + self.cancellable = cancellables + } + + + public convenience init( upstream: VideoDataOutput ) { self.init(upstream: upstream, filter: NoPixelBufferModifier()) @@ -40,7 +56,7 @@ public final class AnyCVPixelBufferOutput: _StatefulObjectBase, PixelBufferOutpu cancellable?.cancel() } - public func setUp(sessionInConfiguring: AVCaptureSession) { + public func setUp(sessionInConfiguring: AVCaptureSession) { upstream.setUp(sessionInConfiguring: sessionInConfiguring) } diff --git a/Capturer/Basic/Outputs/CVPixelBufferModifying.swift b/Capturer/Basic/Outputs/CVPixelBufferModifying.swift index 555001f..af9c3a7 100644 --- a/Capturer/Basic/Outputs/CVPixelBufferModifying.swift +++ b/Capturer/Basic/Outputs/CVPixelBufferModifying.swift @@ -7,7 +7,7 @@ public protocol CVPixelBufferModifying { } -public struct NoPixelBufferModifier: CVPixelBufferModifying { +public struct NoPixelBufferModifier: CVPixelBufferModifying, Sendable { init() { diff --git a/Capturer/Basic/Outputs/OutputNodeType.swift b/Capturer/Basic/Outputs/OutputNodeType.swift index f4a5761..224c0b6 100644 --- a/Capturer/Basic/Outputs/OutputNodeType.swift +++ b/Capturer/Basic/Outputs/OutputNodeType.swift @@ -2,7 +2,7 @@ import Foundation import AVFoundation -public protocol OutputNodeType: AnyObject { +public protocol OutputNodeType: AnyObject, Sendable { func setUp(sessionInConfiguring: AVCaptureSession) func tearDown(sessionInConfiguring: AVCaptureSession) } diff --git a/Capturer/Basic/Outputs/PhotoOutput.swift b/Capturer/Basic/Outputs/PhotoOutput.swift index 3bbc3b3..a673edb 100644 --- a/Capturer/Basic/Outputs/PhotoOutput.swift +++ b/Capturer/Basic/Outputs/PhotoOutput.swift @@ -1,4 +1,4 @@ -import AVFoundation +@preconcurrency import AVFoundation import UIKit import ImageIO @@ -6,9 +6,10 @@ import ImageIO An output node for photo capturing. Use ``PreviewOutput`` for previewing in UI. */ -public final class PhotoOutput: _StatefulObjectBase, OutputNodeType { - public struct CapturePhoto { +public final class PhotoOutput: _StatefulObjectBase, OutputNodeType, @unchecked Sendable { + + public struct CapturePhoto: Sendable { public let photo: AVCapturePhoto diff --git a/Capturer/Basic/Outputs/PreviewOutput.swift b/Capturer/Basic/Outputs/PreviewOutput.swift index cfdd491..32f72e7 100644 --- a/Capturer/Basic/Outputs/PreviewOutput.swift +++ b/Capturer/Basic/Outputs/PreviewOutput.swift @@ -1,6 +1,6 @@ import AVFoundation -open class PreviewOutput: VideoDataOutput { +open class PreviewOutput: VideoDataOutput, @unchecked Sendable { public struct State: Equatable { diff --git a/Capturer/Basic/Outputs/VideoDataOutput.swift b/Capturer/Basic/Outputs/VideoDataOutput.swift index cf4abaa..5e81757 100644 --- a/Capturer/Basic/Outputs/VideoDataOutput.swift +++ b/Capturer/Basic/Outputs/VideoDataOutput.swift @@ -1,14 +1,26 @@ -import AVFoundation +@preconcurrency import AVFoundation +@preconcurrency import CoreMedia + +extension CMSampleBuffer: @unchecked @retroactive Sendable {} + import Foundation -open class VideoDataOutput: _StatefulObjectBase, SampleBufferOutputNodeType, PixelBufferOutputNodeType { +open class VideoDataOutput: _StatefulObjectBase, SampleBufferOutputNodeType, PixelBufferOutputNodeType, @unchecked Sendable { public struct State: Equatable { public var isVideoMirrored: Bool = false } - private struct Handlers { - var didOutput: (CMSampleBuffer) -> Void = { _ in } + private actor Handlers { + var didOutput: @Sendable (CMSampleBuffer) -> Void = { _ in } + + func runOutput(buffer: CMSampleBuffer) { + self.didOutput(buffer) + } + + func setDidOutput(_ didOutput: @escaping @Sendable (CMSampleBuffer) -> Void) { + self.didOutput = didOutput + } } public let sampleBufferBus: EventBus = .init() @@ -35,13 +47,17 @@ open class VideoDataOutput: _StatefulObjectBase, SampleBufferOutputNodeType, Pix output.setSampleBufferDelegate(delegateProxy, queue: queue) - delegateProxy.handlers.didOutput = { [sampleBufferBus, pixelBufferBus] sampleBuffer in - sampleBufferBus.emit(element: sampleBuffer) + Task { + await delegateProxy.handlers.setDidOutput({ [sampleBufferBus, pixelBufferBus] sampleBuffer in + Task { + await sampleBufferBus.emit(element: sampleBuffer) - if pixelBufferBus.hasTargets { - pixelBufferBus.emit(element: sampleBuffer.takeCVPixelBuffer().unsafelyUnwrapped) + if await pixelBufferBus.hasTargets { + await pixelBufferBus.emit(element: sampleBuffer.takeCVPixelBuffer().unsafelyUnwrapped) + } + } + }) } - } observation = output.observe(\.connections, options: [.initial, .new]) { [weak self] output, _ in guard let self = self else { return } @@ -81,14 +97,17 @@ open class VideoDataOutput: _StatefulObjectBase, SampleBufferOutputNodeType, Pix private final class _AVCaptureVideoDataOutputSampleBufferDelegateProxy: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { - var handlers: Handlers = .init() + let handlers: Handlers = .init() func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { } func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { - handlers.didOutput(sampleBuffer) + let handlers = handlers + Task { + await handlers.runOutput(buffer: sampleBuffer) + } } } diff --git a/Capturer/Basic/Views/PixelBufferView.swift b/Capturer/Basic/Views/PixelBufferView.swift index 2464665..79d31a4 100644 --- a/Capturer/Basic/Views/PixelBufferView.swift +++ b/Capturer/Basic/Views/PixelBufferView.swift @@ -17,27 +17,29 @@ public final class PixelBufferView: UIView, PixelBufferDisplaying { fatalError("init(coder:) has not been implemented") } + @MainActor public func input(pixelBuffer: CVPixelBuffer) { - // TODO: Consider using dispatching - DispatchQueue.main.async { - /** - CALayer.contents supports displaying CVPixelBuffer implicitly. - */ - self.layer.contents = pixelBuffer - } + /** + CALayer.contents supports displaying CVPixelBuffer implicitly. + */ + self.layer.contents = pixelBuffer } @MainActor - public func attach(output: Output) { + public func attach(output: Output) { assert(Thread.isMainThread) subscription?.cancel() - subscription = output - .pixelBufferBus - .addHandler { [unowned self] pixelBuffer in - self.input(pixelBuffer: pixelBuffer) + Task { + subscription = await output + .pixelBufferBus + .addHandler { [unowned self] pixelBuffer in + Task { + await self.input(pixelBuffer: pixelBuffer) + } + } } } diff --git a/Capturer/Extended/AnyCMSampleBufferOutput.swift b/Capturer/Extended/AnyCMSampleBufferOutput.swift index a462511..02fb0b6 100644 --- a/Capturer/Extended/AnyCMSampleBufferOutput.swift +++ b/Capturer/Extended/AnyCMSampleBufferOutput.swift @@ -2,7 +2,7 @@ import Foundation import AVFoundation -public final class AnyCMSampleBufferOutput: _StatefulObjectBase, OutputNodeType { +public final class AnyCMSampleBufferOutput: _StatefulObjectBase, OutputNodeType, @unchecked Sendable { public var sampleBufferBus: EventBus { backing.sampleBufferBus diff --git a/Capturer/Extended/MapOutput.swift b/Capturer/Extended/MapOutput.swift index be394ac..ae90d82 100644 --- a/Capturer/Extended/MapOutput.swift +++ b/Capturer/Extended/MapOutput.swift @@ -2,7 +2,7 @@ import Foundation import AVFoundation -open class MapOutput: OutputNodeType { +open class MapOutput: OutputNodeType, @unchecked Sendable { open func perform(upstream: Upstream) -> Downstream { fatalError("Must be override") diff --git a/Capturer/Extended/PixelBufferTransform.swift b/Capturer/Extended/PixelBufferTransform.swift index 5dd392f..b33ea3b 100644 --- a/Capturer/Extended/PixelBufferTransform.swift +++ b/Capturer/Extended/PixelBufferTransform.swift @@ -3,7 +3,7 @@ import Foundation import CoreImage import AVFoundation -open class CoreImagePixelBufferTransform: MapOutput { +open class CoreImagePixelBufferTransform: MapOutput, @unchecked Sendable { private let ciContext = CIContext() // TODO: Use Metal diff --git a/ComponentCameraDemoSwiftUI/ContentView.swift b/ComponentCameraDemoSwiftUI/ContentView.swift index 809cb98..351cf8d 100644 --- a/ComponentCameraDemoSwiftUI/ContentView.swift +++ b/ComponentCameraDemoSwiftUI/ContentView.swift @@ -8,6 +8,8 @@ import Capturer import SwiftUI +extension CoreImageFilter: @retroactive @unchecked Sendable {} + @MainActor final class ViewModel: ObservableObject { @@ -28,8 +30,7 @@ final class ViewModel: ObservableObject { Task { - let input = CameraInput.wideAngleCamera(position: .back) - + let input = try CameraInput.bestBuiltInDevice(position: .back) await sessionManager.attach(input: input) await sessionManager.attach(output: output) await sessionManager.start() diff --git a/ComponentCameraDemoSwiftUI/PreviewLayerView.swift b/ComponentCameraDemoSwiftUI/PreviewLayerView.swift index c268d91..e098759 100644 --- a/ComponentCameraDemoSwiftUI/PreviewLayerView.swift +++ b/ComponentCameraDemoSwiftUI/PreviewLayerView.swift @@ -79,13 +79,17 @@ struct CustomPixelBufferView: UIViewRepresentable { func makeUIView(context: Context) -> View { let view = View() - let cancellable = context.coordinator.output.pixelBufferBus.addHandler { [weak view] (buffer) in - guard let view = view else { return } -// let p = CMSampleBufferGetImageBuffer(buffer)! - view.input(pixelBuffer: buffer) + Task { + let cancellable = await context.coordinator.output.pixelBufferBus.addHandler { [weak view] (buffer) in + guard let view = view else { return } + Task { + await view.input(pixelBuffer: buffer) + } + } + context.coordinator.cancellable = cancellable } - context.coordinator.cancellable = cancellable + return view } diff --git a/ComponentCameraDemoUIKit/Demo/DemoInputPreviewClassicViewController.swift b/ComponentCameraDemoUIKit/Demo/DemoInputPreviewClassicViewController.swift index da0e6cf..bf3881c 100644 --- a/ComponentCameraDemoUIKit/Demo/DemoInputPreviewClassicViewController.swift +++ b/ComponentCameraDemoUIKit/Demo/DemoInputPreviewClassicViewController.swift @@ -27,7 +27,7 @@ final class DemoInputPreviewClassicViewController: UIViewController { // Set Up - let input = CameraInput.wideAngleCamera(position: .back) + let input = try! CameraInput.bestBuiltInDevice(position: .back) let previewOutput = PreviewOutput() let photoOutput = PhotoOutput() @@ -37,7 +37,7 @@ final class DemoInputPreviewClassicViewController: UIViewController { Task { - await captureBody.batchAttaching( + captureBody.batchAttaching( input: input, outputs: [ previewOutput, diff --git a/Package.swift b/Package.swift index 479df72..05b9b3b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,9 +1,9 @@ -// swift-tools-version:5.5 +// swift-tools-version:6.0.0 import PackageDescription let package = Package( name: "Capturer", - platforms: [.iOS(.v13)], + platforms: [.iOS(.v16)], products: [ .library(name: "Capturer", targets: ["Capturer"]), ], diff --git a/Podfile b/Podfile index 0af19e6..f0f318e 100644 --- a/Podfile +++ b/Podfile @@ -5,7 +5,7 @@ target 'CapturerDemoSwiftUI' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! - pod 'Reveal-SDK' +# pod 'Reveal-SDK' end @@ -14,7 +14,7 @@ target 'CapturerDemoUIKit' do use_frameworks! pod 'MondrianLayout' - pod 'Reveal-SDK' + # pod 'Reveal-SDK' pod 'StorybookKit' pod 'StorybookUI' diff --git a/Podfile.lock b/Podfile.lock index ace2336..5b66ea2 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,29 +1,25 @@ PODS: - MondrianLayout (0.10.0) - - Reveal-SDK (41) - StorybookKit (1.7.0) - StorybookUI (1.7.0): - StorybookKit (>= 1.3.0) DEPENDENCIES: - MondrianLayout - - Reveal-SDK - StorybookKit - StorybookUI SPEC REPOS: trunk: - MondrianLayout - - Reveal-SDK - StorybookKit - StorybookUI SPEC CHECKSUMS: MondrianLayout: a817f7dd2f7c2663ba46d8944db09ac04bc39310 - Reveal-SDK: c33a66072cd9ac5f3e6fb94984743ab5f3057d28 StorybookKit: 6821c796c7abd909ea370dbf86eb7bd5087a9e04 StorybookUI: aa60ba1ea5a169f39134822cddd2248f3840b79e -PODFILE CHECKSUM: 3a18d5a639d2237c04cb63f0ffb69cbc2f8d9598 +PODFILE CHECKSUM: fc48912dd4589a3e8a84acff146b5616a7bdd393 -COCOAPODS: 1.12.1 +COCOAPODS: 1.16.2