session-ios/SessionMessagingKit/Utilities/OWSAudioSession.swift

170 lines
5.6 KiB
Swift
Raw Normal View History

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
2021-01-22 04:25:23 +01:00
import AVFoundation
import SignalCoreKit
import SessionUtilitiesKit
2018-10-23 16:40:09 +02:00
@objc(OWSAudioActivity)
public class AudioActivity: NSObject {
let audioDescription: String
2018-10-23 16:40:09 +02:00
let behavior: OWSAudioBehavior
2018-05-25 21:15:19 +02:00
@objc
2018-10-23 16:40:09 +02:00
public init(audioDescription: String, behavior: OWSAudioBehavior) {
self.audioDescription = audioDescription
2018-10-23 16:40:09 +02:00
self.behavior = behavior
}
deinit {
Environment.shared?.audioSession.ensureAudioSessionActivationStateAfterDelay()
}
2018-10-23 01:17:05 +02:00
// MARK:
override public var description: String {
return "<\(self.logTag) audioDescription: \"\(audioDescription)\">"
}
}
@objc
public class OWSAudioSession: NSObject {
@objc
2018-10-23 01:17:05 +02:00
public func setup() {
2019-03-30 14:22:31 +01:00
NotificationCenter.default.addObserver(self, selector: #selector(proximitySensorStateDidChange(notification:)), name: UIDevice.proximityStateDidChangeNotification, object: nil)
}
2018-10-23 01:17:05 +02:00
// MARK: Dependencies
2018-10-23 01:17:05 +02:00
private let avAudioSession = AVAudioSession.sharedInstance()
2018-10-23 01:17:05 +02:00
private let device = UIDevice.current
2018-10-23 01:17:05 +02:00
// MARK:
private var currentActivities: [Weak<AudioActivity>] = []
2018-10-23 16:40:09 +02:00
var aggregateBehaviors: Set<OWSAudioBehavior> {
return Set(self.currentActivities.compactMap { $0.value?.behavior })
}
2018-05-25 21:15:19 +02:00
@objc
2018-10-23 01:17:05 +02:00
public func startAudioActivity(_ audioActivity: AudioActivity) -> Bool {
Logger.debug("with \(audioActivity)")
objc_sync_enter(self)
defer { objc_sync_exit(self) }
2018-10-23 01:17:05 +02:00
self.currentActivities.append(Weak(value: audioActivity))
do {
try ensureAudioCategory()
return true
} catch {
2018-08-27 16:27:48 +02:00
owsFailDebug("failed with error: \(error)")
return false
}
2018-10-23 01:17:05 +02:00
}
@objc
public func endAudioActivity(_ audioActivity: AudioActivity) {
Logger.debug("with audioActivity: \(audioActivity)")
objc_sync_enter(self)
defer { objc_sync_exit(self) }
currentActivities = currentActivities.filter { return $0.value != audioActivity }
do {
try ensureAudioCategory()
} catch {
owsFailDebug("error in ensureAudioCategory: \(error)")
2018-10-23 01:17:05 +02:00
}
}
func ensureAudioCategory() throws {
2018-10-23 16:40:09 +02:00
if aggregateBehaviors.contains(.audioMessagePlayback) {
Environment.shared?.proximityMonitoringManager.add(lifetime: self)
} else {
Environment.shared?.proximityMonitoringManager.remove(lifetime: self)
}
if aggregateBehaviors.contains(.call) {
// Do nothing while on a call.
// WebRTC/CallAudioService manages call audio
// Eventually it would be nice to consolidate more of the audio
// session handling.
} else if aggregateBehaviors.contains(.playAndRecord) {
2019-03-30 14:22:31 +01:00
assert(avAudioSession.recordPermission == .granted)
2019-03-30 15:05:02 +01:00
try avAudioSession.setCategory(.record)
} else if aggregateBehaviors.contains(.audioMessagePlayback) {
if self.device.proximityState {
Logger.debug("proximityState: true")
2019-03-30 15:05:02 +01:00
try avAudioSession.setCategory(.playAndRecord)
try avAudioSession.overrideOutputAudioPort(.none)
} else {
Logger.debug("proximityState: false")
2019-03-30 15:05:02 +01:00
try avAudioSession.setCategory(.playback)
2018-10-23 01:17:05 +02:00
}
} else if aggregateBehaviors.contains(.playback) {
2019-03-30 15:05:02 +01:00
try avAudioSession.setCategory(.playback)
} else {
ensureAudioSessionActivationStateAfterDelay()
2018-10-23 01:17:05 +02:00
}
}
2018-05-25 21:15:19 +02:00
@objc
func proximitySensorStateDidChange(notification: Notification) {
do {
try ensureAudioCategory()
} catch {
owsFailDebug("error in response to proximity change: \(error)")
2018-10-23 01:17:05 +02:00
}
}
fileprivate func ensureAudioSessionActivationStateAfterDelay() {
// Without this delay, we sometimes error when deactivating the audio session with:
// Error Domain=NSOSStatusErrorDomain Code=560030580 The operation couldnt be completed. (OSStatus error 560030580.)
// aka "AVAudioSessionErrorCodeIsBusy"
2020-12-09 04:59:14 +01:00
// FIXME: The code below was causing a bug, and disabling it * seems * fine. Don't feel super confident about it though...
/*
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
self.ensureAudioSessionActivationState()
}
2020-12-09 04:59:14 +01:00
*/
}
private func ensureAudioSessionActivationState() {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
// Cull any stale activities
2018-06-01 20:20:48 +02:00
currentActivities = currentActivities.compactMap { oldActivity in
guard oldActivity.value != nil else {
// Normally we should be explicitly stopping an audio activity, but this allows
// for recovery if the owner of the AudioAcivity was GC'd without ending it's
// audio activity
2018-08-23 16:37:34 +02:00
Logger.warn("an old activity has been gc'd")
return nil
}
// return any still-active activities
return oldActivity
}
guard currentActivities.isEmpty else {
2018-08-24 18:40:16 +02:00
Logger.debug("not deactivating due to currentActivities: \(currentActivities)")
return
}
do {
// When playing audio in Signal, other apps audio (e.g. Music) is paused.
// By notifying when we deactivate, the other app can resume playback.
2019-03-30 14:22:31 +01:00
try avAudioSession.setActive(false, options: [.notifyOthersOnDeactivation])
} catch {
2018-08-27 16:27:48 +02:00
owsFailDebug("failed with error: \(error)")
}
}
}