diff --git a/Telegram/Telegram-iOS/Resources/voip_group_recording_started.mp3 b/Telegram/Telegram-iOS/Resources/voip_group_recording_started.mp3 new file mode 100644 index 0000000000..0060e82c37 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/voip_group_recording_started.mp3 differ diff --git a/Telegram/Telegram-iOS/Resources/voip_group_unmuted.mp3 b/Telegram/Telegram-iOS/Resources/voip_group_unmuted.mp3 new file mode 100644 index 0000000000..b5dfaa606c Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/voip_group_unmuted.mp3 differ diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index a9d3693875..44602ffedb 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -286,6 +286,11 @@ public final class PresentationGroupCallMemberEvent { } } +public enum PresentationGroupCallTone { + case unmuted + case recordingStarted +} + public protocol PresentationGroupCall: class { var account: Account { get } var accountContext: AccountContext { get } @@ -321,6 +326,8 @@ public protocol PresentationGroupCall: class { func setVolume(peerId: PeerId, volume: Int32, sync: Bool) func setFullSizeVideo(peerId: PeerId?) func setCurrentAudioOutput(_ output: AudioSessionOutput) + + func playTone(_ tone: PresentationGroupCallTone) func updateMuteState(peerId: PeerId, isMuted: Bool) -> GroupCallParticipantsContext.Participant.MuteState? func setShouldBeRecording(_ shouldBeRecording: Bool, title: String?) diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index 4b9831dcd3..cc431709c8 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -413,7 +413,7 @@ public final class ManagedAudioSession { }, deactivate: deactivate) } - public func push(audioSessionType: ManagedAudioSessionType, outputMode: AudioSessionOutputMode = .system, once: Bool = false, manualActivate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping () -> Signal, headsetConnectionStatusChanged: @escaping (Bool) -> Void = { _ in }, availableOutputsChanged: @escaping ([AudioSessionOutput], AudioSessionOutput?) -> Void = { _, _ in }) -> Disposable { + public func push(audioSessionType: ManagedAudioSessionType, outputMode: AudioSessionOutputMode = .system, once: Bool = false, activateImmediately: Bool = false, manualActivate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping () -> Signal, headsetConnectionStatusChanged: @escaping (Bool) -> Void = { _ in }, availableOutputsChanged: @escaping ([AudioSessionOutput], AudioSessionOutput?) -> Void = { _, _ in }) -> Disposable { let id = OSAtomicIncrement32(&self.nextId) let queue = self.queue queue.async { @@ -422,7 +422,7 @@ public final class ManagedAudioSession { if let strongSelf = self { for holder in strongSelf.holders { if holder.id == id && holder.active { - strongSelf.setup(type: audioSessionType, outputMode: holder.outputMode, activateNow: false) + strongSelf.setup(type: audioSessionType, outputMode: holder.outputMode, activateNow: activateImmediately) break } } diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallToneData.swift b/submodules/TelegramCallsUI/Sources/PresentationCallToneData.swift index b8efbdfd14..065a6939d7 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallToneData.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallToneData.swift @@ -74,7 +74,7 @@ private func loadToneData(name: String, addSilenceDuration: Double = 0.0) -> Dat return data } -enum PresentationCallTone { +enum PresentationCallTone: Equatable { case ringing case connecting case busy @@ -83,6 +83,7 @@ enum PresentationCallTone { case groupJoined case groupLeft case groupConnecting + case custom(name: String, loopCount: Int?) var loopCount: Int? { switch self { @@ -96,6 +97,8 @@ enum PresentationCallTone { return 1 case .groupConnecting: return nil + case let .custom(_, loopCount): + return loopCount default: return nil } @@ -120,5 +123,7 @@ func presentationCallToneData(_ tone: PresentationCallTone) -> Data? { return loadToneData(name: "voip_group_left.mp3") case .groupConnecting: return loadToneData(name: "voip_group_connecting.mp3", addSilenceDuration: 2.0) + case let .custom(name, _): + return loadToneData(name: name) } } diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index db86ace852..587eed8c7d 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -100,7 +100,8 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext { myPeerId: account.peerId, id: call.id, accessHash: call.accessHash, - state: state + state: state, + previousServiceState: nil ) strongSelf.participantsContext = context @@ -581,7 +582,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.audioOutputStatePromise.set(.single(([], .speaker))) } - self.audioSessionDisposable = audioSession.push(audioSessionType: .voiceCall, manualActivate: { [weak self] control in + self.audioSessionDisposable = audioSession.push(audioSessionType: .voiceCall, activateImmediately: true, manualActivate: { [weak self] control in Queue.mainQueue().async { if let strongSelf = self { strongSelf.updateSessionState(internalState: strongSelf.internalState, audioSessionControl: control) @@ -842,7 +843,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } if let sourceContext = sourceContext, let initialState = sourceContext.immediateState { - let temporaryParticipantsContext = GroupCallParticipantsContext(account: self.account, peerId: self.peerId, myPeerId: myPeerId, id: sourceContext.id, accessHash: sourceContext.accessHash, state: initialState) + let temporaryParticipantsContext = GroupCallParticipantsContext(account: self.account, peerId: self.peerId, myPeerId: myPeerId, id: sourceContext.id, accessHash: sourceContext.accessHash, state: initialState, previousServiceState: sourceContext.serviceState) self.temporaryParticipantsContext = temporaryParticipantsContext self.participantsContextStateDisposable.set((combineLatest(queue: .mainQueue(), myPeer, @@ -1364,8 +1365,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { let myPeerId = self.joinAsPeerId var initialState = initialState + var serviceState: GroupCallParticipantsContext.ServiceState? if let participantsContext = self.participantsContext, let immediateState = participantsContext.immediateState { initialState.mergeActivity(from: immediateState, myPeerId: myPeerId, previousMyPeerId: self.ignorePreviousJoinAsPeerId?.0) + serviceState = participantsContext.serviceState } let participantsContext = GroupCallParticipantsContext( @@ -1374,7 +1377,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { myPeerId: self.joinAsPeerId, id: callInfo.id, accessHash: callInfo.accessHash, - state: initialState + state: initialState, + previousServiceState: serviceState ) self.temporaryParticipantsContext = nil self.participantsContext = participantsContext @@ -1397,6 +1401,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { guard let strongSelf = self else { return } + + strongSelf.participantsContext?.updateAdminIds(adminIds) var topParticipants: [GroupCallParticipantsContext.Participant] = [] @@ -1482,6 +1488,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } if participant.peer.id == strongSelf.joinAsPeerId { + var filteredMuteState = participant.muteState + if isReconnectingAsSpeaker || strongSelf.currentConnectionMode != .rtc { + filteredMuteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: false) + participant.muteState = filteredMuteState + } + let previousRaisedHand = strongSelf.stateValue.raisedHand if !(strongSelf.stateValue.muteState?.canUnmute ?? false) { strongSelf.stateValue.raisedHand = participant.raiseHandRating != nil @@ -1512,15 +1524,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { text = presentationData.strings.VoiceChat_YouCanNowSpeak } strongSelf.accountContext.sharedContext.mainWindow?.present(UndoOverlayController(presentationData: presentationData, content: .voiceChatCanSpeak(text: text), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return true }), on: .root, blockInteraction: false, completion: {}) + strongSelf.playTone(.unmuted) } }) } - - var filteredMuteState = participant.muteState - if isReconnectingAsSpeaker || strongSelf.currentConnectionMode != .rtc { - filteredMuteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: false) - participant.muteState = filteredMuteState - } if let muteState = filteredMuteState { if muteState.canUnmute { @@ -1733,6 +1740,20 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.toneRenderer?.setAudioSessionActive(value) } } + + public func playTone(_ tone: PresentationGroupCallTone) { + let name: String + switch tone { + case .unmuted: + name = "voip_group_unmuted.mp3" + case .recordingStarted: + name = "voip_group_recording_started.mp3" + } + + let toneRenderer = PresentationCallToneRenderer(tone: .custom(name: name, loopCount: 1)) + self.toneRenderer = toneRenderer + toneRenderer.setAudioSessionActive(self.isAudioSessionActive) + } private func markAsCanBeRemoved() { if self.markedAsCanBeRemoved { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 5349803585..3ca27006f5 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -1880,6 +1880,7 @@ public final class VoiceChatController: ViewController { strongSelf.call.setShouldBeRecording(true, title: title) strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false }) + strongSelf.call.playTone(.recordingStarted) } }) self?.controller?.present(controller, in: .window(.root)) diff --git a/submodules/TelegramCore/Sources/GroupCalls.swift b/submodules/TelegramCore/Sources/GroupCalls.swift index 93ce8fb5aa..d098dce1a7 100644 --- a/submodules/TelegramCore/Sources/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/GroupCalls.swift @@ -960,9 +960,6 @@ public final class GroupCallParticipantsContext { let accountPeerId = self.account.peerId return self.statePromise.get() |> map { state -> State in - if state.overlayState.isEmpty { - return state.state - } var publicState = state.state var sortAgain = false let canSeeHands = state.state.isCreator || state.state.adminIds.contains(accountPeerId) @@ -1015,20 +1012,26 @@ public final class GroupCallParticipantsContext { private var isLoadingMore: Bool = false private var shouldResetStateFromServer: Bool = false private var missingSsrcs = Set() - - private var nextActivityRank: Int = 0 + private var activityRankResetTimer: SwiftSignalKit.Timer? private let updateDefaultMuteDisposable = MetaDisposable() private let updateShouldBeRecordingDisposable = MetaDisposable() + + public struct ServiceState { + fileprivate var nextActivityRank: Int = 0 + } + + public private(set) var serviceState: ServiceState - public init(account: Account, peerId: PeerId, myPeerId: PeerId, id: Int64, accessHash: Int64, state: State) { + public init(account: Account, peerId: PeerId, myPeerId: PeerId, id: Int64, accessHash: Int64, state: State, previousServiceState: ServiceState?) { self.account = account self.myPeerId = myPeerId self.id = id self.accessHash = accessHash self.stateValue = InternalState(state: state, overlayState: OverlayState()) self.statePromise = ValuePromise(self.stateValue) + self.serviceState = previousServiceState ?? ServiceState() self.updatesDisposable.set((self.account.stateManager.groupCallParticipantUpdates |> deliverOnMainQueue).start(next: { [weak self] updates in @@ -1167,10 +1170,16 @@ public final class GroupCallParticipantsContext { } private func takeNextActivityRank() -> Int { - let value = self.nextActivityRank - self.nextActivityRank += 1 + let value = self.serviceState.nextActivityRank + self.serviceState.nextActivityRank += 1 return value } + + public func updateAdminIds(_ adminIds: Set) { + if self.stateValue.state.adminIds != adminIds { + self.stateValue.state.adminIds = adminIds + } + } public func reportSpeakingParticipants(ids: [PeerId: UInt32]) { if !ids.isEmpty {