mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
842 lines
33 KiB
Swift
842 lines
33 KiB
Swift
import Foundation
|
|
import SwiftSignalKit
|
|
import AVFoundation
|
|
import UIKit
|
|
|
|
public enum ManagedAudioSessionType: Equatable {
|
|
case play
|
|
case playWithPossiblePortOverride
|
|
case record(speaker: Bool)
|
|
case voiceCall
|
|
|
|
var isPlay: Bool {
|
|
switch self {
|
|
case .play, .playWithPossiblePortOverride:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
private func nativeCategoryForType(_ type: ManagedAudioSessionType, headphones: Bool, outputMode: AudioSessionOutputMode) -> AVAudioSession.Category {
|
|
switch type {
|
|
case .play:
|
|
return .playback
|
|
case .record, .voiceCall:
|
|
return .playAndRecord
|
|
case .playWithPossiblePortOverride:
|
|
if headphones {
|
|
return .playback
|
|
} else {
|
|
switch outputMode {
|
|
case .custom(.speaker), .system:
|
|
return .playAndRecord
|
|
default:
|
|
return .playback
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum AudioSessionPortType {
|
|
case generic
|
|
case bluetooth
|
|
}
|
|
|
|
public struct AudioSessionPort: Equatable {
|
|
fileprivate let uid: String
|
|
public let name: String
|
|
public let type: AudioSessionPortType
|
|
}
|
|
|
|
public enum AudioSessionOutput: Equatable {
|
|
case builtin
|
|
case speaker
|
|
case headphones
|
|
case port(AudioSessionPort)
|
|
}
|
|
|
|
private let bluetoothPortTypes = Set<AVAudioSession.Port>([.bluetoothA2DP, .bluetoothLE, .bluetoothHFP])
|
|
|
|
private extension AudioSessionOutput {
|
|
init(description: AVAudioSessionPortDescription) {
|
|
self = .port(AudioSessionPort(uid: description.uid, name: description.portName, type: bluetoothPortTypes.contains(description.portType) ? .bluetooth : .generic))
|
|
}
|
|
}
|
|
|
|
public enum AudioSessionOutputMode: Equatable {
|
|
case system
|
|
case speakerIfNoHeadphones
|
|
case custom(AudioSessionOutput)
|
|
|
|
public static func ==(lhs: AudioSessionOutputMode, rhs: AudioSessionOutputMode) -> Bool {
|
|
switch lhs {
|
|
case .system:
|
|
if case .system = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .speakerIfNoHeadphones:
|
|
if case .speakerIfNoHeadphones = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .custom(output):
|
|
if case .custom(output) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class HolderRecord {
|
|
let id: Int32
|
|
let audioSessionType: ManagedAudioSessionType
|
|
let control: ManagedAudioSessionControl
|
|
let activate: (ManagedAudioSessionControl) -> Void
|
|
let deactivate: () -> Signal<Void, NoError>
|
|
let headsetConnectionStatusChanged: (Bool) -> Void
|
|
let availableOutputsChanged: ([AudioSessionOutput], AudioSessionOutput?) -> Void
|
|
let once: Bool
|
|
var outputMode: AudioSessionOutputMode
|
|
var active: Bool = false
|
|
var deactivatingDisposable: Disposable? = nil
|
|
|
|
init(id: Int32, audioSessionType: ManagedAudioSessionType, control: ManagedAudioSessionControl, activate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping () -> Signal<Void, NoError>, headsetConnectionStatusChanged: @escaping (Bool) -> Void, availableOutputsChanged: @escaping ([AudioSessionOutput], AudioSessionOutput?) -> Void, once: Bool, outputMode: AudioSessionOutputMode) {
|
|
self.id = id
|
|
self.audioSessionType = audioSessionType
|
|
self.control = control
|
|
self.activate = activate
|
|
self.deactivate = deactivate
|
|
self.headsetConnectionStatusChanged = headsetConnectionStatusChanged
|
|
self.availableOutputsChanged = availableOutputsChanged
|
|
self.once = once
|
|
self.outputMode = outputMode
|
|
}
|
|
}
|
|
|
|
private final class ManagedAudioSessionControlActivate {
|
|
let f: (AudioSessionActivationState) -> Void
|
|
|
|
init(_ f: @escaping (AudioSessionActivationState) -> Void) {
|
|
self.f = f
|
|
}
|
|
}
|
|
|
|
public struct AudioSessionActivationState {
|
|
public let isHeadsetConnected: Bool
|
|
}
|
|
|
|
public class ManagedAudioSessionControl {
|
|
private let setupImpl: (Bool) -> Void
|
|
private let activateImpl: (ManagedAudioSessionControlActivate) -> Void
|
|
private let setupAndActivateImpl: (Bool, ManagedAudioSessionControlActivate) -> Void
|
|
private let setOutputModeImpl: (AudioSessionOutputMode) -> Void
|
|
|
|
fileprivate init(setupImpl: @escaping (Bool) -> Void, activateImpl: @escaping (ManagedAudioSessionControlActivate) -> Void, setOutputModeImpl: @escaping (AudioSessionOutputMode) -> Void, setupAndActivateImpl: @escaping (Bool, ManagedAudioSessionControlActivate) -> Void) {
|
|
self.setupImpl = setupImpl
|
|
self.activateImpl = activateImpl
|
|
self.setOutputModeImpl = setOutputModeImpl
|
|
self.setupAndActivateImpl = setupAndActivateImpl
|
|
}
|
|
|
|
public func setup(synchronous: Bool = false) {
|
|
self.setupImpl(synchronous)
|
|
}
|
|
|
|
public func activate(_ completion: @escaping (AudioSessionActivationState) -> Void) {
|
|
self.activateImpl(ManagedAudioSessionControlActivate(completion))
|
|
}
|
|
|
|
public func setupAndActivate(synchronous: Bool = false, _ completion: @escaping (AudioSessionActivationState) -> Void) {
|
|
self.setupAndActivateImpl(synchronous, ManagedAudioSessionControlActivate(completion))
|
|
}
|
|
|
|
public func setOutputMode(_ mode: AudioSessionOutputMode) {
|
|
self.setOutputModeImpl(mode)
|
|
}
|
|
}
|
|
|
|
public final class ManagedAudioSession {
|
|
private var nextId: Int32 = 0
|
|
private let queue: Queue
|
|
private let hasLoudspeaker: Bool
|
|
private var holders: [HolderRecord] = []
|
|
private var currentTypeAndOutputMode: (ManagedAudioSessionType, AudioSessionOutputMode)?
|
|
private var deactivateTimer: SwiftSignalKit.Timer?
|
|
|
|
private var isHeadsetPluggedInValue = false
|
|
private let outputsToHeadphonesSubscribers = Bag<(Bool) -> Void>()
|
|
|
|
private var availableOutputsValue: [AudioSessionOutput] = []
|
|
private var currentOutputValue: AudioSessionOutput?
|
|
|
|
private let isActiveSubscribers = Bag<(Bool) -> Void>()
|
|
private let isPlaybackActiveSubscribers = Bag<(Bool) -> Void>()
|
|
|
|
public init() {
|
|
self.queue = Queue()
|
|
|
|
self.hasLoudspeaker = UIDevice.current.model == "iPhone"
|
|
|
|
let queue = self.queue
|
|
NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: AVAudioSession.sharedInstance(), queue: nil, using: { [weak self] _ in
|
|
queue.async {
|
|
self?.updateCurrentAudioRouteInfo()
|
|
}
|
|
})
|
|
|
|
NotificationCenter.default.addObserver(forName: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance(), queue: nil, using: { [weak self] notification in
|
|
guard let info = notification.userInfo,
|
|
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
|
|
return
|
|
}
|
|
|
|
queue.async {
|
|
if let strongSelf = self {
|
|
if type == .began {
|
|
strongSelf.updateHolders(interruption: true)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
queue.async {
|
|
self.isHeadsetPluggedInValue = self.isHeadsetPluggedIn()
|
|
self.updateCurrentAudioRouteInfo()
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
self.deactivateTimer?.invalidate()
|
|
}
|
|
|
|
private func updateCurrentAudioRouteInfo() {
|
|
let value = self.isHeadsetPluggedIn()
|
|
if self.isHeadsetPluggedInValue != value {
|
|
self.isHeadsetPluggedInValue = value
|
|
if let (_, outputMode) = self.currentTypeAndOutputMode {
|
|
if case .speakerIfNoHeadphones = outputMode {
|
|
self.updateOutputMode(outputMode)
|
|
}
|
|
}
|
|
for subscriber in self.outputsToHeadphonesSubscribers.copyItems() {
|
|
subscriber(value)
|
|
}
|
|
for i in 0 ..< self.holders.count {
|
|
if self.holders[i].active {
|
|
self.holders[i].headsetConnectionStatusChanged(value)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
let audioSession = AVAudioSession.sharedInstance()
|
|
|
|
var availableOutputs: [AudioSessionOutput] = []
|
|
var activeOutput: AudioSessionOutput = .builtin
|
|
|
|
if let availableInputs = audioSession.availableInputs {
|
|
var hasHeadphones = false
|
|
|
|
var headphonesAreActive = false
|
|
loop: for currentOutput in audioSession.currentRoute.outputs {
|
|
switch currentOutput.portType {
|
|
case .headphones, .bluetoothA2DP, .bluetoothHFP:
|
|
headphonesAreActive = true
|
|
hasHeadphones = true
|
|
activeOutput = .headphones
|
|
break loop
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
for input in availableInputs {
|
|
var isActive = false
|
|
for currentInput in audioSession.currentRoute.inputs {
|
|
if currentInput.uid == input.uid {
|
|
isActive = true
|
|
}
|
|
}
|
|
|
|
if input.portType == .builtInMic {
|
|
if isActive && !headphonesAreActive {
|
|
activeOutput = .builtin
|
|
inner: for currentOutput in audioSession.currentRoute.outputs {
|
|
if currentOutput.portType == .builtInSpeaker {
|
|
activeOutput = .speaker
|
|
break inner
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if input.portType == .headphones {
|
|
if isActive {
|
|
activeOutput = .headphones
|
|
}
|
|
hasHeadphones = true
|
|
continue
|
|
}
|
|
let output = AudioSessionOutput(description: input)
|
|
availableOutputs.append(output)
|
|
if isActive {
|
|
activeOutput = output
|
|
}
|
|
}
|
|
|
|
if self.hasLoudspeaker {
|
|
availableOutputs.insert(.speaker, at: 0)
|
|
}
|
|
|
|
if hasHeadphones {
|
|
availableOutputs.insert(.headphones, at: 0)
|
|
}
|
|
availableOutputs.insert(.builtin, at: 0)
|
|
}
|
|
|
|
if self.availableOutputsValue != availableOutputs || self.currentOutputValue != activeOutput {
|
|
self.availableOutputsValue = availableOutputs
|
|
self.currentOutputValue = activeOutput
|
|
for i in 0 ..< self.holders.count {
|
|
if self.holders[i].active {
|
|
self.holders[i].availableOutputsChanged(availableOutputs, activeOutput)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func headsetConnected() -> Signal<Bool, NoError> {
|
|
let queue = self.queue
|
|
return Signal { [weak self] subscriber in
|
|
if let strongSelf = self {
|
|
subscriber.putNext(strongSelf.isHeadsetPluggedInValue)
|
|
|
|
let index = strongSelf.outputsToHeadphonesSubscribers.add({ value in
|
|
subscriber.putNext(value)
|
|
})
|
|
|
|
return ActionDisposable {
|
|
queue.async {
|
|
if let strongSelf = self {
|
|
strongSelf.outputsToHeadphonesSubscribers.remove(index)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return EmptyDisposable
|
|
}
|
|
} |> runOn(queue)
|
|
}
|
|
|
|
public func isActive() -> Signal<Bool, NoError> {
|
|
let queue = self.queue
|
|
return Signal { [weak self] subscriber in
|
|
if let strongSelf = self {
|
|
subscriber.putNext(strongSelf.currentTypeAndOutputMode != nil)
|
|
|
|
let index = strongSelf.isActiveSubscribers.add({ value in
|
|
subscriber.putNext(value)
|
|
})
|
|
|
|
return ActionDisposable {
|
|
queue.async {
|
|
if let strongSelf = self {
|
|
strongSelf.isActiveSubscribers.remove(index)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return EmptyDisposable
|
|
}
|
|
} |> runOn(queue)
|
|
}
|
|
|
|
public func isPlaybackActive() -> Signal<Bool, NoError> {
|
|
let queue = self.queue
|
|
return Signal { [weak self] subscriber in
|
|
if let strongSelf = self {
|
|
subscriber.putNext(strongSelf.currentTypeAndOutputMode?.0.isPlay ?? false)
|
|
|
|
let index = strongSelf.isPlaybackActiveSubscribers.add({ value in
|
|
subscriber.putNext(value)
|
|
})
|
|
|
|
return ActionDisposable {
|
|
queue.async {
|
|
if let strongSelf = self {
|
|
strongSelf.isPlaybackActiveSubscribers.remove(index)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return EmptyDisposable
|
|
}
|
|
} |> runOn(queue)
|
|
}
|
|
|
|
public func isOtherAudioPlaying() -> Bool {
|
|
return AVAudioSession.sharedInstance().secondaryAudioShouldBeSilencedHint
|
|
}
|
|
|
|
public func push(audioSessionType: ManagedAudioSessionType, outputMode: AudioSessionOutputMode = .system, once: Bool = false, activate: @escaping (AudioSessionActivationState) -> Void, deactivate: @escaping () -> Signal<Void, NoError>) -> Disposable {
|
|
return self.push(audioSessionType: audioSessionType, once: once, manualActivate: { control in
|
|
control.setupAndActivate(synchronous: false, { state in
|
|
activate(state)
|
|
})
|
|
}, deactivate: deactivate)
|
|
}
|
|
|
|
public func push(audioSessionType: ManagedAudioSessionType, outputMode: AudioSessionOutputMode = .system, once: Bool = false, manualActivate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping () -> Signal<Void, NoError>, headsetConnectionStatusChanged: @escaping (Bool) -> Void = { _ in }, availableOutputsChanged: @escaping ([AudioSessionOutput], AudioSessionOutput?) -> Void = { _, _ in }) -> Disposable {
|
|
let id = OSAtomicIncrement32(&self.nextId)
|
|
let queue = self.queue
|
|
queue.async {
|
|
self.holders.append(HolderRecord(id: id, audioSessionType: audioSessionType, control: ManagedAudioSessionControl(setupImpl: { [weak self] synchronous in
|
|
let f: () -> Void = {
|
|
if let strongSelf = self {
|
|
for holder in strongSelf.holders {
|
|
if holder.id == id && holder.active {
|
|
strongSelf.setup(type: audioSessionType, outputMode: holder.outputMode, activateNow: false)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if synchronous {
|
|
queue.sync(f)
|
|
} else {
|
|
queue.async(f)
|
|
}
|
|
}, activateImpl: { [weak self] completion in
|
|
if let strongSelf = self {
|
|
strongSelf.queue.async {
|
|
for holder in strongSelf.holders {
|
|
if holder.id == id && holder.active {
|
|
strongSelf.activate()
|
|
completion.f(AudioSessionActivationState(isHeadsetConnected: strongSelf.isHeadsetPluggedInValue))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, setOutputModeImpl: { [weak self] value in
|
|
if let strongSelf = self {
|
|
strongSelf.queue.async {
|
|
for holder in strongSelf.holders {
|
|
if holder.id == id {
|
|
if holder.outputMode != value {
|
|
holder.outputMode = value
|
|
}
|
|
|
|
if holder.active {
|
|
strongSelf.updateOutputMode(value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, setupAndActivateImpl: { [weak self] synchronous, completion in
|
|
queue.async {
|
|
let f: () -> Void = {
|
|
if let strongSelf = self {
|
|
for holder in strongSelf.holders {
|
|
if holder.id == id && holder.active {
|
|
strongSelf.setup(type: audioSessionType, outputMode: holder.outputMode, activateNow: true)
|
|
completion.f(AudioSessionActivationState(isHeadsetConnected: strongSelf.isHeadsetPluggedInValue))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if synchronous {
|
|
queue.sync(f)
|
|
} else {
|
|
queue.async(f)
|
|
}
|
|
}
|
|
}), activate: { [weak self] state in
|
|
manualActivate(state)
|
|
queue.async {
|
|
if let strongSelf = self {
|
|
strongSelf.updateCurrentAudioRouteInfo()
|
|
availableOutputsChanged(strongSelf.availableOutputsValue, strongSelf.currentOutputValue)
|
|
}
|
|
}
|
|
}, deactivate: deactivate, headsetConnectionStatusChanged: headsetConnectionStatusChanged, availableOutputsChanged: availableOutputsChanged, once: once, outputMode: outputMode))
|
|
self.updateHolders()
|
|
}
|
|
return ActionDisposable { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.queue.async {
|
|
strongSelf.removeDeactivatedHolder(id: id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func dropAll() {
|
|
self.queue.async {
|
|
self.updateHolders(interruption: true)
|
|
}
|
|
}
|
|
|
|
private func removeDeactivatedHolder(id: Int32) {
|
|
assert(self.queue.isCurrent())
|
|
|
|
for i in 0 ..< self.holders.count {
|
|
if self.holders[i].id == id {
|
|
self.holders[i].deactivatingDisposable?.dispose()
|
|
self.holders.remove(at: i)
|
|
self.updateHolders()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateHolders(interruption: Bool = false) {
|
|
assert(self.queue.isCurrent())
|
|
|
|
print("holder count \(self.holders.count)")
|
|
|
|
if !self.holders.isEmpty {
|
|
var activeIndex: Int?
|
|
var deactivating = false
|
|
var index = 0
|
|
for record in self.holders {
|
|
if record.active {
|
|
activeIndex = index
|
|
break
|
|
}
|
|
else if record.deactivatingDisposable != nil {
|
|
deactivating = true
|
|
}
|
|
index += 1
|
|
}
|
|
if !deactivating {
|
|
if let activeIndex = activeIndex {
|
|
var deactivate = false
|
|
|
|
if interruption {
|
|
if self.holders[activeIndex].audioSessionType != .voiceCall {
|
|
deactivate = true
|
|
}
|
|
} else {
|
|
if activeIndex != self.holders.count - 1 {
|
|
if self.holders[activeIndex].audioSessionType == .voiceCall {
|
|
deactivate = false
|
|
} else {
|
|
deactivate = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if deactivate {
|
|
self.holders[activeIndex].active = false
|
|
let id = self.holders[activeIndex].id
|
|
self.holders[activeIndex].deactivatingDisposable = (self.holders[activeIndex].deactivate()
|
|
|> deliverOn(self.queue)).start(completed: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
var index = 0
|
|
for currentRecord in strongSelf.holders {
|
|
if currentRecord.id == id {
|
|
currentRecord.deactivatingDisposable = nil
|
|
if currentRecord.once {
|
|
strongSelf.holders.remove(at: index)
|
|
}
|
|
break
|
|
}
|
|
index += 1
|
|
}
|
|
strongSelf.updateHolders()
|
|
})
|
|
}
|
|
} else if activeIndex == nil {
|
|
let lastIndex = self.holders.count - 1
|
|
|
|
self.deactivateTimer?.invalidate()
|
|
self.deactivateTimer = nil
|
|
|
|
self.holders[lastIndex].active = true
|
|
self.holders[lastIndex].activate(self.holders[lastIndex].control)
|
|
}
|
|
}
|
|
} else {
|
|
self.applyNoneDelayed()
|
|
}
|
|
}
|
|
|
|
private func applyNoneDelayed() {
|
|
self.deactivateTimer?.invalidate()
|
|
|
|
var immediately = false
|
|
if let mode = self.currentTypeAndOutputMode?.0 {
|
|
switch mode {
|
|
case .voiceCall, .record:
|
|
immediately = true
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if immediately {
|
|
self.applyNone()
|
|
} else {
|
|
let deactivateTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.applyNone()
|
|
}
|
|
}, queue: self.queue)
|
|
self.deactivateTimer = deactivateTimer
|
|
deactivateTimer.start()
|
|
}
|
|
}
|
|
|
|
private func isHeadsetPluggedIn() -> Bool {
|
|
assert(self.queue.isCurrent())
|
|
|
|
let route = AVAudioSession.sharedInstance().currentRoute
|
|
//print("\(route)")
|
|
for desc in route.outputs {
|
|
if desc.portType == .headphones || desc.portType == .bluetoothA2DP || desc.portType == .bluetoothHFP {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
private func applyNone() {
|
|
self.deactivateTimer?.invalidate()
|
|
self.deactivateTimer = nil
|
|
|
|
let wasActive = self.currentTypeAndOutputMode != nil
|
|
let wasPlaybackActive = self.currentTypeAndOutputMode?.0.isPlay ?? false
|
|
self.currentTypeAndOutputMode = nil
|
|
|
|
print("ManagedAudioSession setting active false")
|
|
do {
|
|
try AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
|
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
|
|
try AVAudioSession.sharedInstance().setPreferredInput(nil)
|
|
} catch let error {
|
|
print("ManagedAudioSession applyNone error \(error)")
|
|
}
|
|
|
|
if wasActive {
|
|
for subscriber in self.isActiveSubscribers.copyItems() {
|
|
subscriber(false)
|
|
}
|
|
}
|
|
if wasPlaybackActive {
|
|
for subscriber in self.isPlaybackActiveSubscribers.copyItems() {
|
|
subscriber(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func setup(type: ManagedAudioSessionType, outputMode: AudioSessionOutputMode, activateNow: Bool) {
|
|
self.deactivateTimer?.invalidate()
|
|
self.deactivateTimer = nil
|
|
|
|
let wasActive = self.currentTypeAndOutputMode != nil
|
|
let wasPlaybackActive = self.currentTypeAndOutputMode?.0.isPlay ?? false
|
|
|
|
if self.currentTypeAndOutputMode == nil || self.currentTypeAndOutputMode! != (type, outputMode) {
|
|
self.currentTypeAndOutputMode = (type, outputMode)
|
|
|
|
do {
|
|
let nativeCategory = nativeCategoryForType(type, headphones: self.isHeadsetPluggedInValue, outputMode: outputMode)
|
|
|
|
print("ManagedAudioSession setting category for \(type) (native: \(nativeCategory))")
|
|
var options: AVAudioSession.CategoryOptions = []
|
|
switch type {
|
|
case .play:
|
|
break
|
|
case .playWithPossiblePortOverride:
|
|
if case .playAndRecord = nativeCategory {
|
|
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
|
options.insert(.allowBluetoothA2DP)
|
|
} else {
|
|
options.insert(.allowBluetooth)
|
|
}
|
|
}
|
|
case .record, .voiceCall:
|
|
options.insert(.allowBluetooth)
|
|
}
|
|
print("ManagedAudioSession setting active true")
|
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
|
try AVAudioSession.sharedInstance().setCategory(nativeCategory, mode: type == .voiceCall ? .voiceChat : .default, policy: .default, options: options)
|
|
} else {
|
|
AVAudioSession.sharedInstance().perform(NSSelectorFromString("setCategory:error:"), with: nativeCategory)
|
|
try AVAudioSession.sharedInstance().setMode(type == .voiceCall ? .voiceChat : .default)
|
|
}
|
|
} catch let error {
|
|
print("ManagedAudioSession setup error \(error)")
|
|
}
|
|
}
|
|
|
|
if !wasActive {
|
|
for subscriber in self.isActiveSubscribers.copyItems() {
|
|
subscriber(true)
|
|
}
|
|
}
|
|
if !wasPlaybackActive && (self.currentTypeAndOutputMode?.0.isPlay ?? false) {
|
|
for subscriber in self.isPlaybackActiveSubscribers.copyItems() {
|
|
subscriber(true)
|
|
}
|
|
}
|
|
|
|
if activateNow {
|
|
self.activate()
|
|
}
|
|
}
|
|
|
|
private func setupOutputMode(_ outputMode: AudioSessionOutputMode, type: ManagedAudioSessionType) throws {
|
|
print("ManagedAudioSession setup \(outputMode) for \(type)")
|
|
var resetToBuiltin = false
|
|
switch outputMode {
|
|
case .system:
|
|
resetToBuiltin = true
|
|
case let .custom(output):
|
|
switch output {
|
|
case .builtin:
|
|
resetToBuiltin = true
|
|
case .speaker:
|
|
if type == .voiceCall {
|
|
if let routes = AVAudioSession.sharedInstance().availableInputs {
|
|
for route in routes {
|
|
if route.portType == .builtInMic {
|
|
let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
|
|
case .headphones:
|
|
break
|
|
case let .port(port):
|
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
|
|
if let routes = AVAudioSession.sharedInstance().availableInputs {
|
|
for route in routes {
|
|
if route.uid == port.uid {
|
|
let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case .speakerIfNoHeadphones:
|
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
|
|
}
|
|
|
|
if resetToBuiltin {
|
|
var updatedType = type
|
|
if case .record(false) = updatedType, self.isHeadsetPluggedInValue {
|
|
updatedType = .record(speaker: true)
|
|
}
|
|
switch updatedType {
|
|
case .record(false):
|
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
|
|
case .voiceCall, .playWithPossiblePortOverride, .record(true):
|
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
|
|
if let routes = AVAudioSession.sharedInstance().availableInputs {
|
|
var alreadySet = false
|
|
if self.isHeadsetPluggedInValue {
|
|
loop: for route in routes {
|
|
switch route.portType {
|
|
case .headphones, .bluetoothA2DP, .bluetoothHFP:
|
|
let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
|
|
alreadySet = true
|
|
break loop
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !alreadySet {
|
|
for route in routes {
|
|
if route.portType == .builtInMic {
|
|
if case .record = updatedType, self.isHeadsetPluggedInValue {
|
|
} else {
|
|
let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func activate() {
|
|
if let (type, outputMode) = self.currentTypeAndOutputMode {
|
|
do {
|
|
let startTime = CFAbsoluteTimeGetCurrent()
|
|
|
|
try AVAudioSession.sharedInstance().setActive(true, options: [.notifyOthersOnDeactivation])
|
|
|
|
print("\(CFAbsoluteTimeGetCurrent()) AudioSession activate: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
|
|
|
|
self.updateCurrentAudioRouteInfo()
|
|
|
|
print("\(CFAbsoluteTimeGetCurrent()) AudioSession updateCurrentAudioRouteInfo: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
|
|
|
|
try self.setupOutputMode(outputMode, type: type)
|
|
|
|
print("\(CFAbsoluteTimeGetCurrent()) AudioSession setupOutputMode: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
|
|
|
|
if case .voiceCall = type {
|
|
try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.005)
|
|
}
|
|
} catch let error {
|
|
print("ManagedAudioSession activate error \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateOutputMode(_ outputMode: AudioSessionOutputMode) {
|
|
if let (type, currentOutputMode) = self.currentTypeAndOutputMode, currentOutputMode != outputMode {
|
|
//self.currentTypeAndOutputMode = (type, outputMode)
|
|
do {
|
|
try self.setup(type: type, outputMode: outputMode, activateNow: true)
|
|
//try self.setupOutputMode(outputMode, type: type)
|
|
//try self.activate()
|
|
} catch let error {
|
|
print("ManagedAudioSession overrideOutputAudioPort error \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
public func callKitActivatedAudioSession() {
|
|
/*self.queue.async {
|
|
print("ManagedAudioSession callKitDeactivatedAudioSession")
|
|
self.callKitAudioSessionIsActive = true
|
|
self.updateHolders()
|
|
}*/
|
|
}
|
|
|
|
public func callKitDeactivatedAudioSession() {
|
|
/*self.queue.async {
|
|
print("ManagedAudioSession callKitDeactivatedAudioSession")
|
|
self.callKitAudioSessionIsActive = false
|
|
self.updateHolders()
|
|
}*/
|
|
}
|
|
}
|