From 4ea636e0d02325812851d3f9fdaf4a1dc97e9d07 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Mon, 29 Mar 2021 14:12:04 +0200 Subject: [PATCH] Automatically handle Audio interruptions (#113) * Remove audio device when interruption begins * Remove ReactLogger:alsoLogToJS * Fix ReactLogger.logJS calls * Fix `AVCaptureSessionInterruptionReasonKey` cast --- ios/CameraView+AVAudioSession.swift | 6 ++-- ios/CameraView+AVCaptureSession.swift | 48 +++++++++++++++++++++++---- ios/CameraView.swift | 18 ++++++++-- ios/React/ReactLogger.swift | 12 ------- 4 files changed, 62 insertions(+), 22 deletions(-) diff --git a/ios/CameraView+AVAudioSession.swift b/ios/CameraView+AVAudioSession.swift index 5096867..30ee3ad 100644 --- a/ios/CameraView+AVAudioSession.swift +++ b/ios/CameraView+AVAudioSession.swift @@ -48,13 +48,14 @@ extension CameraView { /** Configures the CaptureSession and adds the audio device if it has not already been added yet. */ - private final func addAudioInput() throws { + func addAudioInput() throws { if audioDeviceInput != nil { // we already added the audio device, don't add it again return } removeAudioInput() + ReactLogger.log(level: .info, message: "Adding audio input...") captureSession.beginConfiguration() guard let audioDevice = AVCaptureDevice.default(for: .audio) else { throw CameraError.device(.microphoneUnavailable) @@ -71,11 +72,12 @@ extension CameraView { /** Configures the CaptureSession and removes the audio device if it has been added before. */ - private final func removeAudioInput() { + func removeAudioInput() { guard let audioInput = audioDeviceInput else { return } + ReactLogger.log(level: .info, message: "Removing audio input...") captureSession.beginConfiguration() captureSession.removeInput(audioInput) audioDeviceInput = nil diff --git a/ios/CameraView+AVCaptureSession.swift b/ios/CameraView+AVCaptureSession.swift index 2a01ee2..a09606d 100644 --- a/ios/CameraView+AVCaptureSession.swift +++ b/ios/CameraView+AVCaptureSession.swift @@ -17,7 +17,7 @@ extension CameraView { Configures the Capture Session. */ final func configureCaptureSession() { - ReactLogger.logJS(level: .info, message: "Configuring Session...") + ReactLogger.log(level: .info, message: "Configuring Session...") isReady = false #if targetEnvironment(simulator) @@ -113,14 +113,14 @@ extension CameraView { invokeOnInitialized() isReady = true - ReactLogger.logJS(level: .info, message: "Session successfully configured!") + ReactLogger.log(level: .info, message: "Session successfully configured!") } /** Configures the Video Device with the given FPS, HDR and ColorSpace. */ final func configureDevice() { - ReactLogger.logJS(level: .info, message: "Configuring Device...") + ReactLogger.log(level: .info, message: "Configuring Device...") guard let device = videoDeviceInput?.device else { return invokeOnError(.session(.cameraNotReady)) } @@ -159,7 +159,7 @@ extension CameraView { } device.unlockForConfiguration() - ReactLogger.logJS(level: .info, message: "Device successfully configured!") + ReactLogger.log(level: .info, message: "Device successfully configured!") } catch let error as NSError { return invokeOnError(.device(.configureError), cause: error) } @@ -169,7 +169,7 @@ extension CameraView { Configures the Video Device to find the best matching Format. */ final func configureFormat() { - ReactLogger.logJS(level: .info, message: "Configuring Format...") + ReactLogger.log(level: .info, message: "Configuring Format...") guard let filter = self.format else { // Format Filter was null. Ignore it. return @@ -193,7 +193,7 @@ extension CameraView { try device.lockForConfiguration() device.activeFormat = format device.unlockForConfiguration() - ReactLogger.logJS(level: .info, message: "Format successfully configured!") + ReactLogger.log(level: .info, message: "Format successfully configured!") } catch let error as NSError { return invokeOnError(.device(.configureError), cause: error) } @@ -215,4 +215,40 @@ extension CameraView { } } } + + @objc + func sessionInterruptionBegin(notification: Notification) { + ReactLogger.log(level: .error, message: "Capture Session Interruption begin Notification!") + guard let reasonNumber = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as? NSNumber else { + return + } + let reason = AVCaptureSession.InterruptionReason(rawValue: reasonNumber.intValue) + + switch reason { + case .audioDeviceInUseByAnotherClient: + // remove audio input so iOS thinks nothing is wrong and won't pause the session. + removeAudioInput() + default: + // don't do anything, iOS will automatically pause session + break + } + } + + @objc + func sessionInterruptionEnd(notification: Notification) { + ReactLogger.log(level: .error, message: "Capture Session Interruption end Notification!") + guard let reasonNumber = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as? NSNumber else { + return + } + let reason = AVCaptureSession.InterruptionReason(rawValue: reasonNumber.intValue) + + switch reason { + case .audioDeviceInUseByAnotherClient: + // add audio again because we removed it when we received the interruption. + configureAudioSession() + default: + // don't do anything, iOS will automatically resume session + break + } + } } diff --git a/ios/CameraView.swift b/ios/CameraView.swift index 10c4df8..d93c8cf 100644 --- a/ios/CameraView.swift +++ b/ios/CameraView.swift @@ -46,6 +46,14 @@ final class CameraView: UIView { selector: #selector(sessionRuntimeError), name: .AVCaptureSessionRuntimeError, object: captureSession) + NotificationCenter.default.addObserver(self, + selector: #selector(sessionInterruptionBegin), + name: .AVCaptureSessionWasInterrupted, + object: captureSession) + NotificationCenter.default.addObserver(self, + selector: #selector(sessionInterruptionEnd), + name: .AVCaptureSessionInterruptionEnded, + object: captureSession) NotificationCenter.default.addObserver(self, selector: #selector(audioSessionInterrupted), name: AVAudioSession.interruptionNotification, @@ -56,6 +64,12 @@ final class CameraView: UIView { NotificationCenter.default.removeObserver(self, name: .AVCaptureSessionRuntimeError, object: captureSession) + NotificationCenter.default.removeObserver(self, + name: .AVCaptureSessionWasInterrupted, + object: captureSession) + NotificationCenter.default.removeObserver(self, + name: .AVCaptureSessionInterruptionEnded, + object: captureSession) NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance) @@ -234,7 +248,7 @@ final class CameraView: UIView { // pragma MARK: Event Invokers internal final func invokeOnError(_ error: CameraError, cause: NSError? = nil) { - ReactLogger.log(level: .error, message: "Invoking onError(): \(error.message)", alsoLogToJS: true) + ReactLogger.log(level: .error, message: "Invoking onError(): \(error.message)") guard let onError = self.onError else { return } var causeDictionary: [String: Any]? @@ -254,7 +268,7 @@ final class CameraView: UIView { } internal final func invokeOnInitialized() { - ReactLogger.log(level: .info, message: "Camera initialized!", alsoLogToJS: true) + ReactLogger.log(level: .info, message: "Camera initialized!") guard let onInitialized = self.onInitialized else { return } onInitialized([String: Any]()) } diff --git a/ios/React/ReactLogger.swift b/ios/React/ReactLogger.swift index 068f2da..3ac2a0f 100644 --- a/ios/React/ReactLogger.swift +++ b/ios/React/ReactLogger.swift @@ -15,21 +15,9 @@ let context = "VisionCamera" enum ReactLogger { static func log(level: RCTLogLevel, message: String, - alsoLogToJS: Bool = false, _ file: String = #file, _ lineNumber: Int = #line, _ function: String = #function) { RCTDefaultLogFunction(level, RCTLogSource.native, file, lineNumber as NSNumber, "\(context).\(function): \(message)") - if alsoLogToJS { - ReactLogger.logJS(level: level, message: message, file, lineNumber, function) - } - } - - static func logJS(level: RCTLogLevel, - message: String, - _ file: String = #file, - _ lineNumber: Int = #line, - _ function: String = #function) { - RCTDefaultLogFunction(level, RCTLogSource.javaScript, file, lineNumber as NSNumber, "\(context).\(function): \(message)") } }