Conference updates

This commit is contained in:
Isaac 2025-04-07 12:50:23 +04:00
parent c64c8ab240
commit 9e165ca150
13 changed files with 494 additions and 558 deletions

View File

@ -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)"

View File

@ -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<GroupCallPanelData?>()
public var panelData: Signal<GroupCallPanelData?, NoError> {
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<GroupCallParticipantsContext.State?, NoError> in
return .single(nil)
}
let peer: Signal<EnginePeer?, NoError>
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<Void>()
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<Impl>
public init() {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue)
})
}
}

View File

@ -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

View File

@ -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"
}

View File

@ -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<Bool, NoError> { 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<Bool, NoError> {
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<Bool, NoError> {
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)
}
}

View File

@ -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() {

View File

@ -19,242 +19,6 @@ import TemporaryCachedPeerDataManager
import CallsEmoji
import TdBinding
private extension GroupCallParticipantsContext.Participant {
var allSsrcs: Set<UInt32> {
var participantSsrcs = Set<UInt32>()
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<UInt32> {
var participantSsrcs = Set<UInt32>()
if let videoDescription = self.videoDescription {
for group in videoDescription.ssrcGroups {
for ssrc in group.ssrcs {
participantSsrcs.insert(ssrc)
}
}
}
return participantSsrcs
}
var presentationSsrcs: Set<UInt32> {
var participantSsrcs = Set<UInt32>()
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<GroupCallPanelData?>()
public var panelData: Signal<GroupCallPanelData?, NoError> {
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<GroupCallParticipantsContext.State?, NoError> in
return .single(nil)
}
let peer: Signal<EnginePeer?, NoError>
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<Void>()
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<Impl>
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<Bool, NoError> { 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<Bool, NoError> {
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<Bool, NoError> {
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<ConferenceCallE2EContext.ContextStateHolder>
private let channelId: Int32
init(e2eCall: Atomic<ConferenceCallE2EContext.ContextStateHolder>, 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<ConferenceCallE2EContext.ContextStateHolder>
init(e2eCall: Atomic<ConferenceCallE2EContext.ContextStateHolder>) {
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()

View File

@ -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
}
}
}

View File

@ -157,6 +157,11 @@ extension VideoChatScreenComponent.View {
let canManageCall = callState.canManageCall
var isConference = false
if case let .group(groupCall) = currentCall {
isConference = groupCall.isConference
}
var items: [ContextMenuItem] = []
if self.peer != nil, let displayAsPeers = self.displayAsPeers, displayAsPeers.count > 1 {
@ -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)

View File

@ -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?
}

View File

@ -2165,7 +2165,14 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
let internalId = CallSessionManager.getStableIncomingUUID(peerId: fromPeerId.id._internalGetInt64Value(), messageId: messageId.id)
//TODO:localize
let displayTitle: "\(fromTitle)"
var displayTitle = "\(fromTitle)"
if let memberCountString = payloadJson["member_count"] as? String, let memberCount = Int(memberCountString) {
if memberCount == 1 {
displayTitle.append(" and 1 other")
} else {
displayTitle.append(" and \(memberCount) others")
}
}
callKitIntegration.reportIncomingCall(
uuid: internalId,

View File

@ -42,7 +42,7 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable NSData *)generateRemoveParticipantsBlock:(NSArray<NSNumber *> *)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

View File

@ -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;
}