2018-02-03 00:35:32 +01:00
|
|
|
|
//
|
2019-03-30 14:24:40 +01:00
|
|
|
|
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
2018-02-03 00:35:32 +01:00
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
2021-01-22 04:25:23 +01:00
|
|
|
|
import AVFoundation
|
2018-02-03 00:35:32 +01:00
|
|
|
|
|
2018-10-23 16:40:09 +02:00
|
|
|
|
@objc(OWSAudioActivity)
|
2018-02-16 21:32:29 +01:00
|
|
|
|
public class AudioActivity: NSObject {
|
|
|
|
|
let audioDescription: String
|
|
|
|
|
|
2018-10-23 16:40:09 +02:00
|
|
|
|
let behavior: OWSAudioBehavior
|
2018-02-16 21:32:29 +01:00
|
|
|
|
|
2018-05-25 21:15:19 +02:00
|
|
|
|
@objc
|
2018-10-23 16:40:09 +02:00
|
|
|
|
public init(audioDescription: String, behavior: OWSAudioBehavior) {
|
2018-02-16 21:32:29 +01:00
|
|
|
|
self.audioDescription = audioDescription
|
2018-10-23 16:40:09 +02:00
|
|
|
|
self.behavior = behavior
|
2018-02-16 21:32:29 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deinit {
|
2018-10-23 02:55:39 +02:00
|
|
|
|
audioSession.ensureAudioSessionActivationStateAfterDelay()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Dependencies
|
|
|
|
|
|
|
|
|
|
var audioSession: OWSAudioSession {
|
2021-08-02 07:43:50 +02:00
|
|
|
|
return Environment.shared.audioSessionRef
|
2018-02-16 21:32:29 +01:00
|
|
|
|
}
|
2018-10-23 01:17:05 +02:00
|
|
|
|
|
|
|
|
|
// MARK:
|
|
|
|
|
|
|
|
|
|
override public var description: String {
|
|
|
|
|
return "<\(self.logTag) audioDescription: \"\(audioDescription)\">"
|
|
|
|
|
}
|
2018-02-16 21:32:29 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-03 00:35:32 +01:00
|
|
|
|
@objc
|
|
|
|
|
public class OWSAudioSession: NSObject {
|
|
|
|
|
|
2018-10-23 02:55:39 +02:00
|
|
|
|
@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-03-05 23:59:09 +01:00
|
|
|
|
}
|
2018-03-06 16:12:55 +01:00
|
|
|
|
|
2018-10-23 01:17:05 +02:00
|
|
|
|
// MARK: Dependencies
|
2018-02-03 00:35:32 +01:00
|
|
|
|
|
2018-10-24 22:38:05 +02:00
|
|
|
|
var proximityMonitoringManager: OWSProximityMonitoringManager {
|
|
|
|
|
return Environment.shared.proximityMonitoringManager
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-23 01:17:05 +02:00
|
|
|
|
private let avAudioSession = AVAudioSession.sharedInstance()
|
2018-03-20 01:16:22 +01:00
|
|
|
|
|
2018-10-23 01:17:05 +02:00
|
|
|
|
private let device = UIDevice.current
|
2018-02-16 21:32:29 +01:00
|
|
|
|
|
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-02-03 00:35:32 +01:00
|
|
|
|
}
|
|
|
|
|
|
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)")
|
2018-02-03 00:35:32 +01:00
|
|
|
|
|
2018-03-20 01:16:22 +01:00
|
|
|
|
objc_sync_enter(self)
|
|
|
|
|
defer { objc_sync_exit(self) }
|
|
|
|
|
|
2018-10-23 01:17:05 +02:00
|
|
|
|
self.currentActivities.append(Weak(value: audioActivity))
|
2018-02-16 21:32:29 +01:00
|
|
|
|
|
2018-02-03 00:35:32 +01:00
|
|
|
|
do {
|
2018-10-24 22:38:05 +02:00
|
|
|
|
try ensureAudioCategory()
|
2018-02-03 00:35:32 +01:00
|
|
|
|
return true
|
|
|
|
|
} catch {
|
2018-08-27 16:27:48 +02:00
|
|
|
|
owsFailDebug("failed with error: \(error)")
|
2018-02-03 00:35:32 +01:00
|
|
|
|
return false
|
|
|
|
|
}
|
2018-10-23 01:17:05 +02:00
|
|
|
|
}
|
2018-02-16 21:32:29 +01:00
|
|
|
|
|
2018-10-23 02:56:22 +02:00
|
|
|
|
@objc
|
2018-10-24 22:38:05 +02:00
|
|
|
|
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 }
|
2018-10-23 02:56:22 +02:00
|
|
|
|
do {
|
2018-10-24 22:38:05 +02:00
|
|
|
|
try ensureAudioCategory()
|
2018-10-23 02:56:22 +02:00
|
|
|
|
} catch {
|
2018-10-24 22:38:05 +02:00
|
|
|
|
owsFailDebug("error in ensureAudioCategory: \(error)")
|
2018-10-23 01:17:05 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-03-20 01:16:22 +01:00
|
|
|
|
|
2018-10-24 22:38:05 +02:00
|
|
|
|
func ensureAudioCategory() throws {
|
2018-10-23 16:40:09 +02:00
|
|
|
|
if aggregateBehaviors.contains(.audioMessagePlayback) {
|
2018-10-24 22:38:05 +02:00
|
|
|
|
self.proximityMonitoringManager.add(lifetime: self)
|
|
|
|
|
} else {
|
|
|
|
|
self.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)
|
2018-10-24 22:38:05 +02:00
|
|
|
|
} else if aggregateBehaviors.contains(.audioMessagePlayback) {
|
2018-10-23 02:56:22 +02:00
|
|
|
|
if self.device.proximityState {
|
|
|
|
|
Logger.debug("proximityState: true")
|
|
|
|
|
|
2019-03-30 15:05:02 +01:00
|
|
|
|
try avAudioSession.setCategory(.playAndRecord)
|
2018-10-23 02:56:22 +02:00
|
|
|
|
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
|
|
|
|
}
|
2018-10-24 22:38:05 +02:00
|
|
|
|
} else if aggregateBehaviors.contains(.playback) {
|
2019-03-30 15:05:02 +01:00
|
|
|
|
try avAudioSession.setCategory(.playback)
|
2018-10-24 22:38:05 +02:00
|
|
|
|
} else {
|
|
|
|
|
ensureAudioSessionActivationStateAfterDelay()
|
2018-10-23 01:17:05 +02:00
|
|
|
|
}
|
2018-02-16 21:32:29 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-05-25 21:15:19 +02:00
|
|
|
|
@objc
|
2018-10-24 22:38:05 +02:00
|
|
|
|
func proximitySensorStateDidChange(notification: Notification) {
|
2018-10-23 02:56:22 +02:00
|
|
|
|
do {
|
2018-10-24 22:38:05 +02:00
|
|
|
|
try ensureAudioCategory()
|
2018-10-23 02:56:22 +02:00
|
|
|
|
} catch {
|
2018-10-24 22:38:05 +02:00
|
|
|
|
owsFailDebug("error in response to proximity change: \(error)")
|
2018-10-23 01:17:05 +02:00
|
|
|
|
}
|
2018-02-16 21:32:29 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-03-20 01:16:22 +01:00
|
|
|
|
fileprivate func ensureAudioSessionActivationStateAfterDelay() {
|
|
|
|
|
// Without this delay, we sometimes error when deactivating the audio session with:
|
|
|
|
|
// Error Domain=NSOSStatusErrorDomain Code=560030580 “The operation couldn’t 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...
|
|
|
|
|
/*
|
2018-03-20 01:16:22 +01:00
|
|
|
|
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
|
|
|
|
|
self.ensureAudioSessionActivationState()
|
|
|
|
|
}
|
2020-12-09 04:59:14 +01:00
|
|
|
|
*/
|
2018-03-20 01:16:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func ensureAudioSessionActivationState() {
|
|
|
|
|
objc_sync_enter(self)
|
|
|
|
|
defer { objc_sync_exit(self) }
|
|
|
|
|
|
2018-02-16 21:32:29 +01:00
|
|
|
|
// Cull any stale activities
|
2018-06-01 20:20:48 +02:00
|
|
|
|
currentActivities = currentActivities.compactMap { oldActivity in
|
2018-02-16 21:32:29 +01:00
|
|
|
|
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")
|
2018-02-16 21:32:29 +01:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// return any still-active activities
|
|
|
|
|
return oldActivity
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-30 05:13:00 +02:00
|
|
|
|
guard currentActivities.isEmpty else {
|
2018-08-24 18:40:16 +02:00
|
|
|
|
Logger.debug("not deactivating due to currentActivities: \(currentActivities)")
|
2018-02-16 21:32:29 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
2018-02-03 00:35:32 +01:00
|
|
|
|
|
|
|
|
|
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])
|
2018-02-03 00:35:32 +01:00
|
|
|
|
} catch {
|
2018-08-27 16:27:48 +02:00
|
|
|
|
owsFailDebug("failed with error: \(error)")
|
2018-02-03 00:35:32 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|