Merge branch 'mkirk/fix-audio-route'

This commit is contained in:
Michael Kirk 2017-07-15 17:11:45 -04:00
commit df79c003f0
3 changed files with 53 additions and 50 deletions

View File

@ -89,7 +89,7 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R
// MARK: Audio Source
var hasAlternateAudioSources: Bool {
Logger.info("\(TAG) available audio routes count: \(allAudioSources.count)")
Logger.info("\(TAG) available audio sources: \(allAudioSources)")
// internal mic and speakerphone will be the first two, any more than one indicates e.g. an attached bluetooth device.
// TODO is this sufficient? Are their devices w/ bluetooth but no external speaker? e.g. ipod?

View File

@ -12,19 +12,34 @@ struct AudioSource: Hashable {
let image: UIImage
let localizedName: String
let portDescription: AVAudioSessionPortDescription?
// The built-in loud speaker / aka speakerphone
let isBuiltInSpeaker: Bool
init(localizedName: String, image: UIImage, isBuiltInSpeaker: Bool, portDescription: AVAudioSessionPortDescription? = nil) {
// The built-in quiet speaker, aka the normal phone handset receiver earpiece
let isBuiltInEarPiece: Bool
init(localizedName: String, image: UIImage, isBuiltInSpeaker: Bool, isBuiltInEarPiece: Bool, portDescription: AVAudioSessionPortDescription? = nil) {
self.localizedName = localizedName
self.image = image
self.isBuiltInSpeaker = isBuiltInSpeaker
self.isBuiltInEarPiece = isBuiltInEarPiece
self.portDescription = portDescription
}
init(portDescription: AVAudioSessionPortDescription) {
self.init(localizedName: portDescription.portName,
let isBuiltInEarPiece = portDescription.portType == AVAudioSessionPortBuiltInMic
// portDescription.portName works well for BT linked devices, but if we are using
// the built in mic, we have "iPhone Microphone" which is a little awkward.
// In that case, instead we prefer just the model name e.g. "iPhone" or "iPad"
let localizedName = isBuiltInEarPiece ? UIDevice.current.localizedModel : portDescription.portName
self.init(localizedName: localizedName,
image:#imageLiteral(resourceName: "button_phone_white"), // TODO
isBuiltInSpeaker: false,
isBuiltInEarPiece: isBuiltInEarPiece,
portDescription: portDescription)
}
@ -32,7 +47,8 @@ struct AudioSource: Hashable {
static var builtInSpeaker: AudioSource {
return self.init(localizedName: NSLocalizedString("AUDIO_ROUTE_BUILT_IN_SPEAKER", comment: "action sheet button title to enable built in speaker during a call"),
image: #imageLiteral(resourceName: "button_phone_white"), //TODO
isBuiltInSpeaker: true)
isBuiltInSpeaker: true,
isBuiltInEarPiece: false)
}
// MARK: Hashable
@ -143,15 +159,6 @@ struct AudioSource: Hashable {
AssertIsOnMainThread()
ensureProperAudioSession(call: call)
// It's importent to set preferred input *after* ensuring properAudioSession
// because some sources are only valid for certain category/option combinations.
let session = AVAudioSession.sharedInstance()
do {
try session.setPreferredInput(audioSource?.portDescription)
} catch {
owsFail("\(TAG) setPreferredInput in \(#function) failed with error: \(error)")
}
}
internal func hasLocalVideoDidChange(call: SignalCall, hasLocalVideo: Bool) {
@ -169,32 +176,53 @@ struct AudioSource: Hashable {
return
}
// Disallow bluetooth while (and only while) the user has explicitly chosen the built in receiver.
//
// NOTE: I'm actually not sure why this is required - it seems like we should just be able
// to setPreferredInput to call.audioSource.portDescription in this case,
// but in practice I'm seeing the call revert to the bluetooth headset.
// Presumably something else (in WebRTC?) is touching our shared AudioSession. - mjk
let options: AVAudioSessionCategoryOptions = call.audioSource?.isBuiltInEarPiece == true ? [] : [.allowBluetooth]
if call.state == .localRinging {
// SoloAmbient plays through speaker, but respects silent switch
setAudioSession(category: AVAudioSessionCategorySoloAmbient,
mode: AVAudioSessionModeDefault)
} else if call.hasLocalVideo {
// Don't allow bluetooth for local video if speakerphone has been explicitly chosen by the user.
let options: AVAudioSessionCategoryOptions = call.isSpeakerphoneEnabled ? [.defaultToSpeaker] : [.defaultToSpeaker, .allowBluetooth]
// Apple Docs say that setting mode to AVAudioSessionModeVideoChat has the
// side effect of setting options: .allowBluetooth, when I remove the (seemingly unnecessary)
// option, and inspect AVAudioSession.sharedInstance.categoryOptions == 0. And availableInputs
// does not include my linked bluetooth device
setAudioSession(category: AVAudioSessionCategoryPlayAndRecord,
mode: AVAudioSessionModeVideoChat,
options: options)
} else {
// Apple Docs say that setting mode to AVAudioSessionModeVoiceChat has the
// side effect of setting options: .allowBluetooth, when I remove the (seemingly unnecessary)
// option, and inspect AVAudioSession.sharedInstance.categoryOptions == 0. And availableInputs
// does not include my linked bluetooth device
setAudioSession(category: AVAudioSessionCategoryPlayAndRecord,
mode: AVAudioSessionModeVoiceChat,
options: [.allowBluetooth])
options: options)
}
let session = AVAudioSession.sharedInstance()
do {
// It's important to set preferred input *after* ensuring properAudioSession
// because some sources are only valid for certain category/option combinations.
let existingPreferredInput = session.preferredInput
if existingPreferredInput != call.audioSource?.portDescription {
Logger.info("\(TAG) changing preferred input: \(String(describing: existingPreferredInput)) -> \(String(describing: call.audioSource?.portDescription))")
try session.setPreferredInput(call.audioSource?.portDescription)
}
if call.isSpeakerphoneEnabled {
try session.overrideOutputAudioPort(.speaker)
} else {
try session.overrideOutputAudioPort(.none)
}
} catch {
owsFail("\(TAG) failed overrideing output audio. isSpeakerPhoneEnabled: \(call.isSpeakerphoneEnabled)")
owsFail("\(TAG) failed setting audio source with error: \(error) isSpeakerPhoneEnabled: \(call.isSpeakerphoneEnabled)")
}
}
@ -211,6 +239,11 @@ struct AudioSource: Hashable {
Logger.verbose("\(TAG) in \(#function) new state: \(call.state)")
// Stop playing sounds while switching audio session so we don't
// get any blips across a temporary unintended route.
stopPlayingAnySounds()
self.ensureProperAudioSession(call: call)
switch call.state {
case .idle: handleIdle(call: call)
case .dialing: handleDialing(call: call)
@ -233,8 +266,6 @@ struct AudioSource: Hashable {
Logger.debug("\(TAG) \(#function)")
AssertIsOnMainThread()
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) {
@ -245,17 +276,12 @@ struct AudioSource: Hashable {
private func handleAnswering(call: SignalCall) {
Logger.debug("\(TAG) \(#function)")
AssertIsOnMainThread()
stopPlayingAnySounds()
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)
}
@ -264,27 +290,18 @@ struct AudioSource: Hashable {
Logger.debug("\(TAG) in \(#function)")
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.
ensureProperAudioSession(call: call)
}
private func handleLocalFailure(call: SignalCall) {
Logger.debug("\(TAG) \(#function)")
AssertIsOnMainThread()
stopPlayingAnySounds()
play(sound: Sound.failure)
}
@ -308,9 +325,8 @@ struct AudioSource: Hashable {
Logger.debug("\(TAG) \(#function)")
AssertIsOnMainThread()
stopPlayingAnySounds()
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)
@ -321,8 +337,6 @@ struct AudioSource: Hashable {
Logger.debug("\(TAG) \(#function)")
AssertIsOnMainThread()
stopPlayingAnySounds()
// Stop solo audio, revert to default.
setAudioSession(category: AVAudioSessionCategoryAmbient)
}
@ -431,17 +445,6 @@ struct AudioSource: Hashable {
return AudioSource(portDescription: portDescription)
}
public func setPreferredInput(call: SignalCall, audioSource: AudioSource?) {
let session = AVAudioSession.sharedInstance()
do {
Logger.debug("\(TAG) in \(#function) audioSource: \(String(describing: audioSource))")
try session.setPreferredInput(audioSource?.portDescription)
} catch {
owsFail("\(TAG) failed with error: \(error)")
}
self.ensureProperAudioSession(call: call)
}
private func setAudioSession(category: String,
mode: String? = nil,
options: AVAudioSessionCategoryOptions = AVAudioSessionCategoryOptions(rawValue: 0)) {

View File

@ -140,7 +140,7 @@
"ATTACHMENT_TYPE_VOICE_MESSAGE" = "Voice Message";
/* action sheet button title to enable built in speaker during a call */
"AUDIO_ROUTE_BUILT_IN_SPEAKER" = "Built in Speaker";
"AUDIO_ROUTE_BUILT_IN_SPEAKER" = "Speaker";
/* An explanation of the consequences of blocking another user. */
"BLOCK_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";