Files
Swiftgram/submodules/TelegramAudio/Sources/ManagedAudioSession.swift
Kylmakalle fd86110711 Version 11.3.1
Fixes

fix localeWithStrings globally (#30)

Fix badge on zoomed devices. closes #9

Hide channel bottom panel closes #27

Another attempt to fix badge on some Zoomed devices

Force System Share sheet tg://sg/debug

fixes for device badge

New Crowdin updates (#34)

* New translations sglocalizable.strings (Chinese Traditional)

* New translations sglocalizable.strings (Chinese Simplified)

* New translations sglocalizable.strings (Chinese Traditional)

Fix input panel hidden on selection (#31)

* added if check for selectionState != nil

* same order of subnodes

Revert "Fix input panel hidden on selection (#31)"

This reverts commit e8a8bb1496.

Fix input panel for channels Closes #37

Quickly share links with system's share menu

force tabbar when editing

increase height for correct animation

New translations sglocalizable.strings (Ukrainian) (#38)

Hide Post Story button

Fix 10.15.1

Fix archive option for long-tap

Enable in-app Safari

Disable some unsupported purchases

disableDeleteChatSwipeOption + refactor restart alert

Hide bot in suggestions list

Fix merge v11.0

Fix exceptions for safari webview controller

New Crowdin updates (#47)

* New translations sglocalizable.strings (Romanian)

* New translations sglocalizable.strings (French)

* New translations sglocalizable.strings (Spanish)

* New translations sglocalizable.strings (Afrikaans)

* New translations sglocalizable.strings (Arabic)

* New translations sglocalizable.strings (Catalan)

* New translations sglocalizable.strings (Czech)

* New translations sglocalizable.strings (Danish)

* New translations sglocalizable.strings (German)

* New translations sglocalizable.strings (Greek)

* New translations sglocalizable.strings (Finnish)

* New translations sglocalizable.strings (Hebrew)

* New translations sglocalizable.strings (Hungarian)

* New translations sglocalizable.strings (Italian)

* New translations sglocalizable.strings (Japanese)

* New translations sglocalizable.strings (Korean)

* New translations sglocalizable.strings (Dutch)

* New translations sglocalizable.strings (Norwegian)

* New translations sglocalizable.strings (Polish)

* New translations sglocalizable.strings (Portuguese)

* New translations sglocalizable.strings (Serbian (Cyrillic))

* New translations sglocalizable.strings (Swedish)

* New translations sglocalizable.strings (Turkish)

* New translations sglocalizable.strings (Vietnamese)

* New translations sglocalizable.strings (Indonesian)

* New translations sglocalizable.strings (Hindi)

* New translations sglocalizable.strings (Uzbek)

New Crowdin updates (#49)

* New translations sglocalizable.strings (Arabic)

* New translations sglocalizable.strings (Arabic)

New translations sglocalizable.strings (Russian) (#51)

Call confirmation

WIP Settings search

Settings Search

Localize placeholder

Update AccountUtils.swift

mark mutual contact

Align back context action to left

New Crowdin updates (#54)

* New translations sglocalizable.strings (Chinese Simplified)

* New translations sglocalizable.strings (Chinese Traditional)

* New translations sglocalizable.strings (Ukrainian)

Independent Playground app for simulator

New translations sglocalizable.strings (Ukrainian) (#55)

Playground UIKit base and controllers

Inject SwiftUI view with overflow to AsyncDisplayKit

Launch Playgound project on simulator

Create .swiftformat

Move Playground to example

Update .swiftformat

Init SwiftUIViewController

wip

New translations sglocalizable.strings (Chinese Traditional) (#57)

Xcode 16 fixes

Fix

New translations sglocalizable.strings (Italian) (#59)

New translations sglocalizable.strings (Chinese Simplified) (#63)

Force disable CallKit integration due to missing NSE Entitlement

Fix merge

Fix whole chat translator

Sweetpad config

Bump version

11.3.1 fixes

Mutual contact placement fix

Disable Video PIP swipe

Update versions.json

Fix PIP crash
2024-12-20 09:38:13 +02:00

1165 lines
47 KiB
Swift

import SGSimpleSettings
import Foundation
import UIKit
import SwiftSignalKit
import AVFoundation
private var managedAudioSessionLogger: (String) -> Void = { _ in }
public func setManagedAudioSessionLogger(_ f: @escaping (String) -> Void) {
managedAudioSessionLogger = f
}
func managedAudioSessionLog(_ what: @autoclosure () -> String) {
managedAudioSessionLogger(what())
}
public enum ManagedAudioSessionType: Equatable {
case ambient
case play(mixWithOthers: Bool)
case playWithPossiblePortOverride
case record(speaker: Bool, video: Bool, withOthers: Bool)
case voiceCall
case videoCall
var isPlay: Bool {
switch self {
case .play, .ambient, .playWithPossiblePortOverride:
return true
default:
return false
}
}
}
private func nativeCategoryForType(_ type: ManagedAudioSessionType, headphones: Bool, outputMode: AudioSessionOutputMode) -> AVAudioSession.Category {
switch type {
case .ambient:
return .ambient
case .play:
return .playback
case .record, .voiceCall, .videoCall:
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
case wired
}
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) {
var type: AudioSessionPortType = .generic
if bluetoothPortTypes.contains(description.portType) {
type = .bluetooth
} else if description.uid == "Wired Headphones" || description.uid == "Wired Microphone" {
type = .wired
}
self = .port(AudioSessionPort(uid: description.uid, name: description.portName, type: type))
}
}
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
var audioSessionType: ManagedAudioSessionType
let control: ManagedAudioSessionControl
let activate: (ManagedAudioSessionControl) -> Void
let deactivate: (Bool) -> 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 (Bool) -> 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
private let setTypeImpl: (ManagedAudioSessionType, @escaping () -> Void) -> Void
fileprivate init(setupImpl: @escaping (Bool) -> Void, activateImpl: @escaping (ManagedAudioSessionControlActivate) -> Void, setOutputModeImpl: @escaping (AudioSessionOutputMode) -> Void, setupAndActivateImpl: @escaping (Bool, ManagedAudioSessionControlActivate) -> Void, setTypeImpl: @escaping (ManagedAudioSessionType, @escaping () -> Void) -> Void) {
self.setupImpl = setupImpl
self.activateImpl = activateImpl
self.setOutputModeImpl = setOutputModeImpl
self.setupAndActivateImpl = setupAndActivateImpl
self.setTypeImpl = setTypeImpl
}
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 func setType(_ audioSessionType: ManagedAudioSessionType, completion: @escaping () -> Void) {
self.setTypeImpl(audioSessionType, completion)
}
}
public final class ManagedAudioSessionClientParams {
public let audioSessionType: ManagedAudioSessionType
public let outputMode: AudioSessionOutputMode
public let once: Bool
public let activateImmediately: Bool
public let manualActivate: (ManagedAudioSessionControl) -> Void
public let deactivate: (Bool) -> Signal<Void, NoError>
public let headsetConnectionStatusChanged: (Bool) -> Void
public let availableOutputsChanged: ([AudioSessionOutput], AudioSessionOutput?) -> Void
public init(
audioSessionType: ManagedAudioSessionType,
outputMode: AudioSessionOutputMode = .system,
once: Bool = false,
activateImmediately: Bool = false,
manualActivate: @escaping (ManagedAudioSessionControl) -> Void,
deactivate: @escaping (Bool) -> Signal<Void, NoError>,
headsetConnectionStatusChanged: @escaping (Bool) -> Void,
availableOutputsChanged: @escaping ([AudioSessionOutput], AudioSessionOutput?) -> Void
) {
self.audioSessionType = audioSessionType
self.outputMode = outputMode
self.once = once
self.activateImmediately = activateImmediately
self.manualActivate = manualActivate
self.deactivate = deactivate
self.headsetConnectionStatusChanged = headsetConnectionStatusChanged
self.availableOutputsChanged = availableOutputsChanged
}
}
public protocol ManagedAudioSession: AnyObject {
func getIsHeadsetPluggedIn() -> Bool
func headsetConnected() -> Signal<Bool, NoError>
func isActive() -> Signal<Bool, NoError>
func isPlaybackActive() -> Signal<Bool, NoError>
func isOtherAudioPlaying() -> Bool
func push(params: ManagedAudioSessionClientParams) -> Disposable
func dropAll()
func applyVoiceChatOutputModeInCurrentAudioSession(outputMode: AudioSessionOutputMode)
func callKitActivatedAudioSession()
func callKitDeactivatedAudioSession()
}
public extension ManagedAudioSession {
func push(
audioSessionType: ManagedAudioSessionType,
outputMode: AudioSessionOutputMode = .system,
once: Bool = false,
activate: @escaping (AudioSessionActivationState) -> Void,
deactivate: @escaping (Bool) -> Signal<Void, NoError>
) -> Disposable {
return self.push(audioSessionType: audioSessionType, once: once, manualActivate: { control in
control.setupAndActivate(synchronous: false, { state in
activate(state)
})
}, deactivate: deactivate)
}
func push(
audioSessionType: ManagedAudioSessionType,
outputMode: AudioSessionOutputMode = .system,
once: Bool = false,
activateImmediately: Bool = false,
manualActivate: @escaping (ManagedAudioSessionControl) -> Void,
deactivate: @escaping (Bool) -> Signal<Void, NoError>,
headsetConnectionStatusChanged: @escaping (Bool) -> Void = { _ in },
availableOutputsChanged: @escaping ([AudioSessionOutput], AudioSessionOutput?) -> Void = { _, _ in }
) -> Disposable {
return self.push(params: ManagedAudioSessionClientParams(
audioSessionType: audioSessionType,
outputMode: outputMode,
once: once,
activateImmediately: activateImmediately,
manualActivate: manualActivate,
deactivate: deactivate,
headsetConnectionStatusChanged: headsetConnectionStatusChanged,
availableOutputsChanged: availableOutputsChanged
))
}
}
private var sharedManagedAudioSessionValue: ManagedAudioSession?
public var sharedManagedAudioSession: ManagedAudioSession? {
return sharedManagedAudioSessionValue
}
public final class ManagedAudioSessionImpl: NSObject, 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 let isHeadsetPluggedInSync = Atomic<Bool>(value: false)
private var isHeadsetPluggedInValue = false {
didSet {
if self.isHeadsetPluggedInValue != oldValue {
let _ = self.isHeadsetPluggedInSync.swap(self.isHeadsetPluggedInValue)
}
}
}
public func getIsHeadsetPluggedIn() -> Bool {
return self.isHeadsetPluggedInSync.with { $0 }
}
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>()
private var isActiveValue: Bool = false
private var callKitAudioSessionIsActive: Bool = false
override public init() {
self.queue = Queue()
self.hasLoudspeaker = UIDevice.current.model == "iPhone"
super.init()
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
managedAudioSessionLog("Interruption received")
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
managedAudioSessionLog("Interruption type: \(type)")
queue.async {
if let strongSelf = self {
if type == .began {
strongSelf.updateHolders(interruption: true)
}
}
}
})
NotificationCenter.default.addObserver(forName: AVAudioSession.mediaServicesWereLostNotification, object: AVAudioSession.sharedInstance(), queue: nil, using: { [weak self] _ in
managedAudioSessionLog("Media Services were lost")
queue.after(1.0, {
if let strongSelf = self {
if let (type, outputMode) = strongSelf.currentTypeAndOutputMode {
strongSelf.setup(type: type, outputMode: outputMode, activateNow: true)
}
}
})
})
queue.async {
self.isHeadsetPluggedInValue = self.isHeadsetPluggedIn()
self.updateCurrentAudioRouteInfo()
}
sharedManagedAudioSessionValue = self
}
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 hasBluetoothHeadphones = false
var headphonesAreActive = false
loop: for currentOutput in audioSession.currentRoute.outputs {
switch currentOutput.portType {
case .headphones, .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
headphonesAreActive = true
hasHeadphones = true
hasBluetoothHeadphones = [.bluetoothA2DP, .bluetoothHFP, .bluetoothLE].contains(currentOutput.portType)
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 && !hasBluetoothHeadphones {
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.isActiveValue || strongSelf.callKitAudioSessionIsActive)
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(params: ManagedAudioSessionClientParams) -> Disposable {
let audioSessionType = params.audioSessionType
let outputMode = params.outputMode
let once = params.once
let activateImmediately = params.activateImmediately
let manualActivate = params.manualActivate
let deactivate = params.deactivate
let headsetConnectionStatusChanged = params.headsetConnectionStatusChanged
let availableOutputsChanged = params.availableOutputsChanged
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: activateImmediately)
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 {
if strongSelf.currentTypeAndOutputMode?.0 != holder.audioSessionType || strongSelf.currentTypeAndOutputMode?.1 != holder.outputMode {
strongSelf.setup(type: holder.audioSessionType, outputMode: holder.outputMode, activateNow: true)
} else {
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)
}
}
}, setTypeImpl: { [weak self] audioSessionType, completion in
queue.async {
if let strongSelf = self {
for holder in strongSelf.holders {
if holder.id == id {
if holder.audioSessionType != audioSessionType {
holder.audioSessionType = audioSessionType
}
if holder.active {
strongSelf.updateAudioSessionType(audioSessionType)
}
}
}
}
completion()
}
}), 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())
managedAudioSessionLog("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
}
var lastIsRecordWithOthers = false
if let lastHolder = self.holders.last {
if case let .record(_, _, withOthers) = lastHolder.audioSessionType {
lastIsRecordWithOthers = withOthers
}
}
if !deactivating {
if let activeIndex = activeIndex {
var deactivate = false
var temporary = false
if interruption {
if self.holders[activeIndex].audioSessionType != .voiceCall {
deactivate = true
}
} else {
if activeIndex != self.holders.count - 1 {
if lastIsRecordWithOthers {
deactivate = true
temporary = true
} else 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(temporary)
|> 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
//managedAudioSessionLog("\(route)")
for desc in route.outputs {
if desc.portType == .headphones || desc.portType == .bluetoothA2DP || desc.portType == .bluetoothHFP || desc.portType == .bluetoothLE {
return true
}
}
return false
}
private func applyNone() {
self.deactivateTimer?.invalidate()
self.deactivateTimer = nil
let wasPlaybackActive = self.currentTypeAndOutputMode?.0.isPlay ?? false
self.currentTypeAndOutputMode = nil
managedAudioSessionLog("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 {
managedAudioSessionLog("ManagedAudioSession applyNone error \(error), waiting")
Thread.sleep(forTimeInterval: 2.0)
do {
try AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
try AVAudioSession.sharedInstance().setPreferredInput(nil)
} catch let error {
managedAudioSessionLog("ManagedAudioSession applyNone repeated error \(error), giving up")
}
}
self.isActiveValue = false
for subscriber in self.isActiveSubscribers.copyItems() {
subscriber(self.isActiveValue || self.callKitAudioSessionIsActive)
}
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 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)
managedAudioSessionLog("ManagedAudioSession setting category for \(type) (native: \(nativeCategory)) activateNow: \(activateNow)")
var options: AVAudioSession.CategoryOptions = []
switch type {
case let .play(mixWithOthers):
if mixWithOthers {
options.insert(.mixWithOthers)
}
case .ambient:
options.insert(.mixWithOthers)
case .playWithPossiblePortOverride:
if case .playAndRecord = nativeCategory {
options.insert(.allowBluetoothA2DP)
}
case .voiceCall, .videoCall:
options.insert(.allowBluetooth)
options.insert(.allowBluetoothA2DP)
options.insert(.mixWithOthers)
case let .record(_, video, mixWithOthers):
options.insert(.allowBluetooth)
if video {
options.insert(.allowBluetoothA2DP)
}
if mixWithOthers {
options.insert(.mixWithOthers)
}
}
managedAudioSessionLog("ManagedAudioSession setting category and options")
let mode: AVAudioSession.Mode
switch type {
case .voiceCall:
mode = .voiceChat
case .videoCall:
mode = .videoChat
case .record(_, true, _):
mode = .videoRecording
default:
mode = .default
}
switch type {
case .play(mixWithOthers: true), .ambient:
do {
try AVAudioSession.sharedInstance().setActive(false)
} catch let error {
managedAudioSessionLog("ManagedAudioSession setActive error \(error)")
}
default:
break
}
if #available(iOS 13.0, *) {
#if DEBUG && false
try AVAudioSession.sharedInstance().setCategory(nativeCategory, mode: mode, policy: .longFormVideo, options: options)
#else
try AVAudioSession.sharedInstance().setCategory(nativeCategory, options: options)
try AVAudioSession.sharedInstance().setMode(mode)
#endif
} else {
try AVAudioSession.sharedInstance().setCategory(nativeCategory, options: options)
try AVAudioSession.sharedInstance().setMode(mode)
}
if AVAudioSession.sharedInstance().categoryOptions != options {
switch type {
case .voiceCall, .videoCall:
managedAudioSessionLog("ManagedAudioSession resetting options")
try AVAudioSession.sharedInstance().setCategory(nativeCategory, options: options)
default:
break
}
}
} catch let error {
managedAudioSessionLog("ManagedAudioSession setup error \(error)")
}
}
self.isActiveValue = true
for subscriber in self.isActiveSubscribers.copyItems() {
subscriber(self.isActiveValue || self.callKitAudioSessionIsActive)
}
if !wasPlaybackActive && (self.currentTypeAndOutputMode?.0.isPlay ?? false) {
for subscriber in self.isPlaybackActiveSubscribers.copyItems() {
subscriber(true)
}
}
if activateNow {
self.activate()
}
}
public func applyVoiceChatOutputModeInCurrentAudioSession(outputMode: AudioSessionOutputMode) {
managedAudioSessionLog("applyVoiceChatOutputModeInCurrentAudioSession \(outputMode)")
do {
var resetToBuiltin = false
switch outputMode {
case .system:
resetToBuiltin = true
case let .custom(output):
switch output {
case .builtin:
resetToBuiltin = true
case .speaker:
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 {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
if let routes = AVAudioSession.sharedInstance().availableInputs {
for route in routes {
if route.portType == .builtInMic {
let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
break
}
}
}
}
} catch let e {
managedAudioSessionLog("applyVoiceChatOutputModeInCurrentAudioSession error: \(e)")
}
}
private func setupOutputMode(_ outputMode: AudioSessionOutputMode, type: ManagedAudioSessionType) throws {
managedAudioSessionLog("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().setInputDataSource(route.selectedDataSource)
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 case let .record(_, video, _) = type, video, let input = AVAudioSession.sharedInstance().availableInputs?.first {
if let dataSources = input.dataSources {
for source in dataSources {
if source.dataSourceName.contains("Bottom") {
try? input.setPreferredDataSource(source)
break
}
}
}
}
if resetToBuiltin {
var updatedType = type
if case .record(false, let video, let withOthers) = updatedType, self.isHeadsetPluggedInValue {
updatedType = .record(speaker: true, video: video, withOthers: withOthers)
}
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 {
if case .voiceCall = updatedType, case .custom(.builtin) = outputMode {
} else if SGSimpleSettings.shared.forceBuiltInMic {
let _ = try? AVAudioSession.sharedInstance().setPreferredInput(
routes.first { $0.portType == .builtInMic }
)
} else {
loop: for route in routes {
switch route.portType {
case .headphones, .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
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)
let _ = try? AVAudioSession.sharedInstance().setInputDataSource(nil)
}
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])
managedAudioSessionLog("\(CFAbsoluteTimeGetCurrent()) AudioSession activate: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
try self.setupOutputMode(outputMode, type: type)
managedAudioSessionLog("\(CFAbsoluteTimeGetCurrent()) AudioSession setupOutputMode: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
self.updateCurrentAudioRouteInfo()
managedAudioSessionLog("\(CFAbsoluteTimeGetCurrent()) AudioSession updateCurrentAudioRouteInfo: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
if case .voiceCall = type {
//try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.005)
}
} catch let error {
managedAudioSessionLog("ManagedAudioSession activate error \(error)")
}
}
}
private func updateAudioSessionType(_ audioSessionType: ManagedAudioSessionType) {
if let (_, outputMode) = self.currentTypeAndOutputMode {
self.setup(type: audioSessionType, outputMode: outputMode, activateNow: true)
}
}
private func updateOutputMode(_ outputMode: AudioSessionOutputMode) {
if let (type, _) = self.currentTypeAndOutputMode {
self.setup(type: type, outputMode: outputMode, activateNow: true)
}
}
public func callKitActivatedAudioSession() {
self.queue.async {
managedAudioSessionLog("ManagedAudioSession callKitActivatedAudioSession")
self.callKitAudioSessionIsActive = true
self.updateHolders()
for subscriber in self.isActiveSubscribers.copyItems() {
subscriber(self.isActiveValue || self.callKitAudioSessionIsActive)
}
}
}
public func callKitDeactivatedAudioSession() {
self.queue.async {
managedAudioSessionLog("ManagedAudioSession callKitDeactivatedAudioSession")
self.callKitAudioSessionIsActive = false
self.updateHolders()
for subscriber in self.isActiveSubscribers.copyItems() {
subscriber(self.isActiveValue || self.callKitAudioSessionIsActive)
}
}
}
}