From 5f01a83214147e4f6881285a485443065039198b Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sat, 12 Apr 2025 23:01:52 +0400 Subject: [PATCH] Conference --- .../Sources/PresentationCall.swift | 212 +--------------- .../Sources/PresentationGroupCall.swift | 12 +- .../Sources/SharedCallAudioContext.swift | 231 ++++++++++++++++++ .../ContactMultiselectionController.swift | 2 +- .../Sources/OngoingCallThreadLocalContext.mm | 11 + 5 files changed, 253 insertions(+), 215 deletions(-) create mode 100644 submodules/TelegramCallsUI/Sources/SharedCallAudioContext.swift diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index c6b182ac82..a52f428d9b 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -15,216 +15,6 @@ import AccountContext import DeviceProximity import PhoneNumberFormat -public final class SharedCallAudioContext { - let audioDevice: OngoingCallContext.AudioDevice? - let callKitIntegration: CallKitIntegration? - - private let defaultToSpeaker: Bool - - private var audioSessionDisposable: Disposable? - private var audioSessionShouldBeActiveDisposable: Disposable? - private var isAudioSessionActiveDisposable: Disposable? - private var audioOutputStateDisposable: Disposable? - - private(set) var audioSessionControl: ManagedAudioSessionControl? - - private let isAudioSessionActivePromise = Promise(false) - private var isAudioSessionActive: Signal { - return self.isAudioSessionActivePromise.get() - } - - private let audioOutputStatePromise = Promise<([AudioSessionOutput], AudioSessionOutput?)>(([], nil)) - private var audioOutputStateValue: ([AudioSessionOutput], AudioSessionOutput?) = ([], nil) - public private(set) var currentAudioOutputValue: AudioSessionOutput = .builtin - private var didSetCurrentAudioOutputValue: Bool = false - var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { - return self.audioOutputStatePromise.get() - } - - private let audioSessionShouldBeActive = Promise(true) - private var initialSetupTimer: Foundation.Timer? - - init(audioSession: ManagedAudioSession, callKitIntegration: CallKitIntegration?, defaultToSpeaker: Bool = false) { - self.callKitIntegration = callKitIntegration - self.audioDevice = OngoingCallContext.AudioDevice.create(enableSystemMute: false) - self.defaultToSpeaker = defaultToSpeaker - - if defaultToSpeaker { - self.didSetCurrentAudioOutputValue = true - self.currentAudioOutputValue = .speaker - } - - var didReceiveAudioOutputs = false - self.audioSessionDisposable = audioSession.push(audioSessionType: .voiceCall, manualActivate: { [weak self] control in - Queue.mainQueue().async { - guard let self else { - return - } - let previousControl = self.audioSessionControl - self.audioSessionControl = control - - if previousControl == nil, let audioSessionControl = self.audioSessionControl { - if let callKitIntegration = self.callKitIntegration { - if self.didSetCurrentAudioOutputValue { - callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) - } - } else { - audioSessionControl.setOutputMode(.custom(self.currentAudioOutputValue)) - audioSessionControl.setup(synchronous: true) - } - - let audioSessionActive: Signal - if let callKitIntegration = self.callKitIntegration { - audioSessionActive = callKitIntegration.audioSessionActive - } else { - audioSessionControl.activate({ _ in }) - audioSessionActive = .single(true) - } - self.isAudioSessionActivePromise.set(audioSessionActive) - - self.initialSetupTimer?.invalidate() - self.initialSetupTimer = Foundation.Timer(timeInterval: 0.5, repeats: false, block: { [weak self] _ in - guard let self else { - return - } - - if self.defaultToSpeaker, let audioSessionControl = self.audioSessionControl { - self.currentAudioOutputValue = .speaker - self.didSetCurrentAudioOutputValue = true - - if let callKitIntegration = self.callKitIntegration { - if self.didSetCurrentAudioOutputValue { - callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) - } - } else { - audioSessionControl.setOutputMode(.custom(self.currentAudioOutputValue)) - audioSessionControl.setup(synchronous: true) - } - } - }) - } - } - }, deactivate: { [weak self] _ in - return Signal { subscriber in - Queue.mainQueue().async { - if let self { - self.isAudioSessionActivePromise.set(.single(false)) - self.audioSessionControl = nil - } - subscriber.putCompletion() - } - return EmptyDisposable - } - }, availableOutputsChanged: { [weak self] availableOutputs, currentOutput in - Queue.mainQueue().async { - guard let self else { - return - } - self.audioOutputStateValue = (availableOutputs, currentOutput) - if let currentOutput = currentOutput { - self.currentAudioOutputValue = currentOutput - self.didSetCurrentAudioOutputValue = true - } - - var signal: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> = .single((availableOutputs, currentOutput)) - if !didReceiveAudioOutputs { - didReceiveAudioOutputs = true - if currentOutput == .speaker { - signal = .single((availableOutputs, .builtin)) - |> then( - signal - |> delay(1.0, queue: Queue.mainQueue()) - ) - } - } - self.audioOutputStatePromise.set(signal) - } - }) - - self.audioSessionShouldBeActive.set(.single(true)) - self.audioSessionShouldBeActiveDisposable = (self.audioSessionShouldBeActive.get() - |> deliverOnMainQueue).start(next: { [weak self] value in - guard let self else { - return - } - if value { - if let audioSessionControl = self.audioSessionControl { - let audioSessionActive: Signal - if let callKitIntegration = self.callKitIntegration { - audioSessionActive = callKitIntegration.audioSessionActive - } else { - audioSessionControl.activate({ _ in }) - audioSessionActive = .single(true) - } - self.isAudioSessionActivePromise.set(audioSessionActive) - } else { - self.isAudioSessionActivePromise.set(.single(false)) - } - } else { - self.isAudioSessionActivePromise.set(.single(false)) - } - }) - - self.isAudioSessionActiveDisposable = (self.isAudioSessionActive - |> deliverOnMainQueue).start(next: { [weak self] value in - guard let self else { - return - } - self.audioDevice?.setIsAudioSessionActive(value) - }) - - self.audioOutputStateDisposable = (self.audioOutputStatePromise.get() - |> deliverOnMainQueue).start(next: { [weak self] value in - guard let self else { - return - } - self.audioOutputStateValue = value - if let currentOutput = value.1 { - self.currentAudioOutputValue = currentOutput - } - }) - } - - deinit { - self.audioSessionDisposable?.dispose() - self.audioSessionShouldBeActiveDisposable?.dispose() - self.isAudioSessionActiveDisposable?.dispose() - self.audioOutputStateDisposable?.dispose() - self.initialSetupTimer?.invalidate() - } - - func setCurrentAudioOutput(_ output: AudioSessionOutput) { - self.initialSetupTimer?.invalidate() - self.initialSetupTimer = nil - - guard self.currentAudioOutputValue != output else { - return - } - self.currentAudioOutputValue = output - self.didSetCurrentAudioOutputValue = true - - self.audioOutputStatePromise.set(.single((self.audioOutputStateValue.0, output)) - |> then( - .single(self.audioOutputStateValue) - |> delay(1.0, queue: Queue.mainQueue()) - )) - - if let audioSessionControl = self.audioSessionControl { - if let callKitIntegration = self.callKitIntegration { - callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) - } else { - audioSessionControl.setOutputMode(.custom(output)) - } - } - } - - public func switchToSpeakerIfBuiltin() { - if case .builtin = self.currentAudioOutputValue { - self.setCurrentAudioOutput(.speaker) - } - } -} - public final class PresentationCallImpl: PresentationCall { public let context: AccountContext private let audioSession: ManagedAudioSession @@ -556,7 +346,7 @@ public final class PresentationCallImpl: PresentationCall { if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_call_device"] { self.sharedAudioContext = nil } else { - self.sharedAudioContext = SharedCallAudioContext(audioSession: audioSession, callKitIntegration: callKitIntegration, defaultToSpeaker: startWithVideo || initialState?.type == .video) + self.sharedAudioContext = SharedCallAudioContext.get(audioSession: audioSession, callKitIntegration: callKitIntegration, defaultToSpeaker: startWithVideo || initialState?.type == .video) } if let _ = self.sharedAudioContext { diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 3b8178798e..b46482df4e 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -891,12 +891,18 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { var sharedAudioContext = sharedAudioContext if sharedAudioContext == nil { var useSharedAudio = !isStream - if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_group_shared_audio"] != nil { - useSharedAudio = false + var canReuseCurrent = true + if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data { + if data["ios_killswitch_group_shared_audio"] != nil { + useSharedAudio = false + } + if data["ios_killswitch_group_shared_audio_reuse"] != nil { + canReuseCurrent = false + } } if useSharedAudio { - let sharedAudioContextValue = SharedCallAudioContext(audioSession: audioSession, callKitIntegration: callKitIntegration, defaultToSpeaker: true) + let sharedAudioContextValue = SharedCallAudioContext.get(audioSession: audioSession, callKitIntegration: callKitIntegration, defaultToSpeaker: true, reuseCurrent: canReuseCurrent && callKitIntegration == nil) sharedAudioContext = sharedAudioContextValue } } diff --git a/submodules/TelegramCallsUI/Sources/SharedCallAudioContext.swift b/submodules/TelegramCallsUI/Sources/SharedCallAudioContext.swift new file mode 100644 index 0000000000..7bc847b190 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/SharedCallAudioContext.swift @@ -0,0 +1,231 @@ +import Foundation +import SwiftSignalKit +import TelegramVoip +import TelegramAudio + +public final class SharedCallAudioContext { + private static weak var current: SharedCallAudioContext? + + let audioDevice: OngoingCallContext.AudioDevice? + let callKitIntegration: CallKitIntegration? + + private let defaultToSpeaker: Bool + + private var audioSessionDisposable: Disposable? + private var audioSessionShouldBeActiveDisposable: Disposable? + private var isAudioSessionActiveDisposable: Disposable? + private var audioOutputStateDisposable: Disposable? + + private(set) var audioSessionControl: ManagedAudioSessionControl? + + private let isAudioSessionActivePromise = Promise(false) + private var isAudioSessionActive: Signal { + return self.isAudioSessionActivePromise.get() + } + + private let audioOutputStatePromise = Promise<([AudioSessionOutput], AudioSessionOutput?)>(([], nil)) + private var audioOutputStateValue: ([AudioSessionOutput], AudioSessionOutput?) = ([], nil) + public private(set) var currentAudioOutputValue: AudioSessionOutput = .builtin + private var didSetCurrentAudioOutputValue: Bool = false + var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { + return self.audioOutputStatePromise.get() + } + + private let audioSessionShouldBeActive = Promise(true) + private var initialSetupTimer: Foundation.Timer? + + static func get(audioSession: ManagedAudioSession, callKitIntegration: CallKitIntegration?, defaultToSpeaker: Bool = false, reuseCurrent: Bool = false) -> SharedCallAudioContext { + if let current = self.current, reuseCurrent { + return current + } + let context = SharedCallAudioContext(audioSession: audioSession, callKitIntegration: callKitIntegration, defaultToSpeaker: defaultToSpeaker) + self.current = context + return context + } + + private init(audioSession: ManagedAudioSession, callKitIntegration: CallKitIntegration?, defaultToSpeaker: Bool = false) { + self.callKitIntegration = callKitIntegration + self.audioDevice = OngoingCallContext.AudioDevice.create(enableSystemMute: false) + + var defaultToSpeaker = defaultToSpeaker + if audioSession.getIsHeadsetPluggedIn() { + defaultToSpeaker = false + } + + self.defaultToSpeaker = defaultToSpeaker + + if defaultToSpeaker { + self.didSetCurrentAudioOutputValue = true + self.currentAudioOutputValue = .speaker + } + + var didReceiveAudioOutputs = false + self.audioSessionDisposable = audioSession.push(audioSessionType: .voiceCall, manualActivate: { [weak self] control in + Queue.mainQueue().async { + guard let self else { + return + } + let previousControl = self.audioSessionControl + self.audioSessionControl = control + + if previousControl == nil, let audioSessionControl = self.audioSessionControl { + if let callKitIntegration = self.callKitIntegration { + if self.didSetCurrentAudioOutputValue { + callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) + } + } else { + audioSessionControl.setOutputMode(.custom(self.currentAudioOutputValue)) + audioSessionControl.setup(synchronous: true) + } + + let audioSessionActive: Signal + if let callKitIntegration = self.callKitIntegration { + audioSessionActive = callKitIntegration.audioSessionActive + } else { + audioSessionControl.activate({ _ in }) + audioSessionActive = .single(true) + } + self.isAudioSessionActivePromise.set(audioSessionActive) + + self.initialSetupTimer?.invalidate() + self.initialSetupTimer = Foundation.Timer(timeInterval: 0.5, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + + if self.defaultToSpeaker, let audioSessionControl = self.audioSessionControl { + self.currentAudioOutputValue = .speaker + self.didSetCurrentAudioOutputValue = true + + if let callKitIntegration = self.callKitIntegration { + if self.didSetCurrentAudioOutputValue { + callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) + } + } else { + audioSessionControl.setOutputMode(.custom(self.currentAudioOutputValue)) + audioSessionControl.setup(synchronous: true) + } + } + }) + } + } + }, deactivate: { [weak self] _ in + return Signal { subscriber in + Queue.mainQueue().async { + if let self { + self.isAudioSessionActivePromise.set(.single(false)) + self.audioSessionControl = nil + } + subscriber.putCompletion() + } + return EmptyDisposable + } + }, availableOutputsChanged: { [weak self] availableOutputs, currentOutput in + Queue.mainQueue().async { + guard let self else { + return + } + self.audioOutputStateValue = (availableOutputs, currentOutput) + if let currentOutput = currentOutput { + self.currentAudioOutputValue = currentOutput + self.didSetCurrentAudioOutputValue = true + } + + var signal: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> = .single((availableOutputs, currentOutput)) + if !didReceiveAudioOutputs { + didReceiveAudioOutputs = true + if currentOutput == .speaker { + signal = .single((availableOutputs, .builtin)) + |> then( + signal + |> delay(1.0, queue: Queue.mainQueue()) + ) + } + } + self.audioOutputStatePromise.set(signal) + } + }) + + self.audioSessionShouldBeActive.set(.single(true)) + self.audioSessionShouldBeActiveDisposable = (self.audioSessionShouldBeActive.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + if value { + if let audioSessionControl = self.audioSessionControl { + let audioSessionActive: Signal + if let callKitIntegration = self.callKitIntegration { + audioSessionActive = callKitIntegration.audioSessionActive + } else { + audioSessionControl.activate({ _ in }) + audioSessionActive = .single(true) + } + self.isAudioSessionActivePromise.set(audioSessionActive) + } else { + self.isAudioSessionActivePromise.set(.single(false)) + } + } else { + self.isAudioSessionActivePromise.set(.single(false)) + } + }) + + self.isAudioSessionActiveDisposable = (self.isAudioSessionActive + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + self.audioDevice?.setIsAudioSessionActive(value) + }) + + self.audioOutputStateDisposable = (self.audioOutputStatePromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + self.audioOutputStateValue = value + if let currentOutput = value.1 { + self.currentAudioOutputValue = currentOutput + } + }) + } + + deinit { + self.audioSessionDisposable?.dispose() + self.audioSessionShouldBeActiveDisposable?.dispose() + self.isAudioSessionActiveDisposable?.dispose() + self.audioOutputStateDisposable?.dispose() + self.initialSetupTimer?.invalidate() + } + + func setCurrentAudioOutput(_ output: AudioSessionOutput) { + self.initialSetupTimer?.invalidate() + self.initialSetupTimer = nil + + guard self.currentAudioOutputValue != output else { + return + } + self.currentAudioOutputValue = output + self.didSetCurrentAudioOutputValue = true + + self.audioOutputStatePromise.set(.single((self.audioOutputStateValue.0, output)) + |> then( + .single(self.audioOutputStateValue) + |> delay(1.0, queue: Queue.mainQueue()) + )) + + if let audioSessionControl = self.audioSessionControl { + if let callKitIntegration = self.callKitIntegration { + callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) + } else { + audioSessionControl.setOutputMode(.custom(output)) + } + } + } + + public func switchToSpeakerIfBuiltin() { + if case .builtin = self.currentAudioOutputValue { + self.setCurrentAudioOutput(.speaker) + } + } +} diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index 82301ad2df..b6d0f1c4d2 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -262,7 +262,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection case let .chats(chatsNode): count = chatsNode.currentState.selectedPeerIds.count } - if isCall && count == 0 { + if isCall && count <= 1 { self.titleView.title = CounterControllerTitle(title: self.params.title ?? self.presentationData.strings.Compose_NewGroupTitle, counter: nil) } else { var count = count diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 31b9322769..ef3a51e1e3 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -657,6 +657,17 @@ public: if (!WrappedInstance()->Playing()) { WrappedInstance()->InitPlayout(); + for (int i = 0; i < 3; i++) { + if (!WrappedInstance()->PlayoutIsInitialized()) { + sleep(1); + WrappedInstance()->InitPlayout(); + } else { + break; + } + } + if (!WrappedInstance()->PlayoutIsInitialized()) { + return; + } WrappedInstance()->StartPlayout(); WrappedInstance()->InitRecording(); WrappedInstance()->StartRecording();