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:
Marc Rousavy 2021-03-29 14:12:04 +02:00 committed by GitHub
parent cd180dc73b
commit 4ea636e0d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 62 additions and 22 deletions

View File

@ -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

View File

@ -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
}
}
} }

View File

@ -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]())
} }

View File

@ -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)")
} }
} }