From 9e165ca150d852e221e38665ecccf16919febfbb Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Mon, 7 Apr 2025 12:50:23 +0400 Subject: [PATCH] Conference updates --- .../Sources/NotificationService.swift | 9 +- .../Sources/AccountGroupCallContextImpl.swift | 192 +++++++ .../Sources/CallKitIntegration.swift | 9 +- .../Sources/GroupCallLogs.swift | 45 ++ .../Sources/GroupCallScreencast.swift | 179 +++++++ .../Sources/PresentationCall.swift | 21 +- .../Sources/PresentationGroupCall.swift | 495 +----------------- .../Sources/ReactionStrip.swift | 45 -- .../Sources/VideoChatScreenMoreMenu.swift | 40 +- .../State/ConferenceCallE2EContext.swift | 2 +- .../TelegramUI/Sources/AppDelegate.swift | 9 +- .../td/TdBinding/Public/TdBinding/TdBinding.h | 2 +- third-party/td/TdBinding/Sources/TdBinding.mm | 4 +- 13 files changed, 494 insertions(+), 558 deletions(-) create mode 100644 submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift create mode 100644 submodules/TelegramCallsUI/Sources/GroupCallLogs.swift create mode 100644 submodules/TelegramCallsUI/Sources/GroupCallScreencast.swift delete mode 100644 submodules/TelegramCallsUI/Sources/ReactionStrip.swift diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 5470c8bb81..4c15c2c122 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -930,6 +930,7 @@ private final class NotificationServiceHandler { var id: Int64 var fromId: PeerId var fromTitle: String + var memberCount: Int var isVideo: Bool var messageId: Int32 var accountId: Int64 @@ -972,11 +973,16 @@ private final class NotificationServiceHandler { if let callId = Int64(callIdString), let messageId = Int32(messageIdString) { if let fromTitle = payloadJson["call_conference_from"] as? String { let isVideo = locKey == "CONF_VIDEOCALL_REQUEST" + var memberCount = 0 + if let callParticipantsCountString = payloadJson["call_participants_cnt"] as? String, let callParticipantsCount = Int(callParticipantsCountString) { + memberCount = callParticipantsCount + } groupCallData = GroupCallData( id: callId, fromId: peerId, fromTitle: fromTitle, + memberCount: memberCount, isVideo: isVideo, messageId: messageId, accountId: recordId.int64 @@ -1292,7 +1298,8 @@ private final class NotificationServiceHandler { var voipPayload: [AnyHashable: Any] = [ "group_call_id": "\(groupCallData.id)", "msg_id": "\(groupCallData.messageId)", - "video": "0", + "video": "\(groupCallData.isVideo)", + "member_count": "\(groupCallData.memberCount)", "from_id": "\(groupCallData.fromId.id._internalGetInt64Value())", "from_title": groupCallData.fromTitle, "accountId": "\(groupCallData.accountId)" diff --git a/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift b/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift new file mode 100644 index 0000000000..d87d06bb34 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift @@ -0,0 +1,192 @@ +import Foundation +import SwiftSignalKit +import TelegramCore +import AccountContext + +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() + public var panelData: Signal { + return self.panelDataPromise.get() + } + + public init(account: Account, engine: TelegramEngine, peerId: EnginePeer.Id?, isChannel: Bool, call: EngineGroupCallDescription) { + self.panelDataPromise.set(.single(nil)) + let state = engine.calls.getGroupCallParticipants(reference: .id(id: call.id, accessHash: call.accessHash), offset: "", ssrcs: [], limit: 100, sortAscending: nil) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + + let peer: Signal + if let peerId { + peer = engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + } else { + peer = .single(nil) + } + self.disposable = (combineLatest(queue: .mainQueue(), + state, + peer + ) + |> deliverOnMainQueue).start(next: { [weak self] state, peer in + guard let self, let state = state else { + return + } + let context = engine.calls.groupCall( + peerId: peerId, + myPeerId: account.peerId, + id: call.id, + reference: .id(id: call.id, accessHash: call.accessHash), + state: state, + previousServiceState: nil + ) + + self.participantsContext = context + + if let peerId { + self.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) + } + + var isChannel = false + if let peer = peer, case let .channel(channel) = peer, case .broadcast = channel.info { + isChannel = true + } + + return GroupCallPanelData( + peerId: peerId, + isChannel: isChannel, + info: GroupCallInfo( + id: call.id, + accessHash: call.accessHash, + participantCount: state.totalCount, + streamDcId: nil, + title: state.title, + scheduleTimestamp: state.scheduleTimestamp, + subscribedToScheduled: state.subscribedToScheduled, + recordingStartTimestamp: nil, + sortAscending: state.sortAscending, + defaultParticipantsAreMuted: state.defaultParticipantsAreMuted, + isVideoEnabled: state.isVideoEnabled, + unmutedVideoLimit: state.unmutedVideoLimit, + isStream: state.isStream, + isCreator: state.isCreator + ), + 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() + var removeTimer: SwiftSignalKit.Timer? + + init(context: AccountGroupCallContextImpl) { + self.context = context + } + } + + private let queue: Queue + private var contexts: [Int64: Record] = [:] + + private let leaveDisposables = DisposableSet() + + init(queue: Queue) { + self.queue = queue + } + + public func get(account: Account, engine: TelegramEngine, peerId: EnginePeer.Id, isChannel: Bool, call: EngineGroupCallDescription) -> AccountGroupCallContextImpl.Proxy { + let result: Record + if let current = self.contexts[call.id] { + result = current + } else { + let context = AccountGroupCallContextImpl(account: account, engine: engine, peerId: peerId, isChannel: isChannel, 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 self, self.contexts[call.id] === strongResult { + strongResult.subscribers.remove(index) + if strongResult.subscribers.isEmpty { + let removeTimer = SwiftSignalKit.Timer(timeout: 30, repeat: false, completion: { [weak self] in + if let result = result, let self, self.contexts[call.id] === result, result.subscribers.isEmpty { + self.contexts.removeValue(forKey: call.id) + } + }, queue: .mainQueue()) + strongResult.removeTimer = removeTimer + removeTimer.start() + } + } + } + }) + } + + public func leaveInBackground(engine: TelegramEngine, id: Int64, accessHash: Int64, source: UInt32) { + let disposable = engine.calls.leaveGroupCall(callId: id, accessHash: accessHash, source: source).start(completed: { [weak self] in + guard let self else { + return + } + if let context = self.contexts[id] { + context.context.participantsContext?.removeLocalPeerId() + } + }) + self.leaveDisposables.add(disposable) + } + } + + let queue: Queue = .mainQueue() + public let impl: QueueLocalObject + + public init() { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue) + }) + } +} diff --git a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift index 2e67ed442b..323457dbb7 100644 --- a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift +++ b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift @@ -95,8 +95,8 @@ public final class CallKitIntegration { sharedProviderDelegate?.applyVoiceChatOutputMode(outputMode: outputMode) } - public func updateCallIsConference(uuid: UUID) { - sharedProviderDelegate?.updateCallIsConference(uuid: uuid) + public func updateCallIsConference(uuid: UUID, title: String) { + sharedProviderDelegate?.updateCallIsConference(uuid: uuid, title: title) } } @@ -280,12 +280,11 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate { self.provider.reportOutgoingCall(with: uuid, connectedAt: date) } - func updateCallIsConference(uuid: UUID) { + func updateCallIsConference(uuid: UUID, title: String) { let update = CXCallUpdate() let handle = CXHandle(type: .generic, value: "\(uuid)") update.remoteHandle = handle - //TODO:localize - update.localizedCallerName = "Group Call" + update.localizedCallerName = title update.supportsHolding = false update.supportsGrouping = false update.supportsUngrouping = false diff --git a/submodules/TelegramCallsUI/Sources/GroupCallLogs.swift b/submodules/TelegramCallsUI/Sources/GroupCallLogs.swift new file mode 100644 index 0000000000..7eec2c6b82 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/GroupCallLogs.swift @@ -0,0 +1,45 @@ +import Foundation +import TelegramCore + +public func groupCallLogsPath(account: Account) -> String { + return account.basePath + "/group-calls" +} + +func cleanupGroupCallLogs(account: Account) { + let path = groupCallLogsPath(account: account) + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: path, isDirectory: nil) { + try? fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) + } + + var oldest: [(URL, Date)] = [] + var count = 0 + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + for url in enumerator { + if let url = url as? URL { + if let date = (try? url.resourceValues(forKeys: Set([.contentModificationDateKey])))?.contentModificationDate { + oldest.append((url, date)) + count += 1 + } + } + } + } + let callLogsLimit = 20 + if count > callLogsLimit { + oldest.sort(by: { $0.1 > $1.1 }) + while oldest.count > callLogsLimit { + try? fileManager.removeItem(atPath: oldest[oldest.count - 1].0.path) + oldest.removeLast() + } + } +} + +public func allocateCallLogPath(account: Account) -> String { + let path = groupCallLogsPath(account: account) + + let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: path), withIntermediateDirectories: true, attributes: nil) + + let name = "log-\(Date())".replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: ":", with: "_") + + return "\(path)/\(name).log" +} diff --git a/submodules/TelegramCallsUI/Sources/GroupCallScreencast.swift b/submodules/TelegramCallsUI/Sources/GroupCallScreencast.swift new file mode 100644 index 0000000000..4e95083d02 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/GroupCallScreencast.swift @@ -0,0 +1,179 @@ +import Foundation +import TelegramCore +import TelegramVoip +import SwiftSignalKit + +protocol ScreencastContext: AnyObject { + func addExternalAudioData(data: Data) + func stop(account: Account, reportCallId: CallId?) + func setRTCJoinResponse(clientParams: String) +} + +protocol ScreencastIPCContext: AnyObject { + var isActive: Signal { get } + + func requestScreencast() -> Signal<(String, UInt32), NoError>? + func setJoinResponse(clientParams: String) + func disableScreencast(account: Account) +} + +final class ScreencastInProcessIPCContext: ScreencastIPCContext { + private let isConference: Bool + private let e2eContext: ConferenceCallE2EContext? + + private let screencastBufferServerContext: IpcGroupCallBufferAppContext + private var screencastCallContext: ScreencastContext? + private let screencastCapturer: OngoingCallVideoCapturer + private var screencastFramesDisposable: Disposable? + private var screencastAudioDataDisposable: Disposable? + + var isActive: Signal { + return self.screencastBufferServerContext.isActive + } + + init(basePath: String, isConference: Bool, e2eContext: ConferenceCallE2EContext?) { + self.isConference = isConference + self.e2eContext = e2eContext + + let screencastBufferServerContext = IpcGroupCallBufferAppContext(basePath: basePath + "/broadcast-coordination") + self.screencastBufferServerContext = screencastBufferServerContext + let screencastCapturer = OngoingCallVideoCapturer(isCustom: true) + self.screencastCapturer = screencastCapturer + self.screencastFramesDisposable = (screencastBufferServerContext.frames + |> deliverOnMainQueue).start(next: { [weak screencastCapturer] screencastFrame in + guard let screencastCapturer = screencastCapturer else { + return + } + guard let sampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: screencastFrame.0) else { + return + } + screencastCapturer.injectSampleBuffer(sampleBuffer, rotation: screencastFrame.1, completion: {}) + }) + self.screencastAudioDataDisposable = (screencastBufferServerContext.audioData + |> deliverOnMainQueue).start(next: { [weak self] data in + Queue.mainQueue().async { + guard let self else { + return + } + self.screencastCallContext?.addExternalAudioData(data: data) + } + }) + } + + deinit { + self.screencastFramesDisposable?.dispose() + self.screencastAudioDataDisposable?.dispose() + } + + func requestScreencast() -> Signal<(String, UInt32), NoError>? { + if self.screencastCallContext == nil { + var encryptionContext: OngoingGroupCallEncryptionContext? + if let e2eContext = self.e2eContext { + encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: e2eContext.state, channelId: 1) + } else if self.isConference { + // Prevent non-encrypted conference calls + encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: Atomic(value: ConferenceCallE2EContext.ContextStateHolder()), channelId: 1) + } + + let screencastCallContext = InProcessScreencastContext( + context: OngoingGroupCallContext( + audioSessionActive: .single(true), + video: self.screencastCapturer, + requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, + rejoinNeeded: { }, + outgoingAudioBitrateKbit: nil, + videoContentType: .screencast, + enableNoiseSuppression: false, + disableAudioInput: true, + enableSystemMute: false, + prioritizeVP8: false, + logPath: "", + onMutedSpeechActivityDetected: { _ in }, + isConference: self.isConference, + audioIsActiveByDefault: true, + isStream: false, + sharedAudioDevice: nil, + encryptionContext: encryptionContext + ) + ) + self.screencastCallContext = screencastCallContext + return screencastCallContext.joinPayload + } else { + return nil + } + } + + func setJoinResponse(clientParams: String) { + if let screencastCallContext = self.screencastCallContext { + screencastCallContext.setRTCJoinResponse(clientParams: clientParams) + } + } + + func disableScreencast(account: Account) { + if let screencastCallContext = self.screencastCallContext { + self.screencastCallContext = nil + screencastCallContext.stop(account: account, reportCallId: nil) + + self.screencastBufferServerContext.stopScreencast() + } + } +} + +final class ScreencastEmbeddedIPCContext: ScreencastIPCContext { + private let serverContext: IpcGroupCallEmbeddedAppContext + + var isActive: Signal { + return self.serverContext.isActive + } + + init(basePath: String) { + self.serverContext = IpcGroupCallEmbeddedAppContext(basePath: basePath + "/embedded-broadcast-coordination") + } + + func requestScreencast() -> Signal<(String, UInt32), NoError>? { + if let id = self.serverContext.startScreencast() { + return self.serverContext.joinPayload + |> filter { joinPayload -> Bool in + return joinPayload.id == id + } + |> map { joinPayload -> (String, UInt32) in + return (joinPayload.data, joinPayload.ssrc) + } + } else { + return nil + } + } + + func setJoinResponse(clientParams: String) { + self.serverContext.joinResponse = IpcGroupCallEmbeddedAppContext.JoinResponse(data: clientParams) + } + + func disableScreencast(account: Account) { + self.serverContext.stopScreencast() + } +} + +private final class InProcessScreencastContext: ScreencastContext { + private let context: OngoingGroupCallContext + + var joinPayload: Signal<(String, UInt32), NoError> { + return self.context.joinPayload + } + + init(context: OngoingGroupCallContext) { + self.context = context + } + + func addExternalAudioData(data: Data) { + self.context.addExternalAudioData(data: data) + } + + func stop(account: Account, reportCallId: CallId?) { + self.context.stop(account: account, reportCallId: reportCallId, debugLog: Promise()) + } + + func setRTCJoinResponse(clientParams: String) { + self.context.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: false) + self.context.setJoinResponse(payload: clientParams) + } +} diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 297b119251..41555bd8e4 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -241,6 +241,7 @@ public final class PresentationCallImpl: PresentationCall { public let isOutgoing: Bool private let incomingConferenceSource: EngineMessage.Id? private let conferenceStableId: Int64? + private var conferenceTitle: String? public var isVideo: Bool public var isVideoPossible: Bool private let enableStunMarking: Bool @@ -483,7 +484,7 @@ public final class PresentationCallImpl: PresentationCall { } let state: CallSessionState - if let message = message { + if let message { var foundAction: TelegramMediaAction? for media in message.media { if let action = media as? TelegramMediaAction { @@ -498,6 +499,21 @@ public final class PresentationCallImpl: PresentationCall { } else { state = .ringing } + + var conferenceTitle = "Group Call" + if let peer = message.peers[message.id.peerId].flatMap(EnginePeer.init) { + conferenceTitle = peer.compactDisplayTitle + + let otherCount = conferenceCall.otherParticipants.filter({ $0 != peer.id }).count + if otherCount != 0 { + if otherCount == 1 { + conferenceTitle.append(" and 1 other") + } else { + conferenceTitle.append(" and \(otherCount) others") + } + } + } + self.conferenceTitle = conferenceTitle } else { state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions()) } @@ -749,7 +765,8 @@ public final class PresentationCallImpl: PresentationCall { self.localVideoEndpointId = nil self.remoteVideoEndpointId = nil - self.callKitIntegration?.updateCallIsConference(uuid: self.internalId) + //TODO:localize + self.callKitIntegration?.updateCallIsConference(uuid: self.internalId, title: self.conferenceTitle ?? "Group Call") } func internal_markAsCanBeRemoved() { diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index ce97d819e1..f7fe16141b 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -19,242 +19,6 @@ import TemporaryCachedPeerDataManager import CallsEmoji import TdBinding -private extension GroupCallParticipantsContext.Participant { - var allSsrcs: Set { - var participantSsrcs = Set() - if let ssrc = self.ssrc { - participantSsrcs.insert(ssrc) - } - if let videoDescription = self.videoDescription { - for group in videoDescription.ssrcGroups { - for ssrc in group.ssrcs { - participantSsrcs.insert(ssrc) - } - } - } - if let presentationDescription = self.presentationDescription { - for group in presentationDescription.ssrcGroups { - for ssrc in group.ssrcs { - participantSsrcs.insert(ssrc) - } - } - } - return participantSsrcs - } - - var videoSsrcs: Set { - var participantSsrcs = Set() - if let videoDescription = self.videoDescription { - for group in videoDescription.ssrcGroups { - for ssrc in group.ssrcs { - participantSsrcs.insert(ssrc) - } - } - } - return participantSsrcs - } - - var presentationSsrcs: Set { - var participantSsrcs = Set() - if let presentationDescription = self.presentationDescription { - for group in presentationDescription.ssrcGroups { - for ssrc in group.ssrcs { - participantSsrcs.insert(ssrc) - } - } - } - return participantSsrcs - } -} - -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() - public var panelData: Signal { - return self.panelDataPromise.get() - } - - public init(account: Account, engine: TelegramEngine, peerId: PeerId?, isChannel: Bool, call: EngineGroupCallDescription) { - self.panelDataPromise.set(.single(nil)) - let state = engine.calls.getGroupCallParticipants(reference: .id(id: call.id, accessHash: call.accessHash), offset: "", ssrcs: [], limit: 100, sortAscending: nil) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - - let peer: Signal - if let peerId { - peer = engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - } else { - peer = .single(nil) - } - self.disposable = (combineLatest(queue: .mainQueue(), - state, - peer - ) - |> deliverOnMainQueue).start(next: { [weak self] state, peer in - guard let self, let state = state else { - return - } - let context = engine.calls.groupCall( - peerId: peerId, - myPeerId: account.peerId, - id: call.id, - reference: .id(id: call.id, accessHash: call.accessHash), - state: state, - previousServiceState: nil - ) - - self.participantsContext = context - - if let peerId { - self.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) - } - - var isChannel = false - if let peer = peer, case let .channel(channel) = peer, case .broadcast = channel.info { - isChannel = true - } - - return GroupCallPanelData( - peerId: peerId, - isChannel: isChannel, - info: GroupCallInfo( - id: call.id, - accessHash: call.accessHash, - participantCount: state.totalCount, - streamDcId: nil, - title: state.title, - scheduleTimestamp: state.scheduleTimestamp, - subscribedToScheduled: state.subscribedToScheduled, - recordingStartTimestamp: nil, - sortAscending: state.sortAscending, - defaultParticipantsAreMuted: state.defaultParticipantsAreMuted, - isVideoEnabled: state.isVideoEnabled, - unmutedVideoLimit: state.unmutedVideoLimit, - isStream: state.isStream, - isCreator: state.isCreator - ), - 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() - var removeTimer: SwiftSignalKit.Timer? - - init(context: AccountGroupCallContextImpl) { - self.context = context - } - } - - private let queue: Queue - private var contexts: [Int64: Record] = [:] - - private let leaveDisposables = DisposableSet() - - init(queue: Queue) { - self.queue = queue - } - - public func get(account: Account, engine: TelegramEngine, peerId: PeerId, isChannel: Bool, call: EngineGroupCallDescription) -> AccountGroupCallContextImpl.Proxy { - let result: Record - if let current = self.contexts[call.id] { - result = current - } else { - let context = AccountGroupCallContextImpl(account: account, engine: engine, peerId: peerId, isChannel: isChannel, 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 self, self.contexts[call.id] === strongResult { - strongResult.subscribers.remove(index) - if strongResult.subscribers.isEmpty { - let removeTimer = SwiftSignalKit.Timer(timeout: 30, repeat: false, completion: { [weak self] in - if let result = result, let self, self.contexts[call.id] === result, result.subscribers.isEmpty { - self.contexts.removeValue(forKey: call.id) - } - }, queue: .mainQueue()) - strongResult.removeTimer = removeTimer - removeTimer.start() - } - } - } - }) - } - - public func leaveInBackground(engine: TelegramEngine, id: Int64, accessHash: Int64, source: UInt32) { - let disposable = engine.calls.leaveGroupCall(callId: id, accessHash: accessHash, source: source).start(completed: { [weak self] in - guard let self else { - return - } - if let context = self.contexts[id] { - context.context.participantsContext?.removeLocalPeerId() - } - }) - self.leaveDisposables.add(disposable) - } - } - - let queue: Queue = .mainQueue() - public let impl: QueueLocalObject - - public init() { - let queue = self.queue - self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue) - }) - } -} - private extension PresentationGroupCallState { static func initialValue(myPeerId: PeerId, title: String?, scheduleTimestamp: Int32?, subscribedToScheduled: Bool) -> PresentationGroupCallState { return PresentationGroupCallState( @@ -440,183 +204,6 @@ private extension CurrentImpl { } } -public func groupCallLogsPath(account: Account) -> String { - return account.basePath + "/group-calls" -} - -private func cleanupGroupCallLogs(account: Account) { - let path = groupCallLogsPath(account: account) - let fileManager = FileManager.default - if !fileManager.fileExists(atPath: path, isDirectory: nil) { - try? fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) - } - - var oldest: [(URL, Date)] = [] - var count = 0 - if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { - for url in enumerator { - if let url = url as? URL { - if let date = (try? url.resourceValues(forKeys: Set([.contentModificationDateKey])))?.contentModificationDate { - oldest.append((url, date)) - count += 1 - } - } - } - } - let callLogsLimit = 20 - if count > callLogsLimit { - oldest.sort(by: { $0.1 > $1.1 }) - while oldest.count > callLogsLimit { - try? fileManager.removeItem(atPath: oldest[oldest.count - 1].0.path) - oldest.removeLast() - } - } -} - -public func allocateCallLogPath(account: Account) -> String { - let path = groupCallLogsPath(account: account) - - let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: path), withIntermediateDirectories: true, attributes: nil) - - let name = "log-\(Date())".replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: ":", with: "_") - - return "\(path)/\(name).log" -} - -private protocol ScreencastIPCContext: AnyObject { - var isActive: Signal { get } - - func requestScreencast() -> Signal<(String, UInt32), NoError>? - func setJoinResponse(clientParams: String) - func disableScreencast(account: Account) -} - -private final class ScreencastInProcessIPCContext: ScreencastIPCContext { - private let isConference: Bool - - private let screencastBufferServerContext: IpcGroupCallBufferAppContext - private var screencastCallContext: ScreencastContext? - private let screencastCapturer: OngoingCallVideoCapturer - private var screencastFramesDisposable: Disposable? - private var screencastAudioDataDisposable: Disposable? - - var isActive: Signal { - return self.screencastBufferServerContext.isActive - } - - init(basePath: String, isConference: Bool) { - self.isConference = isConference - - let screencastBufferServerContext = IpcGroupCallBufferAppContext(basePath: basePath + "/broadcast-coordination") - self.screencastBufferServerContext = screencastBufferServerContext - let screencastCapturer = OngoingCallVideoCapturer(isCustom: true) - self.screencastCapturer = screencastCapturer - self.screencastFramesDisposable = (screencastBufferServerContext.frames - |> deliverOnMainQueue).start(next: { [weak screencastCapturer] screencastFrame in - guard let screencastCapturer = screencastCapturer else { - return - } - guard let sampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: screencastFrame.0) else { - return - } - screencastCapturer.injectSampleBuffer(sampleBuffer, rotation: screencastFrame.1, completion: {}) - }) - self.screencastAudioDataDisposable = (screencastBufferServerContext.audioData - |> deliverOnMainQueue).start(next: { [weak self] data in - Queue.mainQueue().async { - guard let self else { - return - } - self.screencastCallContext?.addExternalAudioData(data: data) - } - }) - } - - deinit { - self.screencastFramesDisposable?.dispose() - self.screencastAudioDataDisposable?.dispose() - } - - func requestScreencast() -> Signal<(String, UInt32), NoError>? { - if self.screencastCallContext == nil { - let screencastCallContext = InProcessScreencastContext( - context: OngoingGroupCallContext( - audioSessionActive: .single(true), - video: self.screencastCapturer, - requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, - rejoinNeeded: { }, - outgoingAudioBitrateKbit: nil, - videoContentType: .screencast, - enableNoiseSuppression: false, - disableAudioInput: true, - enableSystemMute: false, - prioritizeVP8: false, - logPath: "", - onMutedSpeechActivityDetected: { _ in }, - isConference: self.isConference, - audioIsActiveByDefault: true, - isStream: false, - sharedAudioDevice: nil, - encryptionContext: nil - ) - ) - self.screencastCallContext = screencastCallContext - return screencastCallContext.joinPayload - } else { - return nil - } - } - - func setJoinResponse(clientParams: String) { - if let screencastCallContext = self.screencastCallContext { - screencastCallContext.setRTCJoinResponse(clientParams: clientParams) - } - } - - func disableScreencast(account: Account) { - if let screencastCallContext = self.screencastCallContext { - self.screencastCallContext = nil - screencastCallContext.stop(account: account, reportCallId: nil) - - self.screencastBufferServerContext.stopScreencast() - } - } -} - -private final class ScreencastEmbeddedIPCContext: ScreencastIPCContext { - private let serverContext: IpcGroupCallEmbeddedAppContext - - var isActive: Signal { - return self.serverContext.isActive - } - - init(basePath: String) { - self.serverContext = IpcGroupCallEmbeddedAppContext(basePath: basePath + "/embedded-broadcast-coordination") - } - - func requestScreencast() -> Signal<(String, UInt32), NoError>? { - if let id = self.serverContext.startScreencast() { - return self.serverContext.joinPayload - |> filter { joinPayload -> Bool in - return joinPayload.id == id - } - |> map { joinPayload -> (String, UInt32) in - return (joinPayload.data, joinPayload.ssrc) - } - } else { - return nil - } - } - - func setJoinResponse(clientParams: String) { - self.serverContext.joinResponse = IpcGroupCallEmbeddedAppContext.JoinResponse(data: clientParams) - } - - func disableScreencast(account: Account) { - self.serverContext.stopScreencast() - } -} - private final class PendingConferenceInvitationContext { enum State { case ringing @@ -748,8 +335,8 @@ private final class ConferenceCallE2EContextStateImpl: ConferenceCallE2EContextS return self.call.takeOutgoingBroadcastBlocks() } - func encrypt(message: Data) -> Data? { - return self.call.encrypt(message) + func encrypt(message: Data, channelId: Int32) -> Data? { + return self.call.encrypt(message, channelId: channelId) } func decrypt(message: Data, userId: Int64) -> Data? { @@ -757,6 +344,25 @@ private final class ConferenceCallE2EContextStateImpl: ConferenceCallE2EContextS } } +class OngoingGroupCallEncryptionContextImpl: OngoingGroupCallEncryptionContext { + private let e2eCall: Atomic + private let channelId: Int32 + + init(e2eCall: Atomic, channelId: Int32) { + self.e2eCall = e2eCall + self.channelId = channelId + } + + func encrypt(message: Data) -> Data? { + let channelId = self.channelId + return self.e2eCall.with({ $0.state?.encrypt(message: message, channelId: channelId) }) + } + + func decrypt(message: Data, userId: Int64) -> Data? { + return self.e2eCall.with({ $0.state?.decrypt(message: message, userId: userId) }) + } +} + public final class PresentationGroupCallImpl: PresentationGroupCall { private enum InternalState { case requesting @@ -1505,9 +1111,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.requestCall(movingFromBroadcastToRtc: false) } - var useIPCContext = "".isEmpty - if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_use_inprocess_screencast"] != nil { - useIPCContext = false + var useIPCContext = false + if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data, let value = data["ios_use_inprocess_screencast"] as? Double { + useIPCContext = value != 0.0 } let embeddedBroadcastImplementationTypePath = self.accountContext.sharedContext.basePath + "/broadcast-coordination-type" @@ -1517,7 +1123,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { screencastIPCContext = ScreencastEmbeddedIPCContext(basePath: self.accountContext.sharedContext.basePath) let _ = try? "ipc".write(toFile: embeddedBroadcastImplementationTypePath, atomically: true, encoding: .utf8) } else { - screencastIPCContext = ScreencastInProcessIPCContext(basePath: self.accountContext.sharedContext.basePath, isConference: self.isConference) + screencastIPCContext = ScreencastInProcessIPCContext(basePath: self.accountContext.sharedContext.basePath, isConference: self.isConference, e2eContext: self.e2eContext) let _ = try? "legacy".write(toFile: embeddedBroadcastImplementationTypePath, atomically: true, encoding: .utf8) } self.screencastIPCContext = screencastIPCContext @@ -2093,28 +1699,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { audioIsActiveByDefault = false } - class OngoingGroupCallEncryptionContextImpl: OngoingGroupCallEncryptionContext { - private let e2eCall: Atomic - - init(e2eCall: Atomic) { - self.e2eCall = e2eCall - } - - func encrypt(message: Data) -> Data? { - return self.e2eCall.with({ $0.state?.encrypt(message: message) }) - } - - func decrypt(message: Data, userId: Int64) -> Data? { - return self.e2eCall.with({ $0.state?.decrypt(message: message, userId: userId) }) - } - } - var encryptionContext: OngoingGroupCallEncryptionContext? if let e2eContext = self.e2eContext { - encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: e2eContext.state) + encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: e2eContext.state, channelId: 0) } else if self.isConference { // Prevent non-encrypted conference calls - encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: Atomic(value: ConferenceCallE2EContext.ContextStateHolder())) + encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: Atomic(value: ConferenceCallE2EContext.ContextStateHolder()), channelId: 0) } var prioritizeVP8 = false @@ -4200,37 +3790,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } -private protocol ScreencastContext: AnyObject { - func addExternalAudioData(data: Data) - func stop(account: Account, reportCallId: CallId?) - func setRTCJoinResponse(clientParams: String) -} - -private final class InProcessScreencastContext: ScreencastContext { - private let context: OngoingGroupCallContext - - var joinPayload: Signal<(String, UInt32), NoError> { - return self.context.joinPayload - } - - init(context: OngoingGroupCallContext) { - self.context = context - } - - func addExternalAudioData(data: Data) { - self.context.addExternalAudioData(data: data) - } - - func stop(account: Account, reportCallId: CallId?) { - self.context.stop(account: account, reportCallId: reportCallId, debugLog: Promise()) - } - - func setRTCJoinResponse(clientParams: String) { - self.context.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: false) - self.context.setJoinResponse(payload: clientParams) - } -} - public final class TelegramE2EEncryptionProviderImpl: TelegramE2EEncryptionProvider { public static let shared = TelegramE2EEncryptionProviderImpl() diff --git a/submodules/TelegramCallsUI/Sources/ReactionStrip.swift b/submodules/TelegramCallsUI/Sources/ReactionStrip.swift deleted file mode 100644 index 4393b949ea..0000000000 --- a/submodules/TelegramCallsUI/Sources/ReactionStrip.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display - -final class ReactionStrip: ASDisplayNode { - private var labelValues: [String] = [] - private var labelNodes: [ImmediateTextNode] = [] - - var selected: ((String) -> Void)? - - override init() { - self.labelValues = ["๐Ÿงก", "๐ŸŽ†", "๐ŸŽˆ", "๐ŸŽ‰", "๐Ÿ‘", "๐Ÿ‘Ž", "๐Ÿ’ฉ", "๐Ÿ’ธ", "๐Ÿ˜‚"] - - super.init() - - for labelValue in self.labelValues { - let labelNode = ImmediateTextNode() - labelNode.attributedText = NSAttributedString(string: labelValue, font: Font.regular(20.0), textColor: .black) - self.labelNodes.append(labelNode) - self.addSubnode(labelNode) - labelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.labelTapGesture(_:)))) - } - } - - @objc private func labelTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - for i in 0 ..< self.labelNodes.count { - if self.labelNodes[i].view === recognizer.view { - self.selected?(self.labelValues[i]) - break - } - } - } - } - - func update(size: CGSize) { - var labelOrigin = CGPoint(x: 0.0, y: 0.0) - for labelNode in self.labelNodes { - let labelSize = labelNode.updateLayout(CGSize(width: 100.0, height: 100.0)) - labelNode.frame = CGRect(origin: labelOrigin, size: labelSize) - labelOrigin.x += labelSize.width + 10.0 - } - } -} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift index 2ba4db0dc0..2bcbbbdcc7 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -156,6 +156,11 @@ extension VideoChatScreenComponent.View { } let canManageCall = callState.canManageCall + + var isConference = false + if case let .group(groupCall) = currentCall { + isConference = groupCall.isConference + } var items: [ContextMenuItem] = [] @@ -175,35 +180,6 @@ extension VideoChatScreenComponent.View { } } - /*if case let .group(groupCall) = currentCall, let encryptionKey = groupCall.encryptionKeyValue { - //TODO:localize - let emojiKey = resolvedEmojiKey(data: encryptionKey) - items.append(.action(ContextMenuActionItem(text: "Encryption Key", textLayout: .secondLineWithValue(emojiKey.joined(separator: "")), icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Lock"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] c, _ in - c?.dismiss(completion: nil) - - guard let self, let environment = self.environment else { - return - } - - let alertController = componentAlertController( - theme: AlertControllerTheme(presentationTheme: defaultDarkPresentationTheme, fontSize: .regular), - content: AnyComponent(EmojiKeyAlertComponet( - theme: defaultDarkPresentationTheme, - emojiKey: emojiKey, - title: "This call is end-to-end encrypted", - text: "If the emojis on everyone's screens are the same, this call is 100% secure." - )), - actions: [ComponentAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})], - actionLayout: .horizontal - ) - - environment.controller()?.present(alertController, in: .window(.root)) - }))) - items.append(.separator) - }*/ - if let (availableOutputs, currentOutput) = self.audioOutputState, availableOutputs.count > 1 { var currentOutputTitle = "" for output in availableOutputs { @@ -233,7 +209,7 @@ extension VideoChatScreenComponent.View { }))) } - if canManageCall { + if canManageCall && !isConference { let text: String if case let .channel(channel) = peer, case .broadcast = channel.info { text = environment.strings.LiveStream_EditTitle @@ -356,7 +332,7 @@ extension VideoChatScreenComponent.View { }))) } - if case let .group(groupCall) = currentCall, !groupCall.isConference, callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { + if callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { if currentCall.hasScreencast { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_StopScreenSharing, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) @@ -375,7 +351,7 @@ extension VideoChatScreenComponent.View { } } - if canManageCall { + if canManageCall && !isConference { if let recordingStartTimestamp = callState.recordingStartTimestamp { items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak self] _, f in f(.dismissWithoutContent) diff --git a/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift b/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift index 367753900a..31d81d6c05 100644 --- a/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift +++ b/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift @@ -12,7 +12,7 @@ public protocol ConferenceCallE2EContextState: AnyObject { func takeOutgoingBroadcastBlocks() -> [Data] - func encrypt(message: Data) -> Data? + func encrypt(message: Data, channelId: Int32) -> Data? func decrypt(message: Data, userId: Int64) -> Data? } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 046a024610..5184a47089 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2165,7 +2165,14 @@ private func extractAccountManagerState(records: AccountRecordsView *)participantIds; -- (nullable NSData *)encrypt:(NSData *)message; +- (nullable NSData *)encrypt:(NSData *)message channelId:(int32_t)channelId; - (nullable NSData *)decrypt:(NSData *)message userId:(int64_t)userId; @end diff --git a/third-party/td/TdBinding/Sources/TdBinding.mm b/third-party/td/TdBinding/Sources/TdBinding.mm index 5008784b64..141c513ad7 100644 --- a/third-party/td/TdBinding/Sources/TdBinding.mm +++ b/third-party/td/TdBinding/Sources/TdBinding.mm @@ -286,9 +286,9 @@ static NSString *hexStringFromData(NSData *data) { return [[NSData alloc] initWithBytes:result.value().data() length:result.value().size()]; } -- (nullable NSData *)encrypt:(NSData *)message { +- (nullable NSData *)encrypt:(NSData *)message channelId:(int32_t)channelId { std::string mappedMessage((uint8_t *)message.bytes, ((uint8_t *)message.bytes) + message.length); - auto result = tde2e_api::call_encrypt(_callId, 0, mappedMessage); + auto result = tde2e_api::call_encrypt(_callId, channelId, mappedMessage); if (!result.is_ok()) { return nil; }