Automatically handle Audio interruptions (#113)
* Remove audio device when interruption begins * Remove ReactLogger:alsoLogToJS * Fix ReactLogger.logJS calls * Fix `AVCaptureSessionInterruptionReasonKey` cast
This commit is contained in:
		| @@ -48,13 +48,14 @@ extension CameraView { | |||||||
|   /** |   /** | ||||||
|    Configures the CaptureSession and adds the audio device if it has not already been added yet. |    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 { |     if audioDeviceInput != nil { | ||||||
|       // we already added the audio device, don't add it again |       // we already added the audio device, don't add it again | ||||||
|       return |       return | ||||||
|     } |     } | ||||||
|     removeAudioInput() |     removeAudioInput() | ||||||
|  |  | ||||||
|  |     ReactLogger.log(level: .info, message: "Adding audio input...") | ||||||
|     captureSession.beginConfiguration() |     captureSession.beginConfiguration() | ||||||
|     guard let audioDevice = AVCaptureDevice.default(for: .audio) else { |     guard let audioDevice = AVCaptureDevice.default(for: .audio) else { | ||||||
|       throw CameraError.device(.microphoneUnavailable) |       throw CameraError.device(.microphoneUnavailable) | ||||||
| @@ -71,11 +72,12 @@ extension CameraView { | |||||||
|   /** |   /** | ||||||
|    Configures the CaptureSession and removes the audio device if it has been added before. |    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 { |     guard let audioInput = audioDeviceInput else { | ||||||
|       return |       return | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     ReactLogger.log(level: .info, message: "Removing audio input...") | ||||||
|     captureSession.beginConfiguration() |     captureSession.beginConfiguration() | ||||||
|     captureSession.removeInput(audioInput) |     captureSession.removeInput(audioInput) | ||||||
|     audioDeviceInput = nil |     audioDeviceInput = nil | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ extension CameraView { | |||||||
|    Configures the Capture Session. |    Configures the Capture Session. | ||||||
|    */ |    */ | ||||||
|   final func configureCaptureSession() { |   final func configureCaptureSession() { | ||||||
|     ReactLogger.logJS(level: .info, message: "Configuring Session...") |     ReactLogger.log(level: .info, message: "Configuring Session...") | ||||||
|     isReady = false |     isReady = false | ||||||
|  |  | ||||||
|     #if targetEnvironment(simulator) |     #if targetEnvironment(simulator) | ||||||
| @@ -113,14 +113,14 @@ extension CameraView { | |||||||
|  |  | ||||||
|     invokeOnInitialized() |     invokeOnInitialized() | ||||||
|     isReady = true |     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. |    Configures the Video Device with the given FPS, HDR and ColorSpace. | ||||||
|    */ |    */ | ||||||
|   final func configureDevice() { |   final func configureDevice() { | ||||||
|     ReactLogger.logJS(level: .info, message: "Configuring Device...") |     ReactLogger.log(level: .info, message: "Configuring Device...") | ||||||
|     guard let device = videoDeviceInput?.device else { |     guard let device = videoDeviceInput?.device else { | ||||||
|       return invokeOnError(.session(.cameraNotReady)) |       return invokeOnError(.session(.cameraNotReady)) | ||||||
|     } |     } | ||||||
| @@ -159,7 +159,7 @@ extension CameraView { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       device.unlockForConfiguration() |       device.unlockForConfiguration() | ||||||
|       ReactLogger.logJS(level: .info, message: "Device successfully configured!") |       ReactLogger.log(level: .info, message: "Device successfully configured!") | ||||||
|     } catch let error as NSError { |     } catch let error as NSError { | ||||||
|       return invokeOnError(.device(.configureError), cause: error) |       return invokeOnError(.device(.configureError), cause: error) | ||||||
|     } |     } | ||||||
| @@ -169,7 +169,7 @@ extension CameraView { | |||||||
|    Configures the Video Device to find the best matching Format. |    Configures the Video Device to find the best matching Format. | ||||||
|    */ |    */ | ||||||
|   final func configureFormat() { |   final func configureFormat() { | ||||||
|     ReactLogger.logJS(level: .info, message: "Configuring Format...") |     ReactLogger.log(level: .info, message: "Configuring Format...") | ||||||
|     guard let filter = self.format else { |     guard let filter = self.format else { | ||||||
|       // Format Filter was null. Ignore it. |       // Format Filter was null. Ignore it. | ||||||
|       return |       return | ||||||
| @@ -193,7 +193,7 @@ extension CameraView { | |||||||
|       try device.lockForConfiguration() |       try device.lockForConfiguration() | ||||||
|       device.activeFormat = format |       device.activeFormat = format | ||||||
|       device.unlockForConfiguration() |       device.unlockForConfiguration() | ||||||
|       ReactLogger.logJS(level: .info, message: "Format successfully configured!") |       ReactLogger.log(level: .info, message: "Format successfully configured!") | ||||||
|     } catch let error as NSError { |     } catch let error as NSError { | ||||||
|       return invokeOnError(.device(.configureError), cause: error) |       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 | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -46,6 +46,14 @@ final class CameraView: UIView { | |||||||
|                                            selector: #selector(sessionRuntimeError), |                                            selector: #selector(sessionRuntimeError), | ||||||
|                                            name: .AVCaptureSessionRuntimeError, |                                            name: .AVCaptureSessionRuntimeError, | ||||||
|                                            object: captureSession) |                                            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, |     NotificationCenter.default.addObserver(self, | ||||||
|                                            selector: #selector(audioSessionInterrupted), |                                            selector: #selector(audioSessionInterrupted), | ||||||
|                                            name: AVAudioSession.interruptionNotification, |                                            name: AVAudioSession.interruptionNotification, | ||||||
| @@ -56,6 +64,12 @@ final class CameraView: UIView { | |||||||
|     NotificationCenter.default.removeObserver(self, |     NotificationCenter.default.removeObserver(self, | ||||||
|                                               name: .AVCaptureSessionRuntimeError, |                                               name: .AVCaptureSessionRuntimeError, | ||||||
|                                               object: captureSession) |                                               object: captureSession) | ||||||
|  |     NotificationCenter.default.removeObserver(self, | ||||||
|  |                                               name: .AVCaptureSessionWasInterrupted, | ||||||
|  |                                               object: captureSession) | ||||||
|  |     NotificationCenter.default.removeObserver(self, | ||||||
|  |                                               name: .AVCaptureSessionInterruptionEnded, | ||||||
|  |                                               object: captureSession) | ||||||
|     NotificationCenter.default.removeObserver(self, |     NotificationCenter.default.removeObserver(self, | ||||||
|                                               name: AVAudioSession.interruptionNotification, |                                               name: AVAudioSession.interruptionNotification, | ||||||
|                                               object: AVAudioSession.sharedInstance) |                                               object: AVAudioSession.sharedInstance) | ||||||
| @@ -234,7 +248,7 @@ final class CameraView: UIView { | |||||||
|  |  | ||||||
|   // pragma MARK: Event Invokers |   // pragma MARK: Event Invokers | ||||||
|   internal final func invokeOnError(_ error: CameraError, cause: NSError? = nil) { |   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 } |     guard let onError = self.onError else { return } | ||||||
|  |  | ||||||
|     var causeDictionary: [String: Any]? |     var causeDictionary: [String: Any]? | ||||||
| @@ -254,7 +268,7 @@ final class CameraView: UIView { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   internal final func invokeOnInitialized() { |   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 } |     guard let onInitialized = self.onInitialized else { return } | ||||||
|     onInitialized([String: Any]()) |     onInitialized([String: Any]()) | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -15,21 +15,9 @@ let context = "VisionCamera" | |||||||
| enum ReactLogger { | enum ReactLogger { | ||||||
|   static func log(level: RCTLogLevel, |   static func log(level: RCTLogLevel, | ||||||
|                   message: String, |                   message: String, | ||||||
|                   alsoLogToJS: Bool = false, |  | ||||||
|                   _ file: String = #file, |                   _ file: String = #file, | ||||||
|                   _ lineNumber: Int = #line, |                   _ lineNumber: Int = #line, | ||||||
|                   _ function: String = #function) { |                   _ function: String = #function) { | ||||||
|     RCTDefaultLogFunction(level, RCTLogSource.native, file, lineNumber as NSNumber, "\(context).\(function): \(message)") |     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)") |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user