mirror of https://github.com/oxen-io/session-ios
Merge branch 'mkirk/webrtc/fix-non-callkit-ringer' into feature/webrtc
commit
cabd85c854
@ -0,0 +1,195 @@
|
||||
//
|
||||
// Copyright © 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc class CallAudioService: NSObject, CallObserver {
|
||||
|
||||
private let TAG = "[CallAudioService]"
|
||||
private var vibrateTimer: Timer?
|
||||
private let soundPlayer = JSQSystemSoundPlayer.shared()!
|
||||
private let handleRinging: Bool
|
||||
|
||||
enum SoundFilenames: String {
|
||||
case incomingRing = "r"
|
||||
}
|
||||
|
||||
// MARK: Vibration config
|
||||
private let vibrateRepeatDuration = 1.6
|
||||
|
||||
// Our ring buzz is a pair of vibrations.
|
||||
// `pulseDuration` is the small pause between the two vibrations in the pair.
|
||||
private let pulseDuration = 0.2
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
init(handleRinging: Bool) {
|
||||
self.handleRinging = handleRinging
|
||||
}
|
||||
|
||||
// MARK: - CallObserver
|
||||
|
||||
internal func stateDidChange(call: SignalCall, state: CallState) {
|
||||
DispatchQueue.main.async {
|
||||
self.handleState(state)
|
||||
}
|
||||
}
|
||||
|
||||
internal func muteDidChange(call: SignalCall, isMuted: Bool) {
|
||||
Logger.verbose("\(TAG) in \(#function) is no-op")
|
||||
}
|
||||
|
||||
internal func speakerphoneDidChange(call: SignalCall, isEnabled: Bool) {
|
||||
if isEnabled {
|
||||
setAudioSession(category: AVAudioSessionCategoryPlayAndRecord, options: .defaultToSpeaker)
|
||||
} else {
|
||||
setAudioSession(category: AVAudioSessionCategoryPlayAndRecord)
|
||||
}
|
||||
}
|
||||
|
||||
internal func hasVideoDidChange(call: SignalCall, hasVideo: Bool) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
// MARK: - Service action handlers
|
||||
|
||||
public func handleState(_ state: CallState) {
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
Logger.verbose("\(TAG) in \(#function) new state: \(state)")
|
||||
|
||||
switch state {
|
||||
case .idle: handleIdle()
|
||||
case .dialing: handleDialing()
|
||||
case .answering: handleAnswering()
|
||||
case .remoteRinging: handleRemoteRinging()
|
||||
case .localRinging: handleLocalRinging()
|
||||
case .connected: handleConnected()
|
||||
case .localFailure: handleLocalFailure()
|
||||
case .localHangup: handleLocalHangup()
|
||||
case .remoteHangup: handleRemoteHangup()
|
||||
case .remoteBusy: handleBusy()
|
||||
}
|
||||
}
|
||||
|
||||
private func handleIdle() {
|
||||
Logger.debug("\(TAG) \(#function)")
|
||||
}
|
||||
|
||||
private func handleDialing() {
|
||||
Logger.debug("\(TAG) \(#function)")
|
||||
}
|
||||
|
||||
private func handleAnswering() {
|
||||
Logger.debug("\(TAG) \(#function)")
|
||||
stopRinging()
|
||||
}
|
||||
|
||||
private func handleRemoteRinging() {
|
||||
Logger.debug("\(TAG) \(#function)")
|
||||
}
|
||||
|
||||
private func handleLocalRinging() {
|
||||
Logger.debug("\(TAG) in \(#function)")
|
||||
startRinging()
|
||||
}
|
||||
|
||||
private func handleConnected() {
|
||||
Logger.debug("\(TAG) \(#function)")
|
||||
stopRinging()
|
||||
|
||||
// disable start recording to transmit call audio.
|
||||
setAudioSession(category: AVAudioSessionCategoryPlayAndRecord)
|
||||
}
|
||||
|
||||
private func handleLocalFailure() {
|
||||
Logger.debug("\(TAG) \(#function)")
|
||||
stopRinging()
|
||||
}
|
||||
|
||||
private func handleLocalHangup() {
|
||||
Logger.debug("\(TAG) \(#function)")
|
||||
stopRinging()
|
||||
}
|
||||
|
||||
private func handleRemoteHangup() {
|
||||
Logger.debug("\(TAG) \(#function)")
|
||||
stopRinging()
|
||||
}
|
||||
|
||||
private func handleBusy() {
|
||||
Logger.debug("\(TAG) \(#function)")
|
||||
stopRinging()
|
||||
}
|
||||
|
||||
// MARK: - Ringing
|
||||
|
||||
private func startRinging() {
|
||||
guard handleRinging else {
|
||||
Logger.debug("\(TAG) ignoring \(#function) since CallKit handles it's own ringing state")
|
||||
return
|
||||
}
|
||||
|
||||
vibrateTimer = WeakTimer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, userInfo: nil, repeats: true) {[weak self] timer in
|
||||
self?.ringVibration()
|
||||
}
|
||||
vibrateTimer?.fire()
|
||||
|
||||
// Stop other sounds and play ringer through external speaker
|
||||
setAudioSession(category: AVAudioSessionCategorySoloAmbient)
|
||||
|
||||
soundPlayer.playSound(withFilename: SoundFilenames.incomingRing.rawValue, fileExtension: kJSQSystemSoundTypeCAF)
|
||||
}
|
||||
|
||||
private func stopRinging() {
|
||||
guard handleRinging else {
|
||||
Logger.debug("\(TAG) ignoring \(#function) since CallKit handles it's own ringing state")
|
||||
return
|
||||
}
|
||||
Logger.debug("\(TAG) in \(#function)")
|
||||
|
||||
// Stop vibrating
|
||||
vibrateTimer?.invalidate()
|
||||
vibrateTimer = nil
|
||||
|
||||
soundPlayer.stopSound(withFilename: SoundFilenames.incomingRing.rawValue)
|
||||
|
||||
// Stop solo audio, revert to default.
|
||||
setAudioSession(category: AVAudioSessionCategoryAmbient)
|
||||
}
|
||||
|
||||
// public so it can be called by timer via selector
|
||||
public func ringVibration() {
|
||||
// Since a call notification is more urgent than a message notifaction, we
|
||||
// vibrate twice, like a pulse, to differentiate from a normal notification vibration.
|
||||
soundPlayer.playVibrateSound()
|
||||
DispatchQueue.default.asyncAfter(deadline: DispatchTime.now() + pulseDuration) {
|
||||
self.soundPlayer.playVibrateSound()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AVAudioSession Helpers
|
||||
|
||||
private func setAudioSession(category: String, options: AVAudioSessionCategoryOptions) {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(category, with: options)
|
||||
Logger.debug("\(self.TAG) set category: \(category) options: \(options)")
|
||||
} catch {
|
||||
let message = "\(self.TAG) in \(#function) failed to set category: \(category) with error: \(error)"
|
||||
assertionFailure(message)
|
||||
Logger.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
private func setAudioSession(category: String) {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(category)
|
||||
Logger.debug("\(self.TAG) set category: \(category)")
|
||||
} catch {
|
||||
let message = "\(self.TAG) in \(#function) failed to set category: \(category) with error: \(error)"
|
||||
assertionFailure(message)
|
||||
Logger.error(message)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
//
|
||||
// Copyright © 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
/**
|
||||
* Container for a weakly referenced object.
|
||||
*
|
||||
* Only use this for |T| with reference-semantic entities
|
||||
* e.g. inheriting from AnyObject or Class-only protocols, but not structs or enums.
|
||||
*
|
||||
*
|
||||
* Based on https://devforums.apple.com/message/981472#981472, but also supports class-only protocols
|
||||
*/
|
||||
struct Weak<T> {
|
||||
private weak var _value: AnyObject?
|
||||
|
||||
var value: T? {
|
||||
get {
|
||||
return _value as? T
|
||||
}
|
||||
set {
|
||||
_value = newValue as AnyObject
|
||||
}
|
||||
}
|
||||
|
||||
init(value: T) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
//
|
||||
// Copyright © 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
/**
|
||||
* As of iOS10, the timer API's take a block, which makes it easy to reference weak self in Swift. This class offers a
|
||||
* similar API that works pre iOS10.
|
||||
*
|
||||
* Solution modified from
|
||||
* http://stackoverflow.com/questions/16821736/weak-reference-to-nstimer-target-to-prevent-retain-cycle/41003985#41003985
|
||||
*/
|
||||
final class WeakTimer {
|
||||
|
||||
fileprivate weak var timer: Timer?
|
||||
fileprivate weak var target: AnyObject?
|
||||
fileprivate let action: (Timer) -> Void
|
||||
|
||||
fileprivate init(timeInterval: TimeInterval, target: AnyObject, userInfo: Any?, repeats: Bool, action: @escaping (Timer) -> Void) {
|
||||
self.target = target
|
||||
self.action = action
|
||||
self.timer = Timer.scheduledTimer(timeInterval: timeInterval,
|
||||
target: self,
|
||||
selector: #selector(fire),
|
||||
userInfo: userInfo,
|
||||
repeats: repeats)
|
||||
}
|
||||
|
||||
class func scheduledTimer(timeInterval: TimeInterval, target: AnyObject, userInfo: Any?, repeats: Bool, action: @escaping (Timer) -> Void) -> Timer {
|
||||
return WeakTimer(timeInterval: timeInterval,
|
||||
target: target,
|
||||
userInfo: userInfo,
|
||||
repeats: repeats,
|
||||
action: action).timer!
|
||||
}
|
||||
|
||||
@objc fileprivate func fire(timer: Timer) {
|
||||
if target != nil {
|
||||
action(timer)
|
||||
} else {
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue