Error when Audio Input is in use by another app (#111)

* Remove Audio Device if it failed to configure

* Add `audio-in-use-by-other-app` error

* Try removing on interruption

* Format code

* Make error more clear
This commit is contained in:
Marc Rousavy 2021-03-29 11:32:00 +02:00 committed by GitHub
parent 12f6ab5217
commit 1558dd2f15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 52 additions and 31 deletions

View File

@ -154,6 +154,7 @@ enum FormatError {
enum SessionError { enum SessionError {
case cameraNotReady case cameraNotReady
case audioSessionSetupFailed(reason: String) case audioSessionSetupFailed(reason: String)
case audioInUseByOtherApp
// MARK: Internal // MARK: Internal
@ -163,6 +164,8 @@ enum SessionError {
return "camera-not-ready" return "camera-not-ready"
case .audioSessionSetupFailed: case .audioSessionSetupFailed:
return "audio-session-setup-failed" return "audio-session-setup-failed"
case .audioInUseByOtherApp:
return "audio-in-use-by-other-app"
} }
} }
@ -172,6 +175,8 @@ enum SessionError {
return "The Camera is not ready yet! Wait for the onInitialized() callback!" return "The Camera is not ready yet! Wait for the onInitialized() callback!"
case let .audioSessionSetupFailed(reason): case let .audioSessionSetupFailed(reason):
return "The audio session failed to setup! \(reason)" return "The audio session failed to setup! \(reason)"
case .audioInUseByOtherApp:
return "The audio session is already in use by another app with higher priority!"
} }
} }
} }

View File

@ -19,7 +19,7 @@ extension CameraView {
final func configureAudioSession() { final func configureAudioSession() {
let start = DispatchTime.now() let start = DispatchTime.now()
do { do {
setAutomaticallyConfiguresAudioSession(false) try addAudioInput()
let audioSession = AVAudioSession.sharedInstance() let audioSession = AVAudioSession.sharedInstance()
if audioSession.category != .playAndRecord { if audioSession.category != .playAndRecord {
// allow background music playback // allow background music playback
@ -30,25 +30,57 @@ extension CameraView {
// activate current audio session because camera is active // activate current audio session because camera is active
try audioSession.setActive(true) try audioSession.setActive(true)
} catch let error as NSError { } catch let error as NSError {
switch error.code {
case 561_017_449:
self.invokeOnError(.session(.audioInUseByOtherApp), cause: error)
default:
self.invokeOnError(.session(.audioSessionSetupFailed(reason: error.description)), cause: error) self.invokeOnError(.session(.audioSessionSetupFailed(reason: error.description)), cause: error)
setAutomaticallyConfiguresAudioSession(true) }
self.removeAudioInput()
} }
let end = DispatchTime.now() let end = DispatchTime.now()
let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds
ReactLogger.log(level: .info, message: "Configured Audio session in \(Double(nanoTime) / 1_000_000)ms!") ReactLogger.log(level: .info, message: "Configured Audio session in \(Double(nanoTime) / 1_000_000)ms!")
} }
private final func setAutomaticallyConfiguresAudioSession(_ automaticallyConfiguresAudioSession: Bool) { /**
if captureSession.automaticallyConfiguresApplicationAudioSession != automaticallyConfiguresAudioSession { Configures the CaptureSession and adds the audio device if it has not already been added yet.
*/
private final func addAudioInput() throws {
if audioDeviceInput != nil {
// we already added the audio device, don't add it again
return
}
removeAudioInput()
captureSession.beginConfiguration() captureSession.beginConfiguration()
captureSession.automaticallyConfiguresApplicationAudioSession = automaticallyConfiguresAudioSession guard let audioDevice = AVCaptureDevice.default(for: .audio) else {
throw CameraError.device(.microphoneUnavailable)
}
audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice)
guard captureSession.canAddInput(audioDeviceInput!) else {
throw CameraError.parameter(.unsupportedInput(inputDescriptor: "audio-input"))
}
captureSession.addInput(audioDeviceInput!)
captureSession.automaticallyConfiguresApplicationAudioSession = true
captureSession.commitConfiguration() captureSession.commitConfiguration()
} }
/**
Configures the CaptureSession and removes the audio device if it has been added before.
*/
private final func removeAudioInput() {
guard let audioInput = audioDeviceInput else {
return
}
captureSession.beginConfiguration()
captureSession.removeInput(audioInput)
audioDeviceInput = nil
captureSession.commitConfiguration()
} }
@objc @objc
func audioSessionInterrupted(notification: Notification) { func audioSessionInterrupted(notification: Notification) {
ReactLogger.log(level: .error, message: "The Audio Session was interrupted!") ReactLogger.log(level: .error, message: "Audio Session Interruption Notification!")
guard let userInfo = notification.userInfo, guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
@ -56,13 +88,15 @@ extension CameraView {
} }
switch type { switch type {
case .began: case .began:
// TODO: Should we also disable the camera here? I think it will throw a runtime error // Something interrupted our Audio Session, stop recording audio.
// disable audio session ReactLogger.log(level: .error, message: "The Audio Session was interrupted!")
try? AVAudioSession.sharedInstance().setActive(false) removeAudioInput()
case .ended: case .ended:
ReactLogger.log(level: .error, message: "The Audio Session interruption has ended.")
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return } guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) { if options.contains(.shouldResume) {
ReactLogger.log(level: .error, message: "Resuming interrupted Audio Session...")
// restart audio session because interruption is over // restart audio session because interruption is over
configureAudioSession() configureAudioSession()
} else { } else {

View File

@ -73,24 +73,6 @@ extension CameraView {
return invokeOnError(.device(.invalid)) return invokeOnError(.device(.invalid))
} }
// Microphone (Audio Input)
do {
if let audioDeviceInput = self.audioDeviceInput {
captureSession.removeInput(audioDeviceInput)
}
guard let audioDevice = AVCaptureDevice.default(for: .audio) else {
return invokeOnError(.device(.microphoneUnavailable))
}
audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice)
guard captureSession.canAddInput(audioDeviceInput!) else {
return invokeOnError(.parameter(.unsupportedInput(inputDescriptor: "audio-input")))
}
captureSession.addInput(audioDeviceInput!)
} catch {
return invokeOnError(.device(.invalid))
}
// OUTPUTS // OUTPUTS
if let photoOutput = self.photoOutput { if let photoOutput = self.photoOutput {
captureSession.removeOutput(photoOutput) captureSession.removeOutput(photoOutput)

View File

@ -20,7 +20,7 @@ export type FormatError =
| 'format/invalid-low-light-boost' | 'format/invalid-low-light-boost'
| 'format/invalid-format' | 'format/invalid-format'
| 'format/invalid-preset'; | 'format/invalid-preset';
export type SessionError = 'session/camera-not-ready' | 'session/audio-session-setup-failed'; export type SessionError = 'session/camera-not-ready' | 'session/audio-session-setup-failed' | 'session/audio-in-use-by-other-app';
export type CaptureError = export type CaptureError =
| 'capture/invalid-photo-format' | 'capture/invalid-photo-format'
| 'capture/encoder-error' | 'capture/encoder-error'