mirror of https://github.com/oxen-io/session-ios

48 changed files with 12613 additions and 5 deletions
@ -0,0 +1,212 @@
|
||||
// Generated by Apple Swift version 5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)
|
||||
#ifndef SIGNALRINGRTC_SWIFT_H |
||||
#define SIGNALRINGRTC_SWIFT_H |
||||
#pragma clang diagnostic push |
||||
#pragma clang diagnostic ignored "-Wgcc-compat" |
||||
|
||||
#if !defined(__has_include) |
||||
# define __has_include(x) 0 |
||||
#endif |
||||
#if !defined(__has_attribute) |
||||
# define __has_attribute(x) 0 |
||||
#endif |
||||
#if !defined(__has_feature) |
||||
# define __has_feature(x) 0 |
||||
#endif |
||||
#if !defined(__has_warning) |
||||
# define __has_warning(x) 0 |
||||
#endif |
||||
|
||||
#if __has_include(<swift/objc-prologue.h>) |
||||
# include <swift/objc-prologue.h> |
||||
#endif |
||||
|
||||
#pragma clang diagnostic ignored "-Wauto-import" |
||||
#include <Foundation/Foundation.h> |
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
#include <stdbool.h> |
||||
|
||||
#if !defined(SWIFT_TYPEDEFS) |
||||
# define SWIFT_TYPEDEFS 1 |
||||
# if __has_include(<uchar.h>) |
||||
# include <uchar.h> |
||||
# elif !defined(__cplusplus) |
||||
typedef uint_least16_t char16_t; |
||||
typedef uint_least32_t char32_t; |
||||
# endif |
||||
typedef float swift_float2 __attribute__((__ext_vector_type__(2))); |
||||
typedef float swift_float3 __attribute__((__ext_vector_type__(3))); |
||||
typedef float swift_float4 __attribute__((__ext_vector_type__(4))); |
||||
typedef double swift_double2 __attribute__((__ext_vector_type__(2))); |
||||
typedef double swift_double3 __attribute__((__ext_vector_type__(3))); |
||||
typedef double swift_double4 __attribute__((__ext_vector_type__(4))); |
||||
typedef int swift_int2 __attribute__((__ext_vector_type__(2))); |
||||
typedef int swift_int3 __attribute__((__ext_vector_type__(3))); |
||||
typedef int swift_int4 __attribute__((__ext_vector_type__(4))); |
||||
typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); |
||||
typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); |
||||
typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); |
||||
#endif |
||||
|
||||
#if !defined(SWIFT_PASTE) |
||||
# define SWIFT_PASTE_HELPER(x, y) x##y |
||||
# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) |
||||
#endif |
||||
#if !defined(SWIFT_METATYPE) |
||||
# define SWIFT_METATYPE(X) Class |
||||
#endif |
||||
#if !defined(SWIFT_CLASS_PROPERTY) |
||||
# if __has_feature(objc_class_property) |
||||
# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ |
||||
# else |
||||
# define SWIFT_CLASS_PROPERTY(...) |
||||
# endif |
||||
#endif |
||||
|
||||
#if __has_attribute(objc_runtime_name) |
||||
# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) |
||||
#else |
||||
# define SWIFT_RUNTIME_NAME(X) |
||||
#endif |
||||
#if __has_attribute(swift_name) |
||||
# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) |
||||
#else |
||||
# define SWIFT_COMPILE_NAME(X) |
||||
#endif |
||||
#if __has_attribute(objc_method_family) |
||||
# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) |
||||
#else |
||||
# define SWIFT_METHOD_FAMILY(X) |
||||
#endif |
||||
#if __has_attribute(noescape) |
||||
# define SWIFT_NOESCAPE __attribute__((noescape)) |
||||
#else |
||||
# define SWIFT_NOESCAPE |
||||
#endif |
||||
#if __has_attribute(ns_consumed) |
||||
# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) |
||||
#else |
||||
# define SWIFT_RELEASES_ARGUMENT |
||||
#endif |
||||
#if __has_attribute(warn_unused_result) |
||||
# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) |
||||
#else |
||||
# define SWIFT_WARN_UNUSED_RESULT |
||||
#endif |
||||
#if __has_attribute(noreturn) |
||||
# define SWIFT_NORETURN __attribute__((noreturn)) |
||||
#else |
||||
# define SWIFT_NORETURN |
||||
#endif |
||||
#if !defined(SWIFT_CLASS_EXTRA) |
||||
# define SWIFT_CLASS_EXTRA |
||||
#endif |
||||
#if !defined(SWIFT_PROTOCOL_EXTRA) |
||||
# define SWIFT_PROTOCOL_EXTRA |
||||
#endif |
||||
#if !defined(SWIFT_ENUM_EXTRA) |
||||
# define SWIFT_ENUM_EXTRA |
||||
#endif |
||||
#if !defined(SWIFT_CLASS) |
||||
# if __has_attribute(objc_subclassing_restricted) |
||||
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA |
||||
# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA |
||||
# else |
||||
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA |
||||
# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA |
||||
# endif |
||||
#endif |
||||
#if !defined(SWIFT_RESILIENT_CLASS) |
||||
# if __has_attribute(objc_class_stub) |
||||
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub)) |
||||
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME) |
||||
# else |
||||
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) |
||||
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) |
||||
# endif |
||||
#endif |
||||
|
||||
#if !defined(SWIFT_PROTOCOL) |
||||
# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA |
||||
# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA |
||||
#endif |
||||
|
||||
#if !defined(SWIFT_EXTENSION) |
||||
# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) |
||||
#endif |
||||
|
||||
#if !defined(OBJC_DESIGNATED_INITIALIZER) |
||||
# if __has_attribute(objc_designated_initializer) |
||||
# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) |
||||
# else |
||||
# define OBJC_DESIGNATED_INITIALIZER |
||||
# endif |
||||
#endif |
||||
#if !defined(SWIFT_ENUM_ATTR) |
||||
# if defined(__has_attribute) && __has_attribute(enum_extensibility) |
||||
# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) |
||||
# else |
||||
# define SWIFT_ENUM_ATTR(_extensibility) |
||||
# endif |
||||
#endif |
||||
#if !defined(SWIFT_ENUM) |
||||
# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type |
||||
# if __has_feature(generalized_swift_name) |
||||
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type |
||||
# else |
||||
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) |
||||
# endif |
||||
#endif |
||||
#if !defined(SWIFT_UNAVAILABLE) |
||||
# define SWIFT_UNAVAILABLE __attribute__((unavailable)) |
||||
#endif |
||||
#if !defined(SWIFT_UNAVAILABLE_MSG) |
||||
# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) |
||||
#endif |
||||
#if !defined(SWIFT_AVAILABILITY) |
||||
# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) |
||||
#endif |
||||
#if !defined(SWIFT_WEAK_IMPORT) |
||||
# define SWIFT_WEAK_IMPORT __attribute__((weak_import)) |
||||
#endif |
||||
#if !defined(SWIFT_DEPRECATED) |
||||
# define SWIFT_DEPRECATED __attribute__((deprecated)) |
||||
#endif |
||||
#if !defined(SWIFT_DEPRECATED_MSG) |
||||
# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) |
||||
#endif |
||||
#if __has_feature(attribute_diagnose_if_objc) |
||||
# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) |
||||
#else |
||||
# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) |
||||
#endif |
||||
#if !defined(IBSegueAction) |
||||
# define IBSegueAction |
||||
#endif |
||||
#if __has_feature(modules) |
||||
#if __has_warning("-Watimport-in-framework-header") |
||||
#pragma clang diagnostic ignored "-Watimport-in-framework-header" |
||||
#endif |
||||
#endif |
||||
|
||||
#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" |
||||
#pragma clang diagnostic ignored "-Wduplicate-method-arg" |
||||
#if __has_warning("-Wpragma-clang-attribute") |
||||
# pragma clang diagnostic ignored "-Wpragma-clang-attribute" |
||||
#endif |
||||
#pragma clang diagnostic ignored "-Wunknown-pragmas" |
||||
#pragma clang diagnostic ignored "-Wnullability" |
||||
|
||||
#if __has_attribute(external_source_symbol) |
||||
# pragma push_macro("any") |
||||
# undef any |
||||
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="SignalRingRTC",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) |
||||
# pragma pop_macro("any") |
||||
#endif |
||||
|
||||
#if __has_attribute(external_source_symbol) |
||||
# pragma clang attribute pop |
||||
#endif |
||||
#pragma clang diagnostic pop |
||||
#endif |
@ -0,0 +1,14 @@
|
||||
//
|
||||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h> |
||||
|
||||
//! Project version number for SignalRingRTC.
|
||||
FOUNDATION_EXPORT double SignalRingRTCVersionNumber; |
||||
|
||||
//! Project version string for SignalRingRTC.
|
||||
FOUNDATION_EXPORT const unsigned char SignalRingRTCVersionString[]; |
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <SignalRingRTC/PublicHeader.h>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,18 @@
|
||||
framework module SignalRingRTC { |
||||
umbrella header "SignalRingRTC.h" |
||||
|
||||
export * |
||||
module * { export * } |
||||
|
||||
explicit module RingRTC { |
||||
header "ringrtc.h" |
||||
link "ringrtc" |
||||
export * |
||||
} |
||||
|
||||
} |
||||
|
||||
module SignalRingRTC.Swift { |
||||
header "SignalRingRTC-Swift.h" |
||||
requires objc |
||||
} |
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1,84 @@
|
||||
// |
||||
// Copyright (c) 2020 Open Whisper Systems. All rights reserved. |
||||
// |
||||
|
||||
import Foundation |
||||
import AVFoundation |
||||
import SignalServiceKit |
||||
|
||||
public struct AudioSource: Hashable { |
||||
|
||||
public let localizedName: String |
||||
public let portDescription: AVAudioSessionPortDescription? |
||||
|
||||
// The built-in loud speaker / aka speakerphone |
||||
public let isBuiltInSpeaker: Bool |
||||
|
||||
// The built-in quiet speaker, aka the normal phone handset receiver earpiece |
||||
public let isBuiltInEarPiece: Bool |
||||
|
||||
public init(localizedName: String, isBuiltInSpeaker: Bool, isBuiltInEarPiece: Bool, portDescription: AVAudioSessionPortDescription? = nil) { |
||||
self.localizedName = localizedName |
||||
self.isBuiltInSpeaker = isBuiltInSpeaker |
||||
self.isBuiltInEarPiece = isBuiltInEarPiece |
||||
self.portDescription = portDescription |
||||
} |
||||
|
||||
public init(portDescription: AVAudioSessionPortDescription) { |
||||
|
||||
let isBuiltInEarPiece = portDescription.portType == AVAudioSession.Port.builtInMic |
||||
|
||||
// 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, |
||||
isBuiltInSpeaker: false, |
||||
isBuiltInEarPiece: isBuiltInEarPiece, |
||||
portDescription: portDescription) |
||||
} |
||||
|
||||
// Speakerphone is handled separately from the other audio routes as it doesn't appear as an "input" |
||||
public 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"), |
||||
isBuiltInSpeaker: true, |
||||
isBuiltInEarPiece: false) |
||||
} |
||||
|
||||
// MARK: Hashable |
||||
|
||||
public static func ==(lhs: AudioSource, rhs: AudioSource) -> Bool { |
||||
// Simply comparing the `portDescription` vs the `portDescription.uid` |
||||
// caused multiple instances of the built in mic to turn up in a set. |
||||
if lhs.isBuiltInSpeaker && rhs.isBuiltInSpeaker { |
||||
return true |
||||
} |
||||
|
||||
if lhs.isBuiltInSpeaker || rhs.isBuiltInSpeaker { |
||||
return false |
||||
} |
||||
|
||||
guard let lhsPortDescription = lhs.portDescription else { |
||||
owsFailDebug("only the built in speaker should lack a port description") |
||||
return false |
||||
} |
||||
|
||||
guard let rhsPortDescription = rhs.portDescription else { |
||||
owsFailDebug("only the built in speaker should lack a port description") |
||||
return false |
||||
} |
||||
|
||||
return lhsPortDescription.uid == rhsPortDescription.uid |
||||
} |
||||
|
||||
public func hash(into hasher: inout Hasher) { |
||||
guard let portDescription = self.portDescription else { |
||||
assert(self.isBuiltInSpeaker) |
||||
hasher.combine("Built In Speaker") |
||||
return |
||||
} |
||||
|
||||
hasher.combine(portDescription.uid) |
||||
} |
||||
} |
@ -0,0 +1,604 @@
|
||||
// |
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved. |
||||
// |
||||
|
||||
import Foundation |
||||
import AVFoundation |
||||
import SignalServiceKit |
||||
import SignalMessaging |
||||
import AVKit |
||||
import SignalRingRTC |
||||
|
||||
protocol CallAudioServiceDelegate: AnyObject { |
||||
func callAudioServiceDidChangeAudioSession(_ callAudioService: CallAudioService) |
||||
func callAudioServiceDidChangeAudioSource(_ callAudioService: CallAudioService, audioSource: AudioSource?) |
||||
} |
||||
|
||||
@objc class CallAudioService: NSObject, CallObserver { |
||||
|
||||
private var vibrateTimer: Timer? |
||||
|
||||
var handleRinging = false |
||||
weak var delegate: CallAudioServiceDelegate? { |
||||
willSet { |
||||
assert(newValue == nil || delegate == nil) |
||||
} |
||||
} |
||||
|
||||
// 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 |
||||
|
||||
var avAudioSession: AVAudioSession { |
||||
return AVAudioSession.sharedInstance() |
||||
} |
||||
|
||||
// MARK: - Initializers |
||||
|
||||
override init() { |
||||
super.init() |
||||
|
||||
// We cannot assert singleton here, because this class gets rebuilt when the user changes relevant call settings |
||||
|
||||
// Configure audio session so we don't prompt user with Record permission until call is connected. |
||||
|
||||
audioSession.configureRTCAudio() |
||||
NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: avAudioSession, queue: OperationQueue()) { _ in |
||||
assert(!Thread.isMainThread) |
||||
self.audioRouteDidChange() |
||||
} |
||||
|
||||
Self.callService.addObserverAndSyncState(observer: self) |
||||
} |
||||
|
||||
deinit { |
||||
NotificationCenter.default.removeObserver(self) |
||||
} |
||||
|
||||
// MARK: - CallObserver |
||||
|
||||
internal func individualCallStateDidChange(_ call: SignalCall, state: CallState) { |
||||
AssertIsOnMainThread() |
||||
handleState(call: call.individualCall) |
||||
} |
||||
|
||||
internal func individualCallLocalAudioMuteDidChange(_ call: SignalCall, isAudioMuted: Bool) { |
||||
AssertIsOnMainThread() |
||||
|
||||
ensureProperAudioSession(call: call) |
||||
} |
||||
|
||||
internal func individualCallHoldDidChange(_ call: SignalCall, isOnHold: Bool) { |
||||
AssertIsOnMainThread() |
||||
|
||||
ensureProperAudioSession(call: call) |
||||
} |
||||
|
||||
internal func individualCallLocalVideoMuteDidChange(_ call: SignalCall, isVideoMuted: Bool) { |
||||
AssertIsOnMainThread() |
||||
|
||||
ensureProperAudioSession(call: call) |
||||
} |
||||
|
||||
func groupCallLocalDeviceStateChanged(_ call: SignalCall) { |
||||
ensureProperAudioSession(call: call) |
||||
} |
||||
|
||||
func groupCallRemoteDeviceStatesChanged(_ call: SignalCall) { |
||||
// This should not be required, but for some reason setting the mode |
||||
// to "videoChat" prior to a remote device being connected gets changed |
||||
// to "voiceChat" by iOS. This results in the audio coming out of the |
||||
// earpiece instead of the speaker. It may be a result of us not actually |
||||
// playing any audio until the remote device connects, or something |
||||
// going on with the underlying RTCAudioSession that's not directly |
||||
// in our control. |
||||
ensureProperAudioSession(call: call) |
||||
} |
||||
|
||||
func groupCallEnded(_ call: SignalCall, reason: GroupCallEndReason) { |
||||
ensureProperAudioSession(call: call) |
||||
} |
||||
|
||||
private let routePicker = AVRoutePickerView() |
||||
|
||||
@discardableResult |
||||
public func presentRoutePicker() -> Bool { |
||||
guard let routeButton = routePicker.subviews.first(where: { $0 is UIButton }) as? UIButton else { |
||||
owsFailDebug("Failed to find subview to present route picker, falling back to old system") |
||||
return false |
||||
} |
||||
|
||||
routeButton.sendActions(for: .touchUpInside) |
||||
|
||||
return true |
||||
} |
||||
|
||||
public func requestSpeakerphone(isEnabled: Bool) { |
||||
// This is a little too slow to execute on the main thread and the results are not immediately available after execution |
||||
// anyway, so we dispatch async. If you need to know the new value, you'll need to check isSpeakerphoneEnabled and take |
||||
// advantage of the CallAudioServiceDelegate.callAudioService(_:didUpdateIsSpeakerphoneEnabled:) |
||||
DispatchQueue.global().async { |
||||
do { |
||||
try self.avAudioSession.overrideOutputAudioPort( isEnabled ? .speaker : .none ) |
||||
} catch { |
||||
Logger.warn("failed to set \(#function) = \(isEnabled) with error: \(error)") |
||||
} |
||||
} |
||||
} |
||||
|
||||
private func audioRouteDidChange() { |
||||
guard let currentAudioSource = currentAudioSource else { |
||||
Logger.warn("Switched to route without audio source") |
||||
return |
||||
} |
||||
|
||||
DispatchQueue.main.async { [weak self] in |
||||
guard let self = self else { return } |
||||
self.delegate?.callAudioServiceDidChangeAudioSource(self, audioSource: currentAudioSource) |
||||
} |
||||
} |
||||
|
||||
private func ensureProperAudioSession(call: SignalCall?) { |
||||
switch call?.mode { |
||||
case .individual(let call): |
||||
ensureProperAudioSession(call: call) |
||||
case .group(let call): |
||||
ensureProperAudioSession(call: call) |
||||
default: |
||||
// Revert to ambient audio |
||||
setAudioSession(category: .ambient, mode: .default) |
||||
} |
||||
} |
||||
|
||||
private func ensureProperAudioSession(call: GroupCall?) { |
||||
guard let call = call, call.localDeviceState.joinState != .notJoined else { |
||||
// Revert to ambient audio |
||||
setAudioSession(category: .ambient, mode: .default) |
||||
return |
||||
} |
||||
|
||||
if call.isOutgoingVideoMuted { |
||||
setAudioSession(category: .playAndRecord, mode: .voiceChat, options: .allowBluetooth) |
||||
} else { |
||||
setAudioSession(category: .playAndRecord, mode: .videoChat, options: .allowBluetooth) |
||||
} |
||||
} |
||||
|
||||
private func ensureProperAudioSession(call: IndividualCall?) { |
||||
AssertIsOnMainThread() |
||||
|
||||
guard let call = call, !call.isEnded else { |
||||
// Revert to ambient audio |
||||
setAudioSession(category: .ambient, |
||||
mode: .default) |
||||
return |
||||
} |
||||
|
||||
if call.state == .localRinging { |
||||
setAudioSession(category: .playback, mode: .default) |
||||
} else if call.hasLocalVideo { |
||||
// Because ModeVideoChat affects gain, we don't want to apply it until the call is connected. |
||||
// otherwise sounds like ringing will be extra loud for video vs. speakerphone |
||||
|
||||
// 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.shared.categoryOptions == 0. And availableInputs |
||||
// does not include my linked bluetooth device |
||||
setAudioSession(category: .playAndRecord, |
||||
mode: .videoChat, |
||||
options: .allowBluetooth) |
||||
} 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.shared.categoryOptions == 0. And availableInputs |
||||
// does not include my linked bluetooth device |
||||
setAudioSession(category: .playAndRecord, |
||||
mode: .voiceChat, |
||||
options: .allowBluetooth) |
||||
} |
||||
} |
||||
|
||||
// MARK: - Service action handlers |
||||
|
||||
public func handleState(call: IndividualCall) { |
||||
assert(Thread.isMainThread) |
||||
|
||||
Logger.verbose("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) |
||||
case .answering: handleAnswering(call: call) |
||||
case .remoteRinging: handleRemoteRinging(call: call) |
||||
case .localRinging: handleLocalRinging(call: call) |
||||
case .connected: handleConnected(call: call) |
||||
case .reconnecting: handleReconnecting(call: call) |
||||
case .localFailure: handleLocalFailure(call: call) |
||||
case .localHangup: handleLocalHangup(call: call) |
||||
case .remoteHangup: handleRemoteHangup(call: call) |
||||
case .remoteHangupNeedPermission: handleRemoteHangup(call: call) |
||||
case .remoteBusy: handleBusy(call: call) |
||||
case .answeredElsewhere: handleAnsweredElsewhere(call: call) |
||||
case .declinedElsewhere: handleAnsweredElsewhere(call: call) |
||||
case .busyElsewhere: handleAnsweredElsewhere(call: call) |
||||
} |
||||
} |
||||
|
||||
private func handleIdle(call: IndividualCall) { |
||||
Logger.debug("") |
||||
} |
||||
|
||||
private func handleDialing(call: IndividualCall) { |
||||
AssertIsOnMainThread() |
||||
Logger.debug("") |
||||
|
||||
// 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: .callConnecting) |
||||
} |
||||
} |
||||
|
||||
private func handleAnswering(call: IndividualCall) { |
||||
AssertIsOnMainThread() |
||||
Logger.debug("") |
||||
} |
||||
|
||||
private func handleRemoteRinging(call: IndividualCall) { |
||||
AssertIsOnMainThread() |
||||
Logger.debug("") |
||||
|
||||
self.play(sound: .callOutboundRinging) |
||||
} |
||||
|
||||
private func handleLocalRinging(call: IndividualCall) { |
||||
AssertIsOnMainThread() |
||||
Logger.debug("") |
||||
|
||||
startRinging(call: call) |
||||
} |
||||
|
||||
private func handleConnected(call: IndividualCall) { |
||||
AssertIsOnMainThread() |
||||
Logger.debug("") |
||||
} |
||||
|
||||
private func handleReconnecting(call: IndividualCall) { |
||||
AssertIsOnMainThread() |
||||
Logger.debug("") |
||||
} |
||||
|
||||
private func handleLocalFailure(call: IndividualCall) { |
||||
AssertIsOnMainThread() |
||||
Logger.debug("") |
||||
|
||||
play(sound: .callEnded) |
||||
handleCallEnded(call: call) |
||||
} |
||||
|
||||
private func handleLocalHangup(call: IndividualCall) { |
||||
AssertIsOnMainThread() |
||||
Logger.debug("") |
||||
|
||||
play(sound: .callEnded) |
||||
handleCallEnded(call: call) |
||||
} |
||||
|
||||
private func handleRemoteHangup(call: IndividualCall) { |
||||
AssertIsOnMainThread() |
||||
Logger.debug("") |
||||
|
||||
vibrate() |
||||
|
||||
play(sound: .callEnded) |
||||
handleCallEnded(call: call) |
||||
} |
||||
|
||||
private func handleBusy(call: IndividualCall) { |
||||
AssertIsOnMainThread() |
||||
Logger.debug("") |
||||
|
||||
play(sound: .callBusy) |
||||
|
||||
// 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) |
||||
} |
||||
} |
||||
|
||||
private func handleAnsweredElsewhere(call: IndividualCall) { |
||||
AssertIsOnMainThread() |
||||
Logger.debug("") |
||||
|
||||
play(sound: .callEnded) |
||||
handleCallEnded(call: call) |
||||
} |
||||
|
||||
private func handleCallEnded(call: IndividualCall) { |
||||
AssertIsOnMainThread() |
||||
Logger.debug("") |
||||
|
||||
// Sometimes (usually but not always) upon ending a call, the currentPlayer does not get |
||||
// played to completion. This is necessary in order for the players |
||||
// audioActivity to remove itself from OWSAudioSession. Otherwise future AudioActivities, |
||||
// like recording a voice note, will be prevented from having their needs met. |
||||
// |
||||
// Furthermore, no interruption delegate is called nor AVAudioSessionInterruptionNotification |
||||
// is posted. I'm not sure why we have to do this. |
||||
if let audioPlayer = currentPlayer { |
||||
audioPlayer.stop() |
||||
} |
||||
|
||||
// Stop solo audio, revert to ambient. |
||||
setAudioSession(category: .ambient) |
||||
} |
||||
|
||||
// MARK: Playing Sounds |
||||
|
||||
var currentPlayer: OWSAudioPlayer? |
||||
|
||||
private func stopPlayingAnySounds() { |
||||
currentPlayer?.stop() |
||||
stopRinging() |
||||
} |
||||
|
||||
private func prepareToPlay(sound: OWSStandardSound) -> OWSAudioPlayer? { |
||||
guard let newPlayer = OWSSounds.audioPlayer(forSound: sound.rawValue, audioBehavior: .call) else { |
||||
owsFailDebug("unable to build player for sound: \(OWSSounds.displayName(forSound: sound.rawValue))") |
||||
return nil |
||||
} |
||||
Logger.info("playing sound: \(OWSSounds.displayName(forSound: sound.rawValue))") |
||||
|
||||
// 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() |
||||
self.currentPlayer = newPlayer |
||||
|
||||
return newPlayer |
||||
} |
||||
|
||||
private func play(sound: OWSStandardSound) { |
||||
guard let newPlayer = prepareToPlay(sound: sound) else { return } |
||||
newPlayer.play() |
||||
} |
||||
|
||||
// MARK: - Ringing |
||||
|
||||
private func startRinging(call: IndividualCall) { |
||||
guard handleRinging else { |
||||
Logger.debug("ignoring \(#function) since CallKit handles it's own ringing state") |
||||
return |
||||
} |
||||
|
||||
vibrateTimer?.invalidate() |
||||
vibrateTimer = .scheduledTimer(withTimeInterval: vibrateRepeatDuration, repeats: true) { [weak self] _ in |
||||
self?.ringVibration() |
||||
} |
||||
|
||||
guard let player = prepareToPlay(sound: .defaultiOSIncomingRingtone) else { |
||||
return owsFailDebug("Failed to prepare player for ringing") |
||||
} |
||||
|
||||
startObservingRingerState { [weak self] isDeviceSilenced in |
||||
AssertIsOnMainThread() |
||||
|
||||
// We must ensure the proper audio session before |
||||
// each time we play / pause, otherwise the category |
||||
// may have changed and no playback would occur. |
||||
self?.ensureProperAudioSession(call: call) |
||||
|
||||
if isDeviceSilenced { |
||||
player.pause() |
||||
} else { |
||||
player.play() |
||||
} |
||||
} |
||||
} |
||||
|
||||
private func stopRinging() { |
||||
guard handleRinging else { |
||||
Logger.debug("ignoring \(#function) since CallKit handles it's own ringing state") |
||||
return |
||||
} |
||||
Logger.debug("") |
||||
|
||||
// Stop vibrating |
||||
vibrateTimer?.invalidate() |
||||
vibrateTimer = nil |
||||
|
||||
stopObservingRingerState() |
||||
|
||||
currentPlayer?.stop() |
||||
} |
||||
|
||||
// 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. |
||||
vibrate() |
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + pulseDuration) { |
||||
self.vibrate() |
||||
} |
||||
} |
||||
|
||||
func vibrate() { |
||||
// TODO implement HapticAdapter for iPhone7 and up |
||||
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) |
||||
} |
||||
|
||||
// MARK: - AudioSession MGMT |
||||
// TODO move this to CallAudioSession? |
||||
|
||||
// Note this method is sensitive to the current audio session configuration. |
||||
// Specifically if you call it while speakerphone is enabled you won't see |
||||
// any connected bluetooth routes. |
||||
var availableInputs: [AudioSource] { |
||||
guard let availableInputs = avAudioSession.availableInputs else { |
||||
// I'm not sure why this would happen, but it may indicate an error. |
||||
owsFailDebug("No available inputs or inputs not ready") |
||||
return [AudioSource.builtInSpeaker] |
||||
} |
||||
|
||||
Logger.info("availableInputs: \(availableInputs)") |
||||
return [AudioSource.builtInSpeaker] + availableInputs.map { portDescription in |
||||
return AudioSource(portDescription: portDescription) |
||||
} |
||||
} |
||||
|
||||
var hasExternalInputs: Bool { return availableInputs.count > 2 } |
||||
|
||||
var currentAudioSource: AudioSource? { |
||||
get { |
||||
let outputsByType = avAudioSession.currentRoute.outputs.reduce( |
||||
into: [AVAudioSession.Port: AVAudioSessionPortDescription]() |
||||
) { result, portDescription in |
||||
result[portDescription.portType] = portDescription |
||||
} |
||||
|
||||
let inputsByType = avAudioSession.currentRoute.inputs.reduce( |
||||
into: [AVAudioSession.Port: AVAudioSessionPortDescription]() |
||||
) { result, portDescription in |
||||
result[portDescription.portType] = portDescription |
||||
} |
||||
|
||||
if let builtInMic = inputsByType[.builtInMic], inputsByType[.builtInReceiver] != nil { |
||||
return AudioSource(portDescription: builtInMic) |
||||
} else if outputsByType[.builtInSpeaker] != nil { |
||||
return AudioSource.builtInSpeaker |
||||
} else if let firstRemaining = inputsByType.values.first { |
||||
return AudioSource(portDescription: firstRemaining) |
||||
} else { |
||||
return nil |
||||
} |
||||
} |
||||
set { |
||||
guard currentAudioSource != newValue else { return } |
||||
|
||||
Logger.info("changing preferred input: \(String(describing: currentAudioSource)) -> \(String(describing: newValue))") |
||||
|
||||
if let portDescription = newValue?.portDescription { |
||||
do { |
||||
try avAudioSession.setPreferredInput(portDescription) |
||||
} catch { |
||||
owsFailDebug("failed setting audio source with error: \(error)") |
||||
} |
||||
} else if newValue == AudioSource.builtInSpeaker { |
||||
requestSpeakerphone(isEnabled: true) |
||||
} else { |
||||
owsFailDebug("Tried to set unexpected audio source") |
||||
} |
||||
|
||||
delegate?.callAudioServiceDidChangeAudioSource(self, audioSource: newValue) |
||||
} |
||||
} |
||||
|
||||
private func setAudioSession(category: AVAudioSession.Category, |
||||
mode: AVAudioSession.Mode? = nil, |
||||
options: AVAudioSession.CategoryOptions = AVAudioSession.CategoryOptions(rawValue: 0)) { |
||||
|
||||
AssertIsOnMainThread() |
||||
|
||||
var audioSessionChanged = false |
||||
do { |
||||
if let mode = mode { |
||||
let oldCategory = avAudioSession.category |
||||
let oldMode = avAudioSession.mode |
||||
let oldOptions = avAudioSession.categoryOptions |
||||
|
||||
guard oldCategory != category || oldMode != mode || oldOptions != options else { |
||||
return |
||||
} |
||||
|
||||
audioSessionChanged = true |
||||
|
||||
if oldCategory != category { |
||||
Logger.debug("audio session changed category: \(oldCategory) -> \(category) ") |
||||
} |
||||
if oldMode != mode { |
||||
Logger.debug("audio session changed mode: \(oldMode) -> \(mode) ") |
||||
} |
||||
if oldOptions != options { |
||||
Logger.debug("audio session changed options: \(oldOptions) -> \(options) ") |
||||
} |
||||
try avAudioSession.setCategory(category, mode: mode, options: options) |
||||
|
||||
} else { |
||||
let oldCategory = avAudioSession.category |
||||
let oldOptions = avAudioSession.categoryOptions |
||||
|
||||
guard avAudioSession.category != category || avAudioSession.categoryOptions != options else { |
||||
return |
||||
} |
||||
|
||||
audioSessionChanged = true |
||||
|
||||
if oldCategory != category { |
||||
Logger.debug("audio session changed category: \(oldCategory) -> \(category) ") |
||||
}< |