diff --git a/HaishinKit/Sources/Mixer/CaptureSession.swift b/HaishinKit/Sources/Mixer/CaptureSession.swift index 69c7bfdf4..e96fc1db1 100644 --- a/HaishinKit/Sources/Mixer/CaptureSession.swift +++ b/HaishinKit/Sources/Mixer/CaptureSession.swift @@ -1,16 +1,5 @@ import AVFoundation -protocol CaptureSessionDelegate: AnyObject { - @available(tvOS 17.0, *) - func captureSession(_ session: CaptureSession, sessionRuntimeError session: AVCaptureSession, error: AVError) - #if os(iOS) || os(tvOS) || os(visionOS) - @available(tvOS 17.0, *) - func captureSession(_ session: CaptureSession, sessionWasInterrupted session: AVCaptureSession, reason: AVCaptureSession.InterruptionReason?) - @available(tvOS 17.0, *) - func captureSession(_ session: CaptureSession, sessionInterruptionEnded session: AVCaptureSession) - #endif -} - final class CaptureSession { #if os(iOS) || os(tvOS) static var isMultiCamSupported: Bool { @@ -35,11 +24,11 @@ final class CaptureSession { } } } - @available(tvOS 17.0, *) var isMultitaskingCameraAccessEnabled: Bool { return session.isMultitaskingCameraAccessEnabled } + #elseif os(macOS) let isMultiCamSessionEnabled = true let isMultitaskingCameraAccessEnabled = true @@ -48,9 +37,20 @@ final class CaptureSession { let isMultitaskingCameraAccessEnabled = false #endif - weak var delegate: (any CaptureSessionDelegate)? private(set) var isRunning = false + var isInturreped: AsyncStream { + AsyncStream { continuation in + isInturrepedContinutation = continuation + } + } + + var runtimeError: AsyncStream { + AsyncStream { continutation in + runtimeErrorContinutation = continutation + } + } + #if os(tvOS) private var _session: Any? /// The capture session instance. @@ -108,6 +108,18 @@ final class CaptureSession { #endif } + private var isInturrepedContinutation: AsyncStream.Continuation? { + didSet { + oldValue?.finish() + } + } + + private var runtimeErrorContinutation: AsyncStream.Continuation? { + didSet { + oldValue?.finish() + } + } + deinit { guard #available(tvOS 17.0, *) else { return @@ -225,6 +237,7 @@ final class CaptureSession { NotificationCenter.default.removeObserver(self, name: .AVCaptureSessionInterruptionEnded, object: session) #endif NotificationCenter.default.removeObserver(self, name: .AVCaptureSessionRuntimeError, object: session) + runtimeErrorContinutation = nil } @available(tvOS 17.0, *) @@ -235,8 +248,7 @@ final class CaptureSession { let errorValue = notification.userInfo?[AVCaptureSessionErrorKey] as? NSError else { return } - let error = AVError(_nsError: errorValue) - delegate?.captureSession(self, sessionRuntimeError: session, error: error) + runtimeErrorContinutation?.yield(AVError(_nsError: errorValue)) } #if os(iOS) || os(tvOS) || os(visionOS) @@ -246,19 +258,13 @@ final class CaptureSession { guard let session = notification.object as? AVCaptureSession else { return } - guard let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?, - let reasonIntegerValue = userInfoValue.integerValue, - let reason = AVCaptureSession.InterruptionReason(rawValue: reasonIntegerValue) else { - delegate?.captureSession(self, sessionWasInterrupted: session, reason: nil) - return - } - delegate?.captureSession(self, sessionWasInterrupted: session, reason: reason) + isInturrepedContinutation?.yield(true) } @available(tvOS 17.0, *) @objc private func sessionInterruptionEnded(_ notification: Notification) { - delegate?.captureSession(self, sessionInterruptionEnded: session) + isInturrepedContinutation?.yield(false) } #endif } diff --git a/HaishinKit/Sources/Mixer/MediaMixer.swift b/HaishinKit/Sources/Mixer/MediaMixer.swift index f83d940d0..1fea9eb71 100644 --- a/HaishinKit/Sources/Mixer/MediaMixer.swift +++ b/HaishinKit/Sources/Mixer/MediaMixer.swift @@ -83,6 +83,11 @@ public final actor MediaMixer { session.isRunning } + /// The interrupts events is occured or not. + public var isInterputted: AsyncStream { + session.isInturreped + } + #if os(iOS) || os(macOS) /// The video orientation for stream. public var videoOrientation: AVCaptureVideoOrientation { @@ -383,6 +388,41 @@ public final actor MediaMixer { } } #endif + + @available(tvOS 17.0, *) + private func sessionRuntimeErrorOccured(_ error: AVError) async { + switch error.code { + #if os(iOS) || os(tvOS) || os(visionOS) + case .mediaServicesWereReset: + session.startRunningIfNeeded() + #endif + #if os(iOS) || os(tvOS) || os(macOS) + case .unsupportedDeviceActiveFormat: + guard let device = error.device, let format = device.videoFormat( + width: session.sessionPreset.width ?? Int32.max, + height: session.sessionPreset.height ?? Int32.max, + frameRate: videoIO.frameRate, + isMultiCamSupported: session.isMultiCamSessionEnabled + ), device.activeFormat != format else { + return + } + do { + try device.lockForConfiguration() + device.activeFormat = format + if format.isFrameRateSupported(videoIO.frameRate) { + device.activeVideoMinFrameDuration = CMTime(value: 100, timescale: CMTimeScale(100 * videoIO.frameRate)) + device.activeVideoMaxFrameDuration = CMTime(value: 100, timescale: CMTimeScale(100 * videoIO.frameRate)) + } + device.unlockForConfiguration() + session.startRunningIfNeeded() + } catch { + logger.warn(error) + } + #endif + default: + break + } + } } extension MediaMixer: AsyncRunner { @@ -421,6 +461,13 @@ extension MediaMixer: AsyncRunner { } } } + if #available(tvOS 17.0, *) { + Task { + for await runtimeError in session.runtimeError { + await sessionRuntimeErrorOccured(runtimeError) + } + } + } setVideoRenderingMode(videoMixerSettings.mode) if useManualCapture { session.startRunning()