From 8abde1dff8d442564fc8b18df048536f62f5f234 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 3 Jul 2017 08:42:30 -0500 Subject: [PATCH 1/3] Non-Callkit adapter plays audio from notification when app is in background // FREEBIE --- Signal/src/call/CallAudioService.swift | 77 ++++++++++++------- Signal/src/call/CallService.swift | 4 +- .../call/UserInterface/CallUIAdapter.swift | 1 + Signal/src/environment/NotificationsManager.m | 2 +- 4 files changed, 53 insertions(+), 31 deletions(-) diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift index be387e2ed..61d8226d0 100644 --- a/Signal/src/call/CallAudioService.swift +++ b/Signal/src/call/CallAudioService.swift @@ -78,24 +78,27 @@ import AVFoundation internal func speakerphoneDidChange(call: SignalCall, isEnabled: Bool) { AssertIsOnMainThread() - ensureIsEnabled(call: call) + ensureProperAudioSession(call: call) } internal func hasLocalVideoDidChange(call: SignalCall, hasLocalVideo: Bool) { AssertIsOnMainThread() - ensureIsEnabled(call: call) + ensureProperAudioSession(call: call) } - private func ensureIsEnabled(call: SignalCall?) { + private func ensureProperAudioSession(call: SignalCall?) { guard let call = call else { setAudioSession(category: AVAudioSessionCategoryPlayback, mode: AVAudioSessionModeDefault) return } - // Auto-enable speakerphone when local video is enabled. - if call.hasLocalVideo { + if call.state == .localRinging { + // SoloAmbient plays through speaker, but respects silent switch + setAudioSession(category: AVAudioSessionCategorySoloAmbient) + } else if call.hasLocalVideo { + // Auto-enable speakerphone when local video is enabled. setAudioSession(category: AVAudioSessionCategoryPlayAndRecord, mode: AVAudioSessionModeVideoChat, options: .defaultToSpeaker) @@ -114,7 +117,7 @@ import AVFoundation public func didUpdateVideoTracks(call: SignalCall?) { Logger.verbose("\(TAG) in \(#function)") - self.ensureIsEnabled(call: call) + self.ensureProperAudioSession(call: call) } public func handleState(call: SignalCall) { @@ -142,68 +145,73 @@ import AVFoundation private func handleDialing(call: SignalCall) { Logger.debug("\(TAG) \(#function)") + AssertIsOnMainThread() - if call.isSpeakerphoneEnabled { - setAudioSession(category: AVAudioSessionCategoryPlayAndRecord, - mode: AVAudioSessionModeVoiceChat, - options: [.defaultToSpeaker, .mixWithOthers]) - } else { - setAudioSession(category: AVAudioSessionCategoryPlayAndRecord, - mode: AVAudioSessionModeVoiceChat, - options: .mixWithOthers) - } + ensureProperAudioSession(call: call) // HACK: Without this async, dialing sound only plays once. I don't really understand why. Does the audioSession // need some time to settle? Is somethign else interrupting our session? DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) { - self.play(sound: Sound.dialing, call: call) + self.play(sound: Sound.dialing) } } private func handleAnswering(call: SignalCall) { Logger.debug("\(TAG) \(#function)") + AssertIsOnMainThread() + stopPlayingAnySounds() - self.ensureIsEnabled(call: call) + self.ensureProperAudioSession(call: call) } private func handleRemoteRinging(call: SignalCall) { Logger.debug("\(TAG) \(#function)") + AssertIsOnMainThread() + stopPlayingAnySounds() // FIXME if you toggled speakerphone before this point, the outgoing ring does not play through speaker. Why? - self.play(sound: Sound.outgoingRing, call: call) + self.play(sound: Sound.outgoingRing) } private func handleLocalRinging(call: SignalCall) { Logger.debug("\(TAG) in \(#function)") - stopPlayingAnySounds() + AssertIsOnMainThread() + stopPlayingAnySounds() + ensureProperAudioSession(call: call) startRinging(call: call) } private func handleConnected(call: SignalCall) { Logger.debug("\(TAG) \(#function)") + AssertIsOnMainThread() + stopPlayingAnySounds() // start recording to transmit call audio. - ensureIsEnabled(call: call) + ensureProperAudioSession(call: call) } private func handleLocalFailure(call: SignalCall) { Logger.debug("\(TAG) \(#function)") + AssertIsOnMainThread() + stopPlayingAnySounds() - play(sound: Sound.failure, call: call) + play(sound: Sound.failure) } private func handleLocalHangup(call: SignalCall) { Logger.debug("\(TAG) \(#function)") + AssertIsOnMainThread() handleCallEnded(call: call) } private func handleRemoteHangup(call: SignalCall) { Logger.debug("\(TAG) \(#function)") + AssertIsOnMainThread() vibrate() @@ -212,9 +220,11 @@ import AVFoundation private func handleBusy(call: SignalCall) { Logger.debug("\(TAG) \(#function)") + AssertIsOnMainThread() + stopPlayingAnySounds() - play(sound: Sound.busy, call: call) + play(sound: Sound.busy) // Let the busy sound play for 4 seconds. The full file is longer than necessary DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 4.0) { self.handleCallEnded(call: call) @@ -223,6 +233,8 @@ import AVFoundation private func handleCallEnded(call: SignalCall) { Logger.debug("\(TAG) \(#function)") + AssertIsOnMainThread() + stopPlayingAnySounds() // Stop solo audio, revert to default. @@ -238,7 +250,7 @@ import AVFoundation stopAnyRingingVibration() } - private func play(sound: Sound, call: SignalCall) { + private func play(sound: Sound) { guard let newPlayer = sound.player else { Logger.error("\(self.TAG) unable to build player") return @@ -259,14 +271,11 @@ import AVFoundation return } - // SoloAmbient plays through speaker, but respects silent switch - setAudioSession(category: AVAudioSessionCategorySoloAmbient) - vibrateTimer = WeakTimer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, userInfo: nil, repeats: true) {[weak self] _ in self?.ringVibration() } vibrateTimer?.fire() - play(sound: Sound.incomingRing, call: call) + play(sound: Sound.incomingRing) } private func stopAnyRingingVibration() { @@ -299,12 +308,22 @@ import AVFoundation private func setAudioSession(category: String, mode: String? = nil, options: AVAudioSessionCategoryOptions = AVAudioSessionCategoryOptions(rawValue: 0)) { + + let session = AVAudioSession.sharedInstance() do { if #available(iOS 10.0, *), let mode = mode { - try AVAudioSession.sharedInstance().setCategory(category, mode: mode, options: options) + if session.category == category, session.mode == mode, session.categoryOptions == options { + Logger.debug("\(self.TAG) in \(#function) ignoring no-op") + return + } + try session.setCategory(category, mode: mode, options: options) Logger.debug("\(self.TAG) set category: \(category) mode: \(mode) options: \(options)") } else { - try AVAudioSession.sharedInstance().setCategory(category, with: options) + if session.category == category, session.categoryOptions == options { + Logger.debug("\(self.TAG) in \(#function) ignoring no-op") + return + } + try session.setCategory(category, with: options) Logger.debug("\(self.TAG) set category: \(category) options: \(options)") } } catch { diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index bfd09fafb..fef8800e3 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -578,7 +578,7 @@ protocol CallServiceObserver: class { : "\(self.TAG) obsolete incoming call connected: \(newCall.identifiersForLogs).") }.catch { error in guard self.call == newCall else { - Logger.debug("\(self.TAG) error: \(error) for obsolete call: \(newCall.identifiersForLogs).") + Logger.debug("\(self.TAG) ignoring error: \(error) for obsolete call: \(newCall.identifiersForLogs).") return } if let callError = error as? CallError { @@ -857,6 +857,8 @@ protocol CallServiceObserver: class { Logger.info("\(TAG) in \(#function): \(call.identifiersForLogs).") + // TODO make call record. + // Currently we just handle this as a hangup. But we could offer more descriptive action. e.g. DataChannel message handleLocalHungupCall(call) } diff --git a/Signal/src/call/UserInterface/CallUIAdapter.swift b/Signal/src/call/UserInterface/CallUIAdapter.swift index 8f183af7f..32ec07fb3 100644 --- a/Signal/src/call/UserInterface/CallUIAdapter.swift +++ b/Signal/src/call/UserInterface/CallUIAdapter.swift @@ -63,6 +63,7 @@ extension CallUIAdaptee { guard self.callService.call == nil else { Logger.info("unexpectedly found an existing call when trying to start outgoing call: \(recipientId)") + //TODO terminate existing call. return } diff --git a/Signal/src/environment/NotificationsManager.m b/Signal/src/environment/NotificationsManager.m index b40b607fd..228de0ca3 100644 --- a/Signal/src/environment/NotificationsManager.m +++ b/Signal/src/environment/NotificationsManager.m @@ -63,7 +63,7 @@ NSString *const kNotificationsManagerNewMesssageSoundName = @"NewMessage.aifc"; UILocalNotification *notification = [UILocalNotification new]; notification.category = PushManagerCategoriesIncomingCall; // Rather than using notification sounds, we control the ringtone and repeat vibrations with the CallAudioManager. - // notification.soundName = @"r.caf"; + notification.soundName = @"r.caf"; NSString *localCallId = call.localId.UUIDString; notification.userInfo = @{ PushManagerUserInfoKeysLocalCallId : localCallId }; From 43a3a4afafad04d6d83b6ac4bc8848868f653476 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 3 Jul 2017 12:57:54 -0500 Subject: [PATCH 2/3] play *after* stop In theory this shouldn't make a difference, since we're not playing the ringer twice, but in practice I fail to here ringer audio 50% of the time (in DEBUG builds) while app is in the foreground. // FREEBIE --- Signal/src/call/CallAudioService.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift index 61d8226d0..33f739e0e 100644 --- a/Signal/src/call/CallAudioService.swift +++ b/Signal/src/call/CallAudioService.swift @@ -253,13 +253,13 @@ import AVFoundation private func play(sound: Sound) { guard let newPlayer = sound.player else { Logger.error("\(self.TAG) unable to build player") + assertionFailure() return } Logger.info("\(self.TAG) playing sound: \(sound.filePath)") - newPlayer.play() self.currentPlayer?.stop() - + newPlayer.play() self.currentPlayer = newPlayer } From 1e0cf89f7a19294a581831a28acabcb6dc289f48 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 5 Jul 2017 11:55:00 -0500 Subject: [PATCH 3/3] Explanatory comment // FREEBIE --- Signal/src/call/CallAudioService.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift index 33f739e0e..03ea9d08f 100644 --- a/Signal/src/call/CallAudioService.swift +++ b/Signal/src/call/CallAudioService.swift @@ -258,6 +258,9 @@ import AVFoundation } Logger.info("\(self.TAG) playing sound: \(sound.filePath)") + // It's important to stop the current player **before** starting the new player. In the case that + // we're playing the same sound, since the player is memoized on the sound instance, we'd otherwise + // stop the sound we just started. self.currentPlayer?.stop() newPlayer.play() self.currentPlayer = newPlayer