Swiftgram/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift

1493 lines
64 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import TelegramCore
import SyncCore
import SwiftSignalKit
import Display
import AVFoundation
import TelegramVoip
import TelegramAudio
import TelegramUIPreferences
import TelegramPresentationData
import DeviceAccess
import UniversalMediaPlayer
import AccountContext
import DeviceProximity
public final class AccountGroupCallContextImpl: AccountGroupCallContext {
public final class Proxy {
public let context: AccountGroupCallContextImpl
let removed: () -> Void
public init(context: AccountGroupCallContextImpl, removed: @escaping () -> Void) {
self.context = context
self.removed = removed
}
deinit {
self.removed()
}
public func keep() {
}
}
var disposable: Disposable?
public var participantsContext: GroupCallParticipantsContext?
private let panelDataPromise = Promise<GroupCallPanelData>()
public var panelData: Signal<GroupCallPanelData, NoError> {
return self.panelDataPromise.get()
}
public init(account: Account, peerId: PeerId, call: CachedChannelData.ActiveCall) {
self.panelDataPromise.set(.single(GroupCallPanelData(
peerId: peerId,
info: GroupCallInfo(
id: call.id,
accessHash: call.accessHash,
participantCount: 0,
clientParams: nil
),
topParticipants: [],
participantCount: 0,
activeSpeakers: Set(),
groupCall: nil
)))
self.disposable = (getGroupCallParticipants(account: account, callId: call.id, accessHash: call.accessHash, offset: "", peerIds: [], limit: 100)
|> map(Optional.init)
|> `catch` { _ -> Signal<GroupCallParticipantsContext.State?, NoError> in
return .single(nil)
}
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self, let state = state else {
return
}
let context = GroupCallParticipantsContext(
account: account,
peerId: peerId,
id: call.id,
accessHash: call.accessHash,
state: state
)
strongSelf.participantsContext = context
strongSelf.panelDataPromise.set(combineLatest(queue: .mainQueue(),
context.state,
context.activeSpeakers
)
|> map { state, activeSpeakers -> GroupCallPanelData in
var topParticipants: [GroupCallParticipantsContext.Participant] = []
for participant in state.participants {
if topParticipants.count >= 3 {
break
}
topParticipants.append(participant)
}
return GroupCallPanelData(
peerId: peerId,
info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, clientParams: nil),
topParticipants: topParticipants,
participantCount: state.totalCount,
activeSpeakers: activeSpeakers,
groupCall: nil
)
})
})
}
deinit {
self.disposable?.dispose()
}
}
public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCache {
public class Impl {
private class Record {
let context: AccountGroupCallContextImpl
let subscribers = Bag<Void>()
var removeTimer: SwiftSignalKit.Timer?
init(context: AccountGroupCallContextImpl) {
self.context = context
}
}
private let queue: Queue
private var contexts: [Int64: Record] = [:]
init(queue: Queue) {
self.queue = queue
}
public func get(account: Account, peerId: PeerId, call: CachedChannelData.ActiveCall) -> AccountGroupCallContextImpl.Proxy {
let result: Record
if let current = self.contexts[call.id] {
result = current
} else {
let context = AccountGroupCallContextImpl(account: account, peerId: peerId, call: call)
result = Record(context: context)
self.contexts[call.id] = result
}
let index = result.subscribers.add(Void())
result.removeTimer?.invalidate()
result.removeTimer = nil
return AccountGroupCallContextImpl.Proxy(context: result.context, removed: { [weak self, weak result] in
Queue.mainQueue().async {
if let strongResult = result, let strongSelf = self, strongSelf.contexts[call.id] === strongResult {
strongResult.subscribers.remove(index)
if strongResult.subscribers.isEmpty {
let removeTimer = SwiftSignalKit.Timer(timeout: 30, repeat: false, completion: {
if let result = result, let strongSelf = self, strongSelf.contexts[call.id] === result, result.subscribers.isEmpty {
strongSelf.contexts.removeValue(forKey: call.id)
}
}, queue: .mainQueue())
strongResult.removeTimer = removeTimer
removeTimer.start()
}
}
}
})
}
}
let queue: Queue = .mainQueue()
public let impl: QueueLocalObject<Impl>
public init() {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue)
})
}
}
private extension PresentationGroupCallState {
static var initialValue: PresentationGroupCallState {
return PresentationGroupCallState(
networkState: .connecting,
canManageCall: false,
adminIds: Set(),
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
defaultParticipantMuteState: nil
)
}
}
public final class PresentationGroupCallImpl: PresentationGroupCall {
private enum InternalState {
case requesting
case active(GroupCallInfo)
case estabilished(info: GroupCallInfo, clientParams: String, localSsrc: UInt32, initialState: GroupCallParticipantsContext.State)
var callInfo: GroupCallInfo? {
switch self {
case .requesting:
return nil
case let .active(info):
return info
case let .estabilished(info, _, _, _):
return info
}
}
}
private struct SummaryInfoState: Equatable {
public var info: GroupCallInfo
public init(
info: GroupCallInfo
) {
self.info = info
}
}
private struct SummaryParticipantsState: Equatable {
public var participantCount: Int
public var topParticipants: [GroupCallParticipantsContext.Participant]
public var activeSpeakers: Set<PeerId>
public init(
participantCount: Int,
topParticipants: [GroupCallParticipantsContext.Participant],
activeSpeakers: Set<PeerId>
) {
self.participantCount = participantCount
self.topParticipants = topParticipants
self.activeSpeakers = activeSpeakers
}
}
private class SpeakingParticipantsContext {
private let speakingLevelThreshold: Float = 0.1
private let cutoffTimeout: Int32 = 3
private let silentTimeout: Int32 = 2
struct Participant {
let timestamp: Int32
let level: Float
}
private var participants: [PeerId: Participant] = [:]
private let speakingParticipantsPromise = ValuePromise<Set<PeerId>>()
private var speakingParticipants = Set<PeerId>() {
didSet {
self.speakingParticipantsPromise.set(self.speakingParticipants)
}
}
private let audioLevelsPromise = Promise<[(PeerId, Float, Bool)]>()
init() {
}
func update(levels: [(PeerId, Float, Bool)]) {
let timestamp = Int32(CFAbsoluteTimeGetCurrent())
let currentParticipants: [PeerId: Participant] = self.participants
var validSpeakers: [PeerId: Participant] = [:]
var silentParticipants = Set<PeerId>()
var speakingParticipants = Set<PeerId>()
for (peerId, level, hasVoice) in levels {
if level > speakingLevelThreshold && hasVoice {
validSpeakers[peerId] = Participant(timestamp: timestamp, level: level)
speakingParticipants.insert(peerId)
} else {
silentParticipants.insert(peerId)
}
}
for (peerId, participant) in currentParticipants {
if let _ = validSpeakers[peerId] {
} else {
let delta = timestamp - participant.timestamp
if silentParticipants.contains(peerId) {
if delta < silentTimeout {
validSpeakers[peerId] = participant
speakingParticipants.insert(peerId)
}
} else if delta < cutoffTimeout {
validSpeakers[peerId] = participant
speakingParticipants.insert(peerId)
}
}
}
var audioLevels: [(PeerId, Float, Bool)] = []
for (peerId, level, hasVoice) in levels {
if level > 0.001 {
audioLevels.append((peerId, level, hasVoice))
}
}
self.participants = validSpeakers
self.speakingParticipants = speakingParticipants
self.audioLevelsPromise.set(.single(audioLevels))
}
func get() -> Signal<Set<PeerId>, NoError> {
return self.speakingParticipantsPromise.get() |> distinctUntilChanged
}
func getAudioLevels() -> Signal<[(PeerId, Float, Bool)], NoError> {
return self.audioLevelsPromise.get()
}
}
public let account: Account
public let accountContext: AccountContext
private let audioSession: ManagedAudioSession
private let callKitIntegration: CallKitIntegration?
public var isIntegratedWithCallKit: Bool {
return self.callKitIntegration != nil
}
private let getDeviceAccessData: () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void)
private var initialCall: CachedChannelData.ActiveCall?
public let internalId: CallSessionInternalId
public let peerId: PeerId
public let peer: Peer?
private let temporaryJoinTimestamp: Int32
private var internalState: InternalState = .requesting
private var callContext: OngoingGroupCallContext?
private var ssrcMapping: [UInt32: PeerId] = [:]
private var summaryInfoState = Promise<SummaryInfoState?>(nil)
private var summaryParticipantsState = Promise<SummaryParticipantsState?>(nil)
private let summaryStatePromise = Promise<PresentationGroupCallSummaryState?>(nil)
public var summaryState: Signal<PresentationGroupCallSummaryState?, NoError> {
return self.summaryStatePromise.get()
}
private var summaryStateDisposable: Disposable?
private var isMutedValue: PresentationGroupCallMuteAction = .muted(isPushToTalkActive: false) {
didSet {
if self.isMutedValue != oldValue {
self.updateProximityMonitoring()
}
}
}
private let isMutedPromise = ValuePromise<PresentationGroupCallMuteAction>(.muted(isPushToTalkActive: false))
public var isMuted: Signal<Bool, NoError> {
return self.isMutedPromise.get()
|> map { value -> Bool in
switch value {
case let .muted(isPushToTalkActive):
return !isPushToTalkActive
case .unmuted:
return false
}
}
}
private let audioOutputStatePromise = Promise<([AudioSessionOutput], AudioSessionOutput?)>(([], nil))
private var audioOutputStateDisposable: Disposable?
private var actualAudioOutputState: ([AudioSessionOutput], AudioSessionOutput?)?
private var audioOutputStateValue: ([AudioSessionOutput], AudioSessionOutput?) = ([], nil)
private var currentSelectedAudioOutputValue: AudioSessionOutput = .builtin
public var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> {
return self.audioOutputStatePromise.get()
}
private var audioLevelsDisposable = MetaDisposable()
private let speakingParticipantsContext = SpeakingParticipantsContext()
private var speakingParticipantsReportTimestamp: [PeerId: Double] = [:]
public var audioLevels: Signal<[(PeerId, Float, Bool)], NoError> {
return self.speakingParticipantsContext.getAudioLevels()
}
private var participantsContextStateDisposable = MetaDisposable()
private var participantsContext: GroupCallParticipantsContext?
private let myAudioLevelPipe = ValuePipe<Float>()
public var myAudioLevel: Signal<Float, NoError> {
return self.myAudioLevelPipe.signal()
}
private var myAudioLevelDisposable = MetaDisposable()
private var audioSessionControl: ManagedAudioSessionControl?
private var audioSessionDisposable: Disposable?
private let audioSessionShouldBeActive = ValuePromise<Bool>(false, ignoreRepeated: true)
private var audioSessionShouldBeActiveDisposable: Disposable?
private let audioSessionActive = Promise<Bool>(false)
private var audioSessionActiveDisposable: Disposable?
private var isAudioSessionActive = false
private let typingDisposable = MetaDisposable()
private let _canBeRemoved = Promise<Bool>(false)
public var canBeRemoved: Signal<Bool, NoError> {
return self._canBeRemoved.get()
}
private let wasRemoved = Promise<Bool>(false)
private var stateValue = PresentationGroupCallState.initialValue {
didSet {
if self.stateValue != oldValue {
self.statePromise.set(self.stateValue)
}
}
}
private let statePromise = ValuePromise<PresentationGroupCallState>(PresentationGroupCallState.initialValue)
public var state: Signal<PresentationGroupCallState, NoError> {
return self.statePromise.get()
}
private var membersValue: PresentationGroupCallMembers? {
didSet {
if self.membersValue != oldValue {
self.membersPromise.set(self.membersValue)
}
}
}
private let membersPromise = ValuePromise<PresentationGroupCallMembers?>(nil)
public var members: Signal<PresentationGroupCallMembers?, NoError> {
return self.membersPromise.get()
}
private var invitedPeersValue: [PeerId] = [] {
didSet {
if self.invitedPeersValue != oldValue {
self.inivitedPeersPromise.set(self.invitedPeersValue)
}
}
}
private let inivitedPeersPromise = ValuePromise<[PeerId]>([])
public var invitedPeers: Signal<[PeerId], NoError> {
return self.inivitedPeersPromise.get()
}
private let memberEventsPipe = ValuePipe<PresentationGroupCallMemberEvent>()
public var memberEvents: Signal<PresentationGroupCallMemberEvent, NoError> {
return self.memberEventsPipe.signal()
}
private let memberEventsPipeDisposable = MetaDisposable()
private let requestDisposable = MetaDisposable()
private var groupCallParticipantUpdatesDisposable: Disposable?
private let networkStateDisposable = MetaDisposable()
private let isMutedDisposable = MetaDisposable()
private let memberStatesDisposable = MetaDisposable()
private let leaveDisposable = MetaDisposable()
private var checkCallDisposable: Disposable?
private var isCurrentlyConnecting: Bool?
private var myAudioLevelTimer: SwiftSignalKit.Timer?
private var proximityManagerIndex: Int?
private var removedChannelMembersDisposable: Disposable?
private var didStartConnectingOnce: Bool = false
private var didConnectOnce: Bool = false
private var toneRenderer: PresentationCallToneRenderer?
init(
accountContext: AccountContext,
audioSession: ManagedAudioSession,
callKitIntegration: CallKitIntegration?,
getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void),
initialCall: CachedChannelData.ActiveCall?,
internalId: CallSessionInternalId,
peerId: PeerId,
peer: Peer?
) {
self.account = accountContext.account
self.accountContext = accountContext
self.audioSession = audioSession
self.callKitIntegration = callKitIntegration
self.getDeviceAccessData = getDeviceAccessData
self.initialCall = initialCall
self.internalId = internalId
self.peerId = peerId
self.peer = peer
self.temporaryJoinTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var didReceiveAudioOutputs = false
if !audioSession.getIsHeadsetPluggedIn() {
self.currentSelectedAudioOutputValue = .speaker
self.audioOutputStatePromise.set(.single(([], .speaker)))
}
self.audioSessionDisposable = audioSession.push(audioSessionType: .voiceCall, manualActivate: { [weak self] control in
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.updateSessionState(internalState: strongSelf.internalState, audioSessionControl: control)
}
}
}, deactivate: { [weak self] in
return Signal { subscriber in
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.updateIsAudioSessionActive(false)
strongSelf.updateSessionState(internalState: strongSelf.internalState, audioSessionControl: nil)
}
subscriber.putCompletion()
}
return EmptyDisposable
}
}, availableOutputsChanged: { [weak self] availableOutputs, currentOutput in
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.audioOutputStateValue = (availableOutputs, currentOutput)
var signal: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> = .single((availableOutputs, currentOutput))
if !didReceiveAudioOutputs {
didReceiveAudioOutputs = true
if currentOutput == .speaker {
signal = .single((availableOutputs, .speaker))
|> then(
signal
|> delay(1.0, queue: Queue.mainQueue())
)
}
}
strongSelf.audioOutputStatePromise.set(signal)
}
})
self.audioSessionShouldBeActiveDisposable = (self.audioSessionShouldBeActive.get()
|> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self {
if value {
if let audioSessionControl = strongSelf.audioSessionControl {
let audioSessionActive: Signal<Bool, NoError>
if let callKitIntegration = strongSelf.callKitIntegration {
audioSessionActive = callKitIntegration.audioSessionActive
|> filter { $0 }
|> timeout(2.0, queue: Queue.mainQueue(), alternate: Signal { subscriber in
if let strongSelf = self, let _ = strongSelf.audioSessionControl {
}
subscriber.putNext(true)
subscriber.putCompletion()
return EmptyDisposable
})
} else {
audioSessionControl.activate({ [weak self] _ in
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.audioSessionActive.set(.single(true))
}
})
}
} else {
strongSelf.audioSessionActive.set(.single(false))
}
} else {
strongSelf.audioSessionActive.set(.single(false))
}
}
})
self.audioSessionActiveDisposable = (self.audioSessionActive.get()
|> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self {
strongSelf.updateIsAudioSessionActive(value)
}
})
self.audioOutputStateDisposable = (self.audioOutputStatePromise.get()
|> deliverOnMainQueue).start(next: { [weak self] availableOutputs, currentOutput in
guard let strongSelf = self else {
return
}
strongSelf.updateAudioOutputs(availableOutputs: availableOutputs, currentOutput: currentOutput)
})
self.groupCallParticipantUpdatesDisposable = (self.account.stateManager.groupCallParticipantUpdates
|> deliverOnMainQueue).start(next: { [weak self] updates in
guard let strongSelf = self else {
return
}
if case let .estabilished(callInfo, _, _, _) = strongSelf.internalState {
var removedSsrc: [UInt32] = []
for (callId, update) in updates {
if callId == callInfo.id {
switch update {
case let .state(update):
for participantUpdate in update.participantUpdates {
if case .left = participantUpdate.participationStatusChange {
removedSsrc.append(participantUpdate.ssrc)
if participantUpdate.peerId == strongSelf.accountContext.account.peerId {
if case let .estabilished(_, _, ssrc, _) = strongSelf.internalState, ssrc == participantUpdate.ssrc {
strongSelf._canBeRemoved.set(.single(true))
}
}
} else if participantUpdate.peerId == strongSelf.accountContext.account.peerId {
if case let .estabilished(_, _, ssrc, _) = strongSelf.internalState, ssrc != participantUpdate.ssrc {
strongSelf._canBeRemoved.set(.single(true))
}
} else if case .joined = participantUpdate.participationStatusChange {
}
}
case let .call(isTerminated, _):
if isTerminated {
strongSelf._canBeRemoved.set(.single(true))
}
}
}
}
if !removedSsrc.isEmpty {
strongSelf.callContext?.removeSsrcs(ssrcs: removedSsrc)
}
}
})
self.summaryStatePromise.set(combineLatest(queue: .mainQueue(),
self.summaryInfoState.get(),
self.summaryParticipantsState.get(),
self.statePromise.get()
)
|> map { infoState, participantsState, callState -> PresentationGroupCallSummaryState? in
guard let participantsState = participantsState else {
return nil
}
return PresentationGroupCallSummaryState(
info: infoState?.info,
participantCount: participantsState.participantCount,
callState: callState,
topParticipants: participantsState.topParticipants,
activeSpeakers: participantsState.activeSpeakers
)
})
if let initialCall = initialCall, let temporaryParticipantsContext = (self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl)?.impl.syncWith({ impl in
impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash))
}) {
if let participantsContext = temporaryParticipantsContext.context.participantsContext {
let accountPeerId = self.accountContext.account.peerId
let accountPeer = self.accountContext.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(accountPeerId)
}
self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(),
accountPeer,
participantsContext.state,
participantsContext.activeSpeakers
).start(next: { [weak self] accountPeer, state, activeSpeakers in
guard let strongSelf = self else {
return
}
var topParticipants: [GroupCallParticipantsContext.Participant] = []
var members = PresentationGroupCallMembers(
participants: [],
speakingParticipants: [],
totalCount: 0,
loadMoreToken: nil
)
var updatedInvitedPeers = strongSelf.invitedPeersValue
var didUpdateInvitedPeers = false
var participants = state.participants
if !participants.contains(where: { $0.peer.id == accountPeerId }) {
if let accountPeer = accountPeer {
participants.append(GroupCallParticipantsContext.Participant(
peer: accountPeer,
ssrc: 0,
joinTimestamp: strongSelf.temporaryJoinTimestamp,
activityTimestamp: nil,
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
volume: nil
))
participants.sort()
}
}
for participant in participants {
members.participants.append(participant)
if topParticipants.count < 3 {
topParticipants.append(participant)
}
if let index = updatedInvitedPeers.firstIndex(of: participant.peer.id) {
updatedInvitedPeers.remove(at: index)
didUpdateInvitedPeers = true
}
}
members.totalCount = state.totalCount
members.loadMoreToken = state.nextParticipantsFetchOffset
strongSelf.membersValue = members
strongSelf.stateValue.adminIds = state.adminIds
strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState(
participantCount: state.totalCount,
topParticipants: topParticipants,
activeSpeakers: activeSpeakers
)))
if didUpdateInvitedPeers {
strongSelf.invitedPeersValue = updatedInvitedPeers
}
}))
}
}
self.removedChannelMembersDisposable = (accountContext.peerChannelMemberCategoriesContextsManager.removedChannelMembers
|> deliverOnMainQueue).start(next: { [weak self] pairs in
guard let strongSelf = self else {
return
}
for (channelId, memberId) in pairs {
if channelId == strongSelf.peerId {
strongSelf.removedPeer(memberId)
}
}
})
self.requestCall()
}
deinit {
self.audioSessionShouldBeActiveDisposable?.dispose()
self.audioSessionActiveDisposable?.dispose()
self.summaryStateDisposable?.dispose()
self.audioSessionDisposable?.dispose()
self.requestDisposable.dispose()
self.groupCallParticipantUpdatesDisposable?.dispose()
self.leaveDisposable.dispose()
self.isMutedDisposable.dispose()
self.memberStatesDisposable.dispose()
self.networkStateDisposable.dispose()
self.checkCallDisposable?.dispose()
self.audioLevelsDisposable.dispose()
self.participantsContextStateDisposable.dispose()
self.myAudioLevelDisposable.dispose()
self.memberEventsPipeDisposable.dispose()
self.myAudioLevelTimer?.invalidate()
self.typingDisposable.dispose()
if let proximityManagerIndex = self.proximityManagerIndex {
DeviceProximityManager.shared().remove(proximityManagerIndex)
}
self.audioOutputStateDisposable?.dispose()
self.removedChannelMembersDisposable?.dispose()
}
private func updateSessionState(internalState: InternalState, audioSessionControl: ManagedAudioSessionControl?) {
let previousControl = self.audioSessionControl
self.audioSessionControl = audioSessionControl
let previousInternalState = self.internalState
self.internalState = internalState
if let audioSessionControl = audioSessionControl, previousControl == nil {
switch self.currentSelectedAudioOutputValue {
case .speaker:
audioSessionControl.setOutputMode(.custom(self.currentSelectedAudioOutputValue))
default:
break
}
audioSessionControl.setup(synchronous: true)
}
self.audioSessionShouldBeActive.set(true)
switch previousInternalState {
case .requesting:
break
default:
if case .requesting = internalState {
self.isCurrentlyConnecting = nil
}
}
switch previousInternalState {
case .active:
break
default:
if case let .active(callInfo) = internalState {
let callContext = OngoingGroupCallContext()
self.callContext = callContext
self.requestDisposable.set((callContext.joinPayload
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] joinPayload, ssrc in
guard let strongSelf = self else {
return
}
strongSelf.requestDisposable.set((joinGroupCall(
account: strongSelf.account,
peerId: strongSelf.peerId,
callId: callInfo.id,
accessHash: callInfo.accessHash,
preferMuted: true,
joinPayload: joinPayload
)
|> deliverOnMainQueue).start(next: { joinCallResult in
guard let strongSelf = self else {
return
}
if let clientParams = joinCallResult.callInfo.clientParams {
strongSelf.updateSessionState(internalState: .estabilished(info: joinCallResult.callInfo, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state), audioSessionControl: strongSelf.audioSessionControl)
}
}, error: { error in
guard let strongSelf = self else {
return
}
if case .anonymousNotAllowed = error {
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
]), on: .root, blockInteraction: false, completion: {})
} else if case .tooManyParticipants = error {
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_ChatFullAlertText, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
]), on: .root, blockInteraction: false, completion: {})
}
strongSelf._canBeRemoved.set(.single(true))
}))
}))
self.networkStateDisposable.set((callContext.networkState
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
let mappedState: PresentationGroupCallState.NetworkState
switch state {
case .connecting:
mappedState = .connecting
case .connected:
mappedState = .connected
}
let wasConnecting = strongSelf.stateValue.networkState == .connecting
if strongSelf.stateValue.networkState != mappedState {
strongSelf.stateValue.networkState = mappedState
}
let isConnecting = mappedState == .connecting
if strongSelf.isCurrentlyConnecting != isConnecting {
strongSelf.isCurrentlyConnecting = isConnecting
if isConnecting {
strongSelf.startCheckingCallIfNeeded()
} else {
strongSelf.checkCallDisposable?.dispose()
strongSelf.checkCallDisposable = nil
}
}
if (wasConnecting != isConnecting && strongSelf.didConnectOnce) { //|| !strongSelf.didStartConnectingOnce {
if isConnecting {
let toneRenderer = PresentationCallToneRenderer(tone: .groupConnecting)
strongSelf.toneRenderer = toneRenderer
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
} else {
strongSelf.toneRenderer = nil
}
}
if isConnecting {
strongSelf.didStartConnectingOnce = true
}
if case .connected = state {
if !strongSelf.didConnectOnce {
strongSelf.didConnectOnce = true
let toneRenderer = PresentationCallToneRenderer(tone: .groupJoined)
strongSelf.toneRenderer = toneRenderer
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
}
}
}))
self.audioLevelsDisposable.set((callContext.audioLevels
|> deliverOnMainQueue).start(next: { [weak self] levels in
guard let strongSelf = self else {
return
}
var result: [(PeerId, Float, Bool)] = []
var myLevel: Float = 0.0
var myLevelHasVoice: Bool = false
for (ssrcKey, level, hasVoice) in levels {
var peerId: PeerId?
switch ssrcKey {
case .local:
peerId = strongSelf.accountContext.account.peerId
case let .source(ssrc):
peerId = strongSelf.ssrcMapping[ssrc]
}
if let peerId = peerId {
if case .local = ssrcKey {
if !strongSelf.isMutedValue.isEffectivelyMuted {
myLevel = level
myLevelHasVoice = hasVoice
}
}
result.append((peerId, level, hasVoice))
}
}
strongSelf.speakingParticipantsContext.update(levels: result)
let mappedLevel = myLevel * 1.5
strongSelf.myAudioLevelPipe.putNext(mappedLevel)
strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice)
}))
}
}
switch previousInternalState {
case .estabilished:
break
default:
if case let .estabilished(callInfo, clientParams, _, initialState) = internalState {
self.summaryInfoState.set(.single(SummaryInfoState(info: callInfo)))
self.stateValue.canManageCall = initialState.isCreator || initialState.adminIds.contains(self.accountContext.account.peerId)
if self.stateValue.canManageCall && initialState.defaultParticipantsAreMuted.canChange {
self.stateValue.defaultParticipantMuteState = initialState.defaultParticipantsAreMuted.isMuted ? .muted : .unmuted
}
self.ssrcMapping.removeAll()
var ssrcs: [UInt32] = []
for participant in initialState.participants {
self.ssrcMapping[participant.ssrc] = participant.peer.id
ssrcs.append(participant.ssrc)
}
self.callContext?.setJoinResponse(payload: clientParams, ssrcs: ssrcs)
let accountContext = self.accountContext
let peerId = self.peerId
let rawAdminIds = Signal<Set<PeerId>, NoError> { subscriber in
let (disposable, _) = accountContext.peerChannelMemberCategoriesContextsManager.admins(postbox: accountContext.account.postbox, network: accountContext.account.network, accountPeerId: accountContext.account.peerId, peerId: peerId, updated: { list in
subscriber.putNext(Set(list.list.map { $0.peer.id }))
})
return disposable
}
|> runOn(.mainQueue())
let adminIds = combineLatest(queue: .mainQueue(),
rawAdminIds,
accountContext.account.postbox.combinedView(keys: [.basicPeer(peerId)])
)
|> map { rawAdminIds, view -> Set<PeerId> in
var rawAdminIds = rawAdminIds
if let peerView = view.views[.basicPeer(peerId)] as? BasicPeerView, let peer = peerView.peer as? TelegramChannel {
if peer.hasPermission(.manageCalls) {
rawAdminIds.insert(accountContext.account.peerId)
} else {
rawAdminIds.remove(accountContext.account.peerId)
}
}
return rawAdminIds
}
|> distinctUntilChanged
let participantsContext = GroupCallParticipantsContext(
account: self.accountContext.account,
peerId: self.peerId,
id: callInfo.id,
accessHash: callInfo.accessHash,
state: initialState
)
self.participantsContext = participantsContext
self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(),
participantsContext.state,
participantsContext.activeSpeakers,
self.speakingParticipantsContext.get(),
adminIds
).start(next: { [weak self] state, activeSpeakers, speakingParticipants, adminIds in
guard let strongSelf = self else {
return
}
var topParticipants: [GroupCallParticipantsContext.Participant] = []
var reportSpeakingParticipants: [PeerId] = []
let timestamp = CACurrentMediaTime()
for peerId in speakingParticipants {
let shouldReport: Bool
if let previousTimestamp = strongSelf.speakingParticipantsReportTimestamp[peerId] {
shouldReport = previousTimestamp + 1.0 < timestamp
} else {
shouldReport = true
}
if shouldReport {
strongSelf.speakingParticipantsReportTimestamp[peerId] = timestamp
reportSpeakingParticipants.append(peerId)
}
}
if !reportSpeakingParticipants.isEmpty {
Queue.mainQueue().justDispatch {
self?.participantsContext?.reportSpeakingParticipants(ids: reportSpeakingParticipants)
}
}
var members = PresentationGroupCallMembers(
participants: [],
speakingParticipants: speakingParticipants,
totalCount: 0,
loadMoreToken: nil
)
var updatedInvitedPeers = strongSelf.invitedPeersValue
var didUpdateInvitedPeers = false
for participant in state.participants {
members.participants.append(participant)
if topParticipants.count < 3 {
topParticipants.append(participant)
}
strongSelf.ssrcMapping[participant.ssrc] = participant.peer.id
if participant.peer.id == strongSelf.accountContext.account.peerId {
if let muteState = participant.muteState {
if muteState.canUnmute {
switch strongSelf.isMutedValue {
case let .muted(isPushToTalkActive):
if !isPushToTalkActive {
strongSelf.callContext?.setIsMuted(true)
}
case .unmuted:
strongSelf.isMutedValue = .muted(isPushToTalkActive: false)
strongSelf.callContext?.setIsMuted(true)
}
} else {
strongSelf.isMutedValue = .muted(isPushToTalkActive: false)
strongSelf.callContext?.setIsMuted(true)
}
strongSelf.stateValue.muteState = muteState
} else if let currentMuteState = strongSelf.stateValue.muteState, !currentMuteState.canUnmute {
strongSelf.isMutedValue = .muted(isPushToTalkActive: false)
strongSelf.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false)
strongSelf.callContext?.setIsMuted(true)
}
} else {
if let volume = participant.volume {
strongSelf.callContext?.setVolume(ssrc: participant.ssrc, volume: Double(volume) / 10000.0)
} else if participant.muteState?.mutedByYou == true {
strongSelf.callContext?.setVolume(ssrc: participant.ssrc, volume: 0.0)
}
}
if let index = updatedInvitedPeers.firstIndex(of: participant.peer.id) {
updatedInvitedPeers.remove(at: index)
didUpdateInvitedPeers = true
}
}
members.totalCount = state.totalCount
members.loadMoreToken = state.nextParticipantsFetchOffset
strongSelf.membersValue = members
strongSelf.stateValue.adminIds = adminIds
strongSelf.stateValue.canManageCall = state.isCreator || adminIds.contains(strongSelf.accountContext.account.peerId)
if (state.isCreator || strongSelf.stateValue.adminIds.contains(strongSelf.accountContext.account.peerId)) && state.defaultParticipantsAreMuted.canChange {
strongSelf.stateValue.defaultParticipantMuteState = state.defaultParticipantsAreMuted.isMuted ? .muted : .unmuted
}
strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState(
participantCount: state.totalCount,
topParticipants: topParticipants,
activeSpeakers: activeSpeakers
)))
if didUpdateInvitedPeers {
strongSelf.invitedPeersValue = updatedInvitedPeers
}
}))
let postbox = self.accountContext.account.postbox
self.memberEventsPipeDisposable.set((participantsContext.memberEvents
|> mapToSignal { event -> Signal<PresentationGroupCallMemberEvent, NoError> in
return postbox.transaction { transaction -> Signal<PresentationGroupCallMemberEvent, NoError> in
if let peer = transaction.getPeer(event.peerId) {
return .single(PresentationGroupCallMemberEvent(peer: peer, joined: event.joined))
} else {
return .complete()
}
}
|> switchToLatest
}
|> deliverOnMainQueue).start(next: { [weak self] event in
self?.memberEventsPipe.putNext(event)
}))
if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting {
self.startCheckingCallIfNeeded()
}
}
}
}
private func startCheckingCallIfNeeded() {
if self.checkCallDisposable != nil {
return
}
if case let .estabilished(callInfo, _, ssrc, _) = self.internalState {
let checkSignal = checkGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, ssrc: Int32(bitPattern: ssrc))
self.checkCallDisposable = ((
checkSignal
|> castError(Bool.self)
|> delay(4.0, queue: .mainQueue())
|> mapToSignal { result -> Signal<Bool, Bool> in
if case .success = result {
return .fail(true)
} else {
return .single(true)
}
}
)
|> restartIfError
|> take(1)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.checkCallDisposable = nil
strongSelf.requestCall()
})
}
}
private func updateIsAudioSessionActive(_ value: Bool) {
if self.isAudioSessionActive != value {
self.isAudioSessionActive = value
self.toneRenderer?.setAudioSessionActive(value)
}
}
private func markAsCanBeRemoved() {
self.callContext?.stop()
self._canBeRemoved.set(.single(true))
if self.didConnectOnce {
if let callManager = self.accountContext.sharedContext.callManager {
let _ = (callManager.currentGroupCallSignal
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] call in
guard let strongSelf = self else {
return
}
if let call = call, call !== strongSelf {
strongSelf.wasRemoved.set(.single(true))
return
}
let toneRenderer = PresentationCallToneRenderer(tone: .groupLeft)
strongSelf.toneRenderer = toneRenderer
toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive)
Queue.mainQueue().after(1.0, {
strongSelf.wasRemoved.set(.single(true))
})
})
}
}
}
public func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError> {
if case let .estabilished(callInfo, _, localSsrc, _) = self.internalState {
if terminateIfPossible {
self.leaveDisposable.set((stopGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.markAsCanBeRemoved()
}))
} else {
self.leaveDisposable.set((leaveGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, source: localSsrc)
|> deliverOnMainQueue).start(error: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.markAsCanBeRemoved()
}, completed: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.markAsCanBeRemoved()
}))
}
} else {
self.markAsCanBeRemoved()
}
return self._canBeRemoved.get()
}
public func toggleIsMuted() {
switch self.isMutedValue {
case .muted:
self.setIsMuted(action: .unmuted)
case .unmuted:
self.setIsMuted(action: .muted(isPushToTalkActive: false))
}
}
public func setIsMuted(action: PresentationGroupCallMuteAction) {
if self.isMutedValue == action {
return
}
if let muteState = self.stateValue.muteState, !muteState.canUnmute {
return
}
self.isMutedValue = action
self.isMutedPromise.set(self.isMutedValue)
let isEffectivelyMuted: Bool
let isVisuallyMuted: Bool
switch self.isMutedValue {
case let .muted(isPushToTalkActive):
isEffectivelyMuted = !isPushToTalkActive
isVisuallyMuted = true
let _ = self.updateMuteState(peerId: self.accountContext.account.peerId, isMuted: true)
case .unmuted:
isEffectivelyMuted = false
isVisuallyMuted = false
let _ = self.updateMuteState(peerId: self.accountContext.account.peerId, isMuted: false)
}
self.callContext?.setIsMuted(isEffectivelyMuted)
if isVisuallyMuted {
self.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false)
} else {
self.stateValue.muteState = nil
}
}
public func setVolume(peerId: PeerId, volume: Int32, sync: Bool) {
for (ssrc, id) in self.ssrcMapping {
if id == peerId {
self.callContext?.setVolume(ssrc: ssrc, volume: Double(volume) / 10000.0)
if sync {
self.participantsContext?.updateMuteState(peerId: peerId, muteState: nil, volume: volume)
}
break
}
}
}
public func setCurrentAudioOutput(_ output: AudioSessionOutput) {
guard self.currentSelectedAudioOutputValue != output else {
return
}
self.currentSelectedAudioOutputValue = output
self.updateProximityMonitoring()
self.audioOutputStatePromise.set(.single((self.audioOutputStateValue.0, output))
|> then(
.single(self.audioOutputStateValue)
|> delay(1.0, queue: Queue.mainQueue())
))
if let audioSessionControl = self.audioSessionControl {
audioSessionControl.setOutputMode(.custom(output))
}
}
private func updateProximityMonitoring() {
var shouldMonitorProximity = false
switch self.currentSelectedAudioOutputValue {
case .builtin:
shouldMonitorProximity = true
default:
break
}
if case .muted(isPushToTalkActive: true) = self.isMutedValue {
shouldMonitorProximity = false
}
if shouldMonitorProximity {
if self.proximityManagerIndex == nil {
self.proximityManagerIndex = DeviceProximityManager.shared().add { _ in
}
}
} else {
if let proximityManagerIndex = self.proximityManagerIndex {
self.proximityManagerIndex = nil
DeviceProximityManager.shared().remove(proximityManagerIndex)
}
}
}
private func updateAudioOutputs(availableOutputs: [AudioSessionOutput], currentOutput: AudioSessionOutput?) {
if self.actualAudioOutputState?.0 != availableOutputs || self.actualAudioOutputState?.1 != currentOutput {
self.actualAudioOutputState = (availableOutputs, currentOutput)
self.setupAudioOutputs()
}
}
private func setupAudioOutputs() {
if let actualAudioOutputState = self.actualAudioOutputState, let currentOutput = actualAudioOutputState.1 {
self.currentSelectedAudioOutputValue = currentOutput
switch currentOutput {
case .headphones, .speaker:
break
case let .port(port) where port.type == .bluetooth:
break
default:
//self.setCurrentAudioOutput(.speaker)
break
}
}
}
public func updateMuteState(peerId: PeerId, isMuted: Bool) -> GroupCallParticipantsContext.Participant.MuteState? {
let canThenUnmute: Bool
if isMuted {
var mutedByYou = false
if peerId == self.accountContext.account.peerId {
canThenUnmute = true
} else if self.stateValue.canManageCall {
if self.stateValue.adminIds.contains(peerId) {
canThenUnmute = true
} else {
canThenUnmute = false
}
} else if self.stateValue.adminIds.contains(self.accountContext.account.peerId) {
canThenUnmute = true
} else {
self.setVolume(peerId: peerId, volume: 0, sync: false)
mutedByYou = true
canThenUnmute = true
}
let muteState = isMuted ? GroupCallParticipantsContext.Participant.MuteState(canUnmute: canThenUnmute, mutedByYou: mutedByYou) : nil
self.participantsContext?.updateMuteState(peerId: peerId, muteState: muteState, volume: nil)
return muteState
} else {
if peerId == self.accountContext.account.peerId {
self.participantsContext?.updateMuteState(peerId: peerId, muteState: nil, volume: nil)
return nil
} else if self.stateValue.canManageCall || self.stateValue.adminIds.contains(self.accountContext.account.peerId) {
let muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false)
self.participantsContext?.updateMuteState(peerId: peerId, muteState: muteState, volume: nil)
return muteState
} else {
self.setVolume(peerId: peerId, volume: 10000, sync: true)
self.participantsContext?.updateMuteState(peerId: peerId, muteState: nil, volume: nil)
return nil
}
}
}
private func requestCall() {
self.callContext?.stop()
self.callContext = nil
self.internalState = .requesting
self.isCurrentlyConnecting = nil
enum CallError {
case generic
}
let account = self.account
let currentCall: Signal<GroupCallInfo?, CallError>
if let initialCall = self.initialCall {
currentCall = getCurrentGroupCall(account: account, callId: initialCall.id, accessHash: initialCall.accessHash)
|> mapError { _ -> CallError in
return .generic
}
|> map { summary -> GroupCallInfo? in
return summary?.info
}
} else {
currentCall = .single(nil)
}
let currentOrRequestedCall = currentCall
|> mapToSignal { callInfo -> Signal<GroupCallInfo?, CallError> in
if let callInfo = callInfo {
return .single(callInfo)
} else {
return .single(nil)
}
}
self.requestDisposable.set((currentOrRequestedCall
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
if let value = value {
strongSelf.initialCall = CachedChannelData.ActiveCall(id: value.id, accessHash: value.accessHash)
strongSelf.updateSessionState(internalState: .active(value), audioSessionControl: strongSelf.audioSessionControl)
} else {
strongSelf._canBeRemoved.set(.single(true))
}
}))
}
public func invitePeer(_ peerId: PeerId) -> Bool {
guard case let .estabilished(callInfo, _, _, _) = self.internalState, !self.invitedPeersValue.contains(peerId) else {
return false
}
var updatedInvitedPeers = self.invitedPeersValue
updatedInvitedPeers.insert(peerId, at: 0)
self.invitedPeersValue = updatedInvitedPeers
let _ = inviteToGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId).start()
return true
}
public func removedPeer(_ peerId: PeerId) {
var updatedInvitedPeers = self.invitedPeersValue
updatedInvitedPeers.removeAll(where: { $0 == peerId})
self.invitedPeersValue = updatedInvitedPeers
}
private var currentMyAudioLevel: Float = 0.0
private var currentMyAudioLevelTimestamp: Double = 0.0
private var isSendingTyping: Bool = false
private func restartMyAudioLevelTimer() {
self.myAudioLevelTimer?.invalidate()
let myAudioLevelTimer = SwiftSignalKit.Timer(timeout: 0.1, repeat: false, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.myAudioLevelTimer = nil
let timestamp = CACurrentMediaTime()
var shouldBeSendingTyping = false
if strongSelf.currentMyAudioLevel > 0.01 && timestamp < strongSelf.currentMyAudioLevelTimestamp + 1.0 {
strongSelf.restartMyAudioLevelTimer()
shouldBeSendingTyping = true
} else {
if timestamp < strongSelf.currentMyAudioLevelTimestamp + 1.0 {
strongSelf.restartMyAudioLevelTimer()
shouldBeSendingTyping = true
}
}
if shouldBeSendingTyping != strongSelf.isSendingTyping {
strongSelf.isSendingTyping = shouldBeSendingTyping
if shouldBeSendingTyping {
strongSelf.typingDisposable.set(strongSelf.accountContext.account.acquireLocalInputActivity(peerId: PeerActivitySpace(peerId: strongSelf.peerId, category: .voiceChat), activity: .speakingInGroupCall(timestamp: 0)))
strongSelf.restartMyAudioLevelTimer()
} else {
strongSelf.typingDisposable.set(nil)
}
}
}, queue: .mainQueue())
self.myAudioLevelTimer = myAudioLevelTimer
myAudioLevelTimer.start()
}
private func processMyAudioLevel(level: Float, hasVoice: Bool) {
self.currentMyAudioLevel = level
if level > 0.01 && hasVoice {
self.currentMyAudioLevelTimestamp = CACurrentMediaTime()
if self.myAudioLevelTimer == nil {
self.restartMyAudioLevelTimer()
}
}
}
public func updateDefaultParticipantsAreMuted(isMuted: Bool) {
self.participantsContext?.updateDefaultParticipantsAreMuted(isMuted: isMuted)
}
public func loadMoreMembers(token: String) {
self.participantsContext?.loadMore(token: token)
}
}