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.
*/
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

View File

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

View File

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

View File

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