mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Conference updates
This commit is contained in:
parent
c64c8ab240
commit
9e165ca150
@ -930,6 +930,7 @@ private final class NotificationServiceHandler {
|
|||||||
var id: Int64
|
var id: Int64
|
||||||
var fromId: PeerId
|
var fromId: PeerId
|
||||||
var fromTitle: String
|
var fromTitle: String
|
||||||
|
var memberCount: Int
|
||||||
var isVideo: Bool
|
var isVideo: Bool
|
||||||
var messageId: Int32
|
var messageId: Int32
|
||||||
var accountId: Int64
|
var accountId: Int64
|
||||||
@ -972,11 +973,16 @@ private final class NotificationServiceHandler {
|
|||||||
if let callId = Int64(callIdString), let messageId = Int32(messageIdString) {
|
if let callId = Int64(callIdString), let messageId = Int32(messageIdString) {
|
||||||
if let fromTitle = payloadJson["call_conference_from"] as? String {
|
if let fromTitle = payloadJson["call_conference_from"] as? String {
|
||||||
let isVideo = locKey == "CONF_VIDEOCALL_REQUEST"
|
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(
|
groupCallData = GroupCallData(
|
||||||
id: callId,
|
id: callId,
|
||||||
fromId: peerId,
|
fromId: peerId,
|
||||||
fromTitle: fromTitle,
|
fromTitle: fromTitle,
|
||||||
|
memberCount: memberCount,
|
||||||
isVideo: isVideo,
|
isVideo: isVideo,
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
accountId: recordId.int64
|
accountId: recordId.int64
|
||||||
@ -1292,7 +1298,8 @@ private final class NotificationServiceHandler {
|
|||||||
var voipPayload: [AnyHashable: Any] = [
|
var voipPayload: [AnyHashable: Any] = [
|
||||||
"group_call_id": "\(groupCallData.id)",
|
"group_call_id": "\(groupCallData.id)",
|
||||||
"msg_id": "\(groupCallData.messageId)",
|
"msg_id": "\(groupCallData.messageId)",
|
||||||
"video": "0",
|
"video": "\(groupCallData.isVideo)",
|
||||||
|
"member_count": "\(groupCallData.memberCount)",
|
||||||
"from_id": "\(groupCallData.fromId.id._internalGetInt64Value())",
|
"from_id": "\(groupCallData.fromId.id._internalGetInt64Value())",
|
||||||
"from_title": groupCallData.fromTitle,
|
"from_title": groupCallData.fromTitle,
|
||||||
"accountId": "\(groupCallData.accountId)"
|
"accountId": "\(groupCallData.accountId)"
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -95,8 +95,8 @@ public final class CallKitIntegration {
|
|||||||
sharedProviderDelegate?.applyVoiceChatOutputMode(outputMode: outputMode)
|
sharedProviderDelegate?.applyVoiceChatOutputMode(outputMode: outputMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateCallIsConference(uuid: UUID) {
|
public func updateCallIsConference(uuid: UUID, title: String) {
|
||||||
sharedProviderDelegate?.updateCallIsConference(uuid: uuid)
|
sharedProviderDelegate?.updateCallIsConference(uuid: uuid, title: title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,12 +280,11 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
|
|||||||
self.provider.reportOutgoingCall(with: uuid, connectedAt: date)
|
self.provider.reportOutgoingCall(with: uuid, connectedAt: date)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateCallIsConference(uuid: UUID) {
|
func updateCallIsConference(uuid: UUID, title: String) {
|
||||||
let update = CXCallUpdate()
|
let update = CXCallUpdate()
|
||||||
let handle = CXHandle(type: .generic, value: "\(uuid)")
|
let handle = CXHandle(type: .generic, value: "\(uuid)")
|
||||||
update.remoteHandle = handle
|
update.remoteHandle = handle
|
||||||
//TODO:localize
|
update.localizedCallerName = title
|
||||||
update.localizedCallerName = "Group Call"
|
|
||||||
update.supportsHolding = false
|
update.supportsHolding = false
|
||||||
update.supportsGrouping = false
|
update.supportsGrouping = false
|
||||||
update.supportsUngrouping = false
|
update.supportsUngrouping = false
|
||||||
|
45
submodules/TelegramCallsUI/Sources/GroupCallLogs.swift
Normal file
45
submodules/TelegramCallsUI/Sources/GroupCallLogs.swift
Normal 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"
|
||||||
|
}
|
179
submodules/TelegramCallsUI/Sources/GroupCallScreencast.swift
Normal file
179
submodules/TelegramCallsUI/Sources/GroupCallScreencast.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -241,6 +241,7 @@ public final class PresentationCallImpl: PresentationCall {
|
|||||||
public let isOutgoing: Bool
|
public let isOutgoing: Bool
|
||||||
private let incomingConferenceSource: EngineMessage.Id?
|
private let incomingConferenceSource: EngineMessage.Id?
|
||||||
private let conferenceStableId: Int64?
|
private let conferenceStableId: Int64?
|
||||||
|
private var conferenceTitle: String?
|
||||||
public var isVideo: Bool
|
public var isVideo: Bool
|
||||||
public var isVideoPossible: Bool
|
public var isVideoPossible: Bool
|
||||||
private let enableStunMarking: Bool
|
private let enableStunMarking: Bool
|
||||||
@ -483,7 +484,7 @@ public final class PresentationCallImpl: PresentationCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let state: CallSessionState
|
let state: CallSessionState
|
||||||
if let message = message {
|
if let message {
|
||||||
var foundAction: TelegramMediaAction?
|
var foundAction: TelegramMediaAction?
|
||||||
for media in message.media {
|
for media in message.media {
|
||||||
if let action = media as? TelegramMediaAction {
|
if let action = media as? TelegramMediaAction {
|
||||||
@ -498,6 +499,21 @@ public final class PresentationCallImpl: PresentationCall {
|
|||||||
} else {
|
} else {
|
||||||
state = .ringing
|
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 {
|
} else {
|
||||||
state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions())
|
state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions())
|
||||||
}
|
}
|
||||||
@ -749,7 +765,8 @@ public final class PresentationCallImpl: PresentationCall {
|
|||||||
self.localVideoEndpointId = nil
|
self.localVideoEndpointId = nil
|
||||||
self.remoteVideoEndpointId = 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() {
|
func internal_markAsCanBeRemoved() {
|
||||||
|
@ -19,242 +19,6 @@ import TemporaryCachedPeerDataManager
|
|||||||
import CallsEmoji
|
import CallsEmoji
|
||||||
import TdBinding
|
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 {
|
private extension PresentationGroupCallState {
|
||||||
static func initialValue(myPeerId: PeerId, title: String?, scheduleTimestamp: Int32?, subscribedToScheduled: Bool) -> PresentationGroupCallState {
|
static func initialValue(myPeerId: PeerId, title: String?, scheduleTimestamp: Int32?, subscribedToScheduled: Bool) -> PresentationGroupCallState {
|
||||||
return 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 {
|
private final class PendingConferenceInvitationContext {
|
||||||
enum State {
|
enum State {
|
||||||
case ringing
|
case ringing
|
||||||
@ -748,8 +335,8 @@ private final class ConferenceCallE2EContextStateImpl: ConferenceCallE2EContextS
|
|||||||
return self.call.takeOutgoingBroadcastBlocks()
|
return self.call.takeOutgoingBroadcastBlocks()
|
||||||
}
|
}
|
||||||
|
|
||||||
func encrypt(message: Data) -> Data? {
|
func encrypt(message: Data, channelId: Int32) -> Data? {
|
||||||
return self.call.encrypt(message)
|
return self.call.encrypt(message, channelId: channelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func decrypt(message: Data, userId: Int64) -> Data? {
|
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 {
|
public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||||
private enum InternalState {
|
private enum InternalState {
|
||||||
case requesting
|
case requesting
|
||||||
@ -1505,9 +1111,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
self.requestCall(movingFromBroadcastToRtc: false)
|
self.requestCall(movingFromBroadcastToRtc: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
var useIPCContext = "".isEmpty
|
var useIPCContext = false
|
||||||
if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_use_inprocess_screencast"] != nil {
|
if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data, let value = data["ios_use_inprocess_screencast"] as? Double {
|
||||||
useIPCContext = false
|
useIPCContext = value != 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
let embeddedBroadcastImplementationTypePath = self.accountContext.sharedContext.basePath + "/broadcast-coordination-type"
|
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)
|
screencastIPCContext = ScreencastEmbeddedIPCContext(basePath: self.accountContext.sharedContext.basePath)
|
||||||
let _ = try? "ipc".write(toFile: embeddedBroadcastImplementationTypePath, atomically: true, encoding: .utf8)
|
let _ = try? "ipc".write(toFile: embeddedBroadcastImplementationTypePath, atomically: true, encoding: .utf8)
|
||||||
} else {
|
} 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)
|
let _ = try? "legacy".write(toFile: embeddedBroadcastImplementationTypePath, atomically: true, encoding: .utf8)
|
||||||
}
|
}
|
||||||
self.screencastIPCContext = screencastIPCContext
|
self.screencastIPCContext = screencastIPCContext
|
||||||
@ -2093,28 +1699,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
audioIsActiveByDefault = false
|
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?
|
var encryptionContext: OngoingGroupCallEncryptionContext?
|
||||||
if let e2eContext = self.e2eContext {
|
if let e2eContext = self.e2eContext {
|
||||||
encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: e2eContext.state)
|
encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: e2eContext.state, channelId: 0)
|
||||||
} else if self.isConference {
|
} else if self.isConference {
|
||||||
// Prevent non-encrypted conference calls
|
// Prevent non-encrypted conference calls
|
||||||
encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: Atomic(value: ConferenceCallE2EContext.ContextStateHolder()))
|
encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: Atomic(value: ConferenceCallE2EContext.ContextStateHolder()), channelId: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
var prioritizeVP8 = false
|
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 final class TelegramE2EEncryptionProviderImpl: TelegramE2EEncryptionProvider {
|
||||||
public static let shared = TelegramE2EEncryptionProviderImpl()
|
public static let shared = TelegramE2EEncryptionProviderImpl()
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -156,6 +156,11 @@ extension VideoChatScreenComponent.View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let canManageCall = callState.canManageCall
|
let canManageCall = callState.canManageCall
|
||||||
|
|
||||||
|
var isConference = false
|
||||||
|
if case let .group(groupCall) = currentCall {
|
||||||
|
isConference = groupCall.isConference
|
||||||
|
}
|
||||||
|
|
||||||
var items: [ContextMenuItem] = []
|
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 {
|
if let (availableOutputs, currentOutput) = self.audioOutputState, availableOutputs.count > 1 {
|
||||||
var currentOutputTitle = ""
|
var currentOutputTitle = ""
|
||||||
for output in availableOutputs {
|
for output in availableOutputs {
|
||||||
@ -233,7 +209,7 @@ extension VideoChatScreenComponent.View {
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
if canManageCall {
|
if canManageCall && !isConference {
|
||||||
let text: String
|
let text: String
|
||||||
if case let .channel(channel) = peer, case .broadcast = channel.info {
|
if case let .channel(channel) = peer, case .broadcast = channel.info {
|
||||||
text = environment.strings.LiveStream_EditTitle
|
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 {
|
if currentCall.hasScreencast {
|
||||||
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_StopScreenSharing, icon: { theme in
|
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)
|
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 {
|
if let recordingStartTimestamp = callState.recordingStartTimestamp {
|
||||||
items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak self] _, f in
|
items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak self] _, f in
|
||||||
f(.dismissWithoutContent)
|
f(.dismissWithoutContent)
|
||||||
|
@ -12,7 +12,7 @@ public protocol ConferenceCallE2EContextState: AnyObject {
|
|||||||
|
|
||||||
func takeOutgoingBroadcastBlocks() -> [Data]
|
func takeOutgoingBroadcastBlocks() -> [Data]
|
||||||
|
|
||||||
func encrypt(message: Data) -> Data?
|
func encrypt(message: Data, channelId: Int32) -> Data?
|
||||||
func decrypt(message: Data, userId: Int64) -> Data?
|
func decrypt(message: Data, userId: Int64) -> Data?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2165,7 +2165,14 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
|||||||
let internalId = CallSessionManager.getStableIncomingUUID(peerId: fromPeerId.id._internalGetInt64Value(), messageId: messageId.id)
|
let internalId = CallSessionManager.getStableIncomingUUID(peerId: fromPeerId.id._internalGetInt64Value(), messageId: messageId.id)
|
||||||
|
|
||||||
//TODO:localize
|
//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(
|
callKitIntegration.reportIncomingCall(
|
||||||
uuid: internalId,
|
uuid: internalId,
|
||||||
|
@ -42,7 +42,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
|
|
||||||
- (nullable NSData *)generateRemoveParticipantsBlock:(NSArray<NSNumber *> *)participantIds;
|
- (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;
|
- (nullable NSData *)decrypt:(NSData *)message userId:(int64_t)userId;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -286,9 +286,9 @@ static NSString *hexStringFromData(NSData *data) {
|
|||||||
return [[NSData alloc] initWithBytes:result.value().data() length:result.value().size()];
|
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);
|
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()) {
|
if (!result.is_ok()) {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user