diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 6e73ee816e..c6ebaa44e6 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -57,6 +57,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case sendLogs(PresentationTheme) case sendOneLog(PresentationTheme) case sendShareLogs + case sendGroupCallLogs case sendNotificationLogs(PresentationTheme) case sendCriticalLogs(PresentationTheme) case accounts(PresentationTheme) @@ -97,7 +98,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { switch self { case .testStickerImport: return DebugControllerSection.sticker.rawValue - case .sendLogs, .sendOneLog, .sendShareLogs, .sendNotificationLogs, .sendCriticalLogs: + case .sendLogs, .sendOneLog, .sendShareLogs, .sendGroupCallLogs, .sendNotificationLogs, .sendCriticalLogs: return DebugControllerSection.logs.rawValue case .accounts: return DebugControllerSection.logs.rawValue @@ -126,68 +127,70 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 2 case .sendShareLogs: return 3 - case .sendNotificationLogs: + case .sendGroupCallLogs: return 4 - case .sendCriticalLogs: + case .sendNotificationLogs: return 5 - case .accounts: + case .sendCriticalLogs: return 6 - case .logToFile: + case .accounts: return 7 - case .logToConsole: + case .logToFile: return 8 - case .redactSensitiveData: + case .logToConsole: return 9 - case .enableRaiseToSpeak: + case .redactSensitiveData: return 10 - case .keepChatNavigationStack: + case .enableRaiseToSpeak: return 11 - case .skipReadHistory: + case .keepChatNavigationStack: return 12 - case .crashOnSlowQueries: + case .skipReadHistory: return 13 - case .clearTips: + case .crashOnSlowQueries: return 14 - case .crash: + case .clearTips: return 15 - case .resetData: + case .crash: return 16 - case .resetDatabase: + case .resetData: return 17 - case .resetDatabaseAndCache: + case .resetDatabase: return 18 - case .resetHoles: + case .resetDatabaseAndCache: return 19 - case .reindexUnread: + case .resetHoles: return 20 - case .resetBiometricsData: + case .reindexUnread: return 21 - case .resetWebViewCache: + case .resetBiometricsData: return 22 - case .optimizeDatabase: + case .resetWebViewCache: return 23 - case .photoPreview: + case .optimizeDatabase: return 24 - case .knockoutWallpaper: + case .photoPreview: return 25 - case .experimentalCompatibility: + case .knockoutWallpaper: return 26 - case .enableDebugDataDisplay: + case .experimentalCompatibility: return 27 - case .acceleratedStickers: + case .enableDebugDataDisplay: return 28 - case .experimentalBackground: + case .acceleratedStickers: return 29 - case .snow: + case .experimentalBackground: return 30 - case .playerEmbedding: + case .snow: return 31 - case .playlistPlayback: + case .playerEmbedding: return 32 - case .voiceConference: + case .playlistPlayback: return 33 + case .voiceConference: + return 34 case let .preferredVideoCodec(index, _, _, _): - return 34 + index + return 35 + index case .disableVideoAspectScaling: return 100 case .enableVoipTcp: @@ -463,6 +466,90 @@ private enum DebugControllerEntry: ItemListNodeEntry { arguments.getRootController()?.present(composeController, animated: true, completion: nil) })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + arguments.presentController(actionSheet, nil) + }) + }) + case .sendGroupCallLogs: + return ItemListDisclosureItem(presentationData: presentationData, title: "Send Group Call Logs (Up to 40 MB)", label: "", sectionId: self.section, style: .blocks, action: { + let _ = (Logger.shared.collectLogs(basePath: arguments.context!.account.basePath + "/group-calls") + |> deliverOnMainQueue).start(next: { logs in + let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + + var items: [ActionSheetButtonItem] = [] + + if let context = arguments.context, context.sharedContext.applicationBindings.isMainApp { + items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) + controller.peerSelected = { [weak controller] peer in + let peerId = peer.id + + if let strongController = controller { + strongController.dismiss() + + let lineFeed = "\n".data(using: .utf8)! + var rawLogData: Data = Data() + for (name, path) in logs { + if !rawLogData.isEmpty { + rawLogData.append(lineFeed) + rawLogData.append(lineFeed) + } + + rawLogData.append("------ File: \(name) ------\n".data(using: .utf8)!) + + if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + rawLogData.append(data) + } + } + + let tempSource = TempBox.shared.tempFile(fileName: "Log.txt") + let tempZip = TempBox.shared.tempFile(fileName: "destination.zip") + + let _ = try? rawLogData.write(to: URL(fileURLWithPath: tempSource.path)) + + SSZipArchive.createZipFile(atPath: tempZip.path, withFilesAtPaths: [tempSource.path]) + + guard let gzippedData = try? Data(contentsOf: URL(fileURLWithPath: tempZip.path)) else { + return + } + + TempBox.shared.dispose(tempSource) + TempBox.shared.dispose(tempZip) + + let id = Int64.random(in: Int64.min ... Int64.max) + let fileResource = LocalFileMediaResource(fileId: id, size: gzippedData.count, isSecretRelated: false) + context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) + + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: gzippedData.count, attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) + + let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() + } + } + arguments.pushController(controller) + })) + } + items.append(ActionSheetButtonItem(title: "Via Email", color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + let composeController = MFMailComposeViewController() + composeController.mailComposeDelegate = arguments.mailComposeDelegate + composeController.setSubject("Telegram Logs") + for (name, path) in logs { + if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) { + composeController.addAttachmentData(data, mimeType: "application/text", fileName: name) + } + } + arguments.getRootController()?.present(composeController, animated: true, completion: nil) + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -948,6 +1035,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.sendLogs(presentationData.theme)) //entries.append(.sendOneLog(presentationData.theme)) entries.append(.sendShareLogs) + entries.append(.sendGroupCallLogs) entries.append(.sendNotificationLogs(presentationData.theme)) entries.append(.sendCriticalLogs(presentationData.theme)) if isMainApp { diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 6e25bfdbb3..65b6676674 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -435,6 +435,49 @@ 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" +} + public final class PresentationGroupCallImpl: PresentationGroupCall { private enum InternalState { case requesting @@ -1618,7 +1661,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { strongSelf.requestCall(movingFromBroadcastToRtc: false) } } - }, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: self.isVideoEnabled ? .generic : .none, enableNoiseSuppression: false, disableAudioInput: self.isStream, preferX264: self.accountContext.sharedContext.immediateExperimentalUISettings.preferredVideoCodec == "H264" + }, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: self.isVideoEnabled ? .generic : .none, enableNoiseSuppression: false, disableAudioInput: self.isStream, preferX264: self.accountContext.sharedContext.immediateExperimentalUISettings.preferredVideoCodec == "H264", logPath: allocateCallLogPath(account: self.account) )) } @@ -2927,7 +2970,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.hasScreencast = true - let screencastCallContext = OngoingGroupCallContext(video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false, disableAudioInput: true, preferX264: false) + let screencastCallContext = OngoingGroupCallContext(video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false, disableAudioInput: true, preferX264: false, logPath: "") self.screencastCallContext = screencastCallContext self.screencastJoinDisposable.set((screencastCallContext.joinPayload diff --git a/submodules/TelegramCore/Sources/Utils/Log.swift b/submodules/TelegramCore/Sources/Utils/Log.swift index 65f44bb7ea..3d0410a786 100644 --- a/submodules/TelegramCore/Sources/Utils/Log.swift +++ b/submodules/TelegramCore/Sources/Utils/Log.swift @@ -157,6 +157,30 @@ public final class Logger { } } + public func collectLogs(basePath: String) -> Signal<[(String, String)], NoError> { + return Signal { subscriber in + self.queue.async { + let logsPath: String = basePath + + var result: [(Date, String, String)] = [] + if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logsPath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) { + for url in files { + if url.lastPathComponent.hasPrefix("log-") { + if let creationDate = (try? url.resourceValues(forKeys: Set([.creationDateKey])))?.creationDate { + result.append((creationDate, url.lastPathComponent, url.path)) + } + } + } + } + result.sort(by: { $0.0 < $1.0 }) + subscriber.putNext(result.map { ($0.1, $0.2) }) + subscriber.putCompletion() + } + + return EmptyDisposable + } + } + public func collectShortLogFiles() -> Signal<[(String, String)], NoError> { return Signal { subscriber in self.queue.async { diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index 00c5c23326..cc912d35a3 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -416,7 +416,7 @@ public final class OngoingGroupCallContext { private let broadcastPartsSource = Atomic(value: nil) - init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool) { + init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String) { self.queue = queue var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)? @@ -523,7 +523,8 @@ public final class OngoingGroupCallContext { videoContentType: _videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, - preferX264: preferX264 + preferX264: preferX264, + logPath: logPath ) let queue = self.queue @@ -935,10 +936,10 @@ public final class OngoingGroupCallContext { } } - public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool) { + public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, preferX264: preferX264) + return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, preferX264: preferX264, logPath: logPath) }) } diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index 3b0258585c..09872ede43 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -3,6 +3,7 @@ import UIKit import SwiftSignalKit import TelegramCore import TelegramUIPreferences +import Network import TgVoip import TgVoipWebrtc @@ -739,6 +740,8 @@ public final class OngoingCallContext { private let tempLogFile: EngineTempBoxFile private let tempStatsLogFile: EngineTempBoxFile + private var signalingConnectionManager: QueueLocalObject? + public static func versions(includeExperimental: Bool, includeReference: Bool) -> [(version: String, supportsVideo: Bool)] { if debugUseLegacyVersionForReflectors { return [(OngoingCallThreadLocalContext.version(), true)] @@ -781,7 +784,7 @@ public final class OngoingCallContext { var allowP2P = allowP2P if debugUseLegacyVersionForReflectors { useModernImplementation = true - version = "4.0.2" + version = "4.0.3" allowP2P = false } else { useModernImplementation = version != OngoingCallThreadLocalContext.version() @@ -830,7 +833,21 @@ public final class OngoingCallContext { if debugUseLegacyVersionForReflectors { for connection in filteredConnections { if connection.username == "reflector" { - filteredConnections.append(OngoingCallConnectionDescriptionWebrtc(reflectorId: 0, hasStun: false, hasTurn: true, hasTcp: true, ip: "91.108.12.1", port: 533, username: "reflector", password: connection.password)) + let peerTag = dataWithHexString(connection.password) + if #available(iOS 12.0, *) { + strongSelf.signalingConnectionManager = QueueLocalObject(queue: queue, generate: { + return CallSignalingConnectionManager(queue: queue, peerTag: peerTag, servers: [OngoingCallConnectionDescriptionWebrtc(reflectorId: 0, hasStun: false, hasTurn: true, hasTcp: true, ip: "91.108.12.1", port: 533, username: "reflector", password: connection.password)], dataReceived: { data in + guard let strongSelf = self else { + return + } + strongSelf.withContext { context in + if let context = context as? OngoingCallThreadLocalContextWebrtc { + context.addSignaling(data) + } + } + }) + }) + } break } @@ -838,7 +855,20 @@ public final class OngoingCallContext { } let context = OngoingCallThreadLocalContextWebrtc(version: version, queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer, networkType: ongoingNetworkTypeForTypeWebrtc(initialNetworkType), dataSaving: ongoingDataSavingForTypeWebrtc(dataSaving), derivedState: derivedState.data, key: key, isOutgoing: isOutgoing, connections: filteredConnections, maxLayer: maxLayer, allowP2P: allowP2P, allowTCP: enableTCP, enableStunMarking: enableStunMarking, logPath: tempLogPath, statsLogPath: tempStatsLogPath, sendSignalingData: { [weak callSessionManager] data in - callSessionManager?.sendSignalingData(internalId: internalId, data: data) + queue.async { + guard let strongSelf = self else { + return + } + if let signalingConnectionManager = strongSelf.signalingConnectionManager { + signalingConnectionManager.with { impl in + impl.send(payloadData: data) + } + } + + if let callSessionManager = callSessionManager { + callSessionManager.sendSignalingData(internalId: internalId, data: data) + } + } }, videoCapturer: video?.impl, preferredVideoCodec: preferredVideoCodec, audioInputDeviceId: "") strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context)) @@ -948,6 +978,10 @@ public final class OngoingCallContext { } } }) + + strongSelf.signalingConnectionManager?.with { impl in + impl.start() + } } })) } @@ -1143,3 +1177,257 @@ public final class OngoingCallContext { } } } + +private protocol CallSignalingConnection { + func start() + func stop() + func send(payloadData: Data) +} + +@available(iOS 12.0, *) +private final class CallSignalingConnectionImpl: CallSignalingConnection { + private let queue: Queue + private let host: NWEndpoint.Host + private let port: NWEndpoint.Port + private let peerTag: Data + private let dataReceived: (Data) -> Void + private let isClosed: () -> Void + private let connection: NWConnection + + private var isConnected: Bool = false + + private var pingTimer: SwiftSignalKit.Timer? + + private var queuedPayloads: [Data] = [] + + init(queue: Queue, host: String, port: UInt16, peerTag: Data, dataReceived: @escaping (Data) -> Void, isClosed: @escaping () -> Void) { + self.queue = queue + self.host = NWEndpoint.Host(host) + self.port = NWEndpoint.Port(rawValue: port)! + self.peerTag = peerTag + self.dataReceived = dataReceived + self.isClosed = isClosed + self.connection = NWConnection(host: self.host, port: self.port, using: .tcp) + + self.connection.stateUpdateHandler = { [weak self] state in + queue.async { + self?.stateUpdated(state: state) + } + } + } + + private func stateUpdated(state: NWConnection.State) { + switch state { + case .ready: + Logger.shared.log("CallSignaling", "Connection state is ready") + + var headerData = Data(count: 4) + headerData.withUnsafeMutableBytes { bytes in + bytes.baseAddress!.assumingMemoryBound(to: UInt32.self).pointee = 0xeeeeeeee + } + self.connection.send(content: headerData, completion: .contentProcessed({ error in + if let error = error { + Logger.shared.log("CallSignaling", "Connection send header error: \(error)") + } + })) + + self.beginPingTimer() + + self.sendPacket(payload: Data()) + case let .failed(error): + Logger.shared.log("CallSignaling", "Connection error: \(error)") + self.onIsClosed() + default: + break + } + } + + func start() { + self.connection.start(queue: self.queue.queue) + self.receivePacketHeader() + } + + private func beginPingTimer() { + self.pingTimer = SwiftSignalKit.Timer(timeout: self.isConnected ? 2.0 : 0.15, repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.sendPacket(payload: Data()) + + strongSelf.beginPingTimer() + }, queue: self.queue) + self.pingTimer?.start() + } + + private func receivePacketHeader() { + self.connection.receive(minimumIncompleteLength: 4, maximumLength: 4, completion: { [weak self] data, _, _, error in + guard let strongSelf = self else { + return + } + if let data = data, data.count == 4 { + let payloadSize = data.withUnsafeBytes { bytes -> UInt32 in + return bytes.baseAddress!.assumingMemoryBound(to: UInt32.self).pointee + } + if payloadSize < 2 * 1024 * 1024 { + strongSelf.receivePacketPayload(size: Int(payloadSize)) + } else { + Logger.shared.log("CallSignaling", "Connection received invalid packet size: \(payloadSize)") + } + } else { + Logger.shared.log("CallSignaling", "Connection receive packet header error: \(String(describing: error))") + strongSelf.onIsClosed() + } + }) + } + + private func receivePacketPayload(size: Int) { + self.connection.receive(minimumIncompleteLength: size, maximumLength: size, completion: { [weak self] data, _, _, error in + guard let strongSelf = self else { + return + } + if let data = data, data.count == size { + Logger.shared.log("CallSignaling", "Connection receive packet payload: \(data.count) bytes") + + if data.count < 16 + 4 { + Logger.shared.log("CallSignaling", "Connection invalid payload size: \(data.count)") + strongSelf.onIsClosed() + } else { + let readPeerTag = data.subdata(in: 0 ..< 16) + if readPeerTag != strongSelf.peerTag { + Logger.shared.log("CallSignaling", "Peer tag mismatch: \(hexString(readPeerTag))") + strongSelf.onIsClosed() + } else { + let actualPayloadSize = data.withUnsafeBytes { bytes -> UInt32 in + var result: UInt32 = 0 + memcpy(&result, bytes.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: 16), 4) + return result + } + + if Int(actualPayloadSize) > data.count - 16 - 4 { + Logger.shared.log("CallSignaling", "Connection invalid actual payload size: \(actualPayloadSize)") + strongSelf.onIsClosed() + } else { + if !strongSelf.isConnected { + strongSelf.isConnected = true + + for payload in strongSelf.queuedPayloads { + strongSelf.sendPacket(payload: payload) + } + strongSelf.queuedPayloads.removeAll() + } + + if actualPayloadSize != 0 { + strongSelf.dataReceived(data.subdata(in: (16 + 4) ..< (16 + 4 + Int(actualPayloadSize)))) + } else { + //strongSelf.sendPacket(payload: Data()) + } + strongSelf.receivePacketHeader() + } + } + } + } else { + Logger.shared.log("CallSignaling", "Connection receive packet payload error: \(String(describing: error))") + strongSelf.onIsClosed() + } + }) + } + + func stop() { + self.connection.stateUpdateHandler = nil + self.connection.cancel() + } + + private func onIsClosed() { + self.connection.stateUpdateHandler = nil + self.connection.cancel() + + self.isClosed() + } + + func send(payloadData: Data) { + if self.isConnected { + self.sendPacket(payload: payloadData) + } else { + self.queuedPayloads.append(payloadData) + } + } + + private func sendPacket(payload: Data) { + var payloadSize = UInt32(payload.count) + let cleanSize = 16 + 4 + payloadSize + let paddingSize = ((cleanSize + 3) & ~(4 - 1)) - cleanSize + var totalSize = cleanSize + paddingSize + + var sendBuffer = Data(count: 4 + Int(totalSize)) + sendBuffer.withUnsafeMutableBytes { bytes in + let baseAddress = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + + memcpy(baseAddress, &totalSize, 4) + + self.peerTag.withUnsafeBytes { peerTagBytes -> Void in + memcpy(baseAddress.advanced(by: 4), peerTagBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), 16) + } + + memcpy(baseAddress.advanced(by: 4 + 16), &payloadSize, 4) + + payload.withUnsafeBytes { payloadBytes -> Void in + memcpy(baseAddress.advanced(by: 4 + 16 + 4), payloadBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), payloadBytes.count) + } + } + + Logger.shared.log("CallSignaling", "Send packet payload: \(totalSize) bytes") + + self.connection.send(content: sendBuffer, isComplete: true, completion: .contentProcessed({ error in + if let error = error { + Logger.shared.log("CallSignaling", "Connection send payload error: \(error)") + } + })) + } +} + +private final class CallSignalingConnectionManager { + private let queue: Queue + + private var nextConnectionId: Int = 0 + private var connections: [Int: CallSignalingConnection] = [:] + + init(queue: Queue, peerTag: Data, servers: [OngoingCallConnectionDescriptionWebrtc], dataReceived: @escaping (Data) -> Void) { + self.queue = queue + + for server in servers { + if server.hasTcp { + let id = self.nextConnectionId + self.nextConnectionId += 1 + if #available(iOS 12.0, *) { + let connection = CallSignalingConnectionImpl(queue: queue, host: server.ip, port: UInt16(server.port), peerTag: peerTag, dataReceived: { data in + dataReceived(data) + }, isClosed: { [weak self] in + guard let strongSelf = self else { + return + } + let _ = strongSelf + }) + connections[id] = connection + } + } + } + } + + func start() { + for (_, connection) in self.connections { + connection.start() + } + } + + func stop() { + for (_, connection) in self.connections { + connection.stop() + } + } + + func send(payloadData: Data) { + for (_, connection) in self.connections { + connection.send(payloadData: payloadData) + } + } +} diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index 768b115e39..e46333f88a 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -353,7 +353,8 @@ typedef NS_ENUM(int32_t, OngoingGroupCallRequestedVideoQuality) { videoContentType:(OngoingGroupCallVideoContentType)videoContentType enableNoiseSuppression:(bool)enableNoiseSuppression disableAudioInput:(bool)disableAudioInput - preferX264:(bool)preferX264; + preferX264:(bool)preferX264 + logPath:(NSString * _Nonnull)logPath; - (void)stop; diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index e3b7a18268..58ed57d11e 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -1391,7 +1391,8 @@ private: videoContentType:(OngoingGroupCallVideoContentType)videoContentType enableNoiseSuppression:(bool)enableNoiseSuppression disableAudioInput:(bool)disableAudioInput - preferX264:(bool)preferX264 { + preferX264:(bool)preferX264 + logPath:(NSString * _Nonnull)logPath { self = [super init]; if (self != nil) { _queue = queue; @@ -1429,10 +1430,8 @@ private: bool disableOutgoingAudioProcessing = false; tgcalls::GroupConfig config; - config.need_log = false; -#if DEBUG config.need_log = true; -#endif + config.logPath.data = std::string(logPath.length == 0 ? "" : logPath.UTF8String); __weak GroupCallThreadLocalContext *weakSelf = self; _instance.reset(new tgcalls::GroupInstanceCustomImpl((tgcalls::GroupInstanceDescriptor){ diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 644b8e5757..0ecfbd22fd 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 644b8e5757cdd286ba665d2d3e5adf57f9b66f4c +Subproject commit 0ecfbd22fdc764d341a50236e6c1546884bafa3c