diff --git a/Telegram/BUILD b/Telegram/BUILD index 0bcde3fffd..68b6446275 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -1470,6 +1470,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/BuildConfig:BuildConfig", "//submodules/WidgetItems:WidgetItems", + "//submodules/BroadcastUploadHelpers:BroadcastUploadHelpers", ], ) diff --git a/Telegram/BroadcastUpload/BroadcastUploadExtension.swift b/Telegram/BroadcastUpload/BroadcastUploadExtension.swift index 465763a5ba..f9f73db09e 100644 --- a/Telegram/BroadcastUpload/BroadcastUploadExtension.swift +++ b/Telegram/BroadcastUpload/BroadcastUploadExtension.swift @@ -4,6 +4,7 @@ import CoreVideo import TelegramVoip import SwiftSignalKit import BuildConfig +import BroadcastUploadHelpers private func rootPathForBasePath(_ appGroupPath: String) -> String { return appGroupPath + "/telegram-data" @@ -34,28 +35,29 @@ private func rootPathForBasePath(_ appGroupPath: String) -> String { super.beginRequest(with: context) } - private func finishWithGenericError() { - let error = NSError(domain: "BroadcastUploadExtension", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Finished" - ]) - finishBroadcastWithError(error) - - /*self.callContext?.stop() - self.callContext = nil - - self.ipcContext = nil*/ + private func finish(with reason: IpcGroupCallBufferBroadcastContext.Status.FinishReason) { + var errorString: String? + switch reason { + case .callEnded: + errorString = "You're not in a voice chat" + case .error: + errorString = "Finished" + case .screencastEnded: + break + } + if let errorString = errorString { + let error = NSError(domain: "BroadcastUploadExtension", code: 1, userInfo: [ + NSLocalizedDescriptionKey: errorString + ]) + finishBroadcastWithError(error) + } else { + finishBroadcastGracefully(self) + } } - private func finishWithNoBroadcast() { - let error = NSError(domain: "BroadcastUploadExtension", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "You're not in a voice chat" - ]) - finishBroadcastWithError(error) - } - override public func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) { guard let appBundleIdentifier = Bundle.main.bundleIdentifier, let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else { - self.finishWithGenericError() + self.finish(with: .error) return } @@ -65,7 +67,7 @@ private func rootPathForBasePath(_ appGroupPath: String) -> String { let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) guard let appGroupUrl = maybeAppGroupUrl else { - self.finishWithGenericError() + self.finish(with: .error) return } @@ -83,8 +85,8 @@ private func rootPathForBasePath(_ appGroupPath: String) -> String { return } switch status { - case .finished: - strongSelf.finishWithNoBroadcast() + case let .finished(reason): + strongSelf.finish(with: reason) } }) diff --git a/submodules/BroadcastUploadHelpers/BUILD b/submodules/BroadcastUploadHelpers/BUILD new file mode 100644 index 0000000000..276b7c9756 --- /dev/null +++ b/submodules/BroadcastUploadHelpers/BUILD @@ -0,0 +1,22 @@ + +objc_library( + name = "BroadcastUploadHelpers", + enable_modules = True, + module_name = "BroadcastUploadHelpers", + srcs = glob([ + "Sources/**/*.m", + "Sources/**/*.h", + ]), + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + sdk_frameworks = [ + "Foundation", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/BroadcastUploadHelpers/PublicHeaders/BroadcastUploadHelpers/BroadcastUploadHelpers.h b/submodules/BroadcastUploadHelpers/PublicHeaders/BroadcastUploadHelpers/BroadcastUploadHelpers.h new file mode 100755 index 0000000000..a1b5fe7b6e --- /dev/null +++ b/submodules/BroadcastUploadHelpers/PublicHeaders/BroadcastUploadHelpers/BroadcastUploadHelpers.h @@ -0,0 +1,8 @@ +#ifndef BroadcastUploadHelpers_h +#define BroadcastUploadHelpers_h + +#import + +void finishBroadcastGracefully(RPBroadcastSampleHandler * _Nonnull broadcastSampleHandler); + +#endif /* BroadcastUploadHelpers_h */ diff --git a/submodules/BroadcastUploadHelpers/Sources/BroadcastUploadHelpers.m b/submodules/BroadcastUploadHelpers/Sources/BroadcastUploadHelpers.m new file mode 100755 index 0000000000..fbb5705b47 --- /dev/null +++ b/submodules/BroadcastUploadHelpers/Sources/BroadcastUploadHelpers.m @@ -0,0 +1,8 @@ +#import + +void finishBroadcastGracefully(RPBroadcastSampleHandler * _Nonnull broadcastSampleHandler) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + [broadcastSampleHandler finishBroadcastWithError:nil]; +#pragma clang diagnostic pop +} diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index fb4696beed..3d3045a0e5 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -2637,6 +2637,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { accessHash: callInfo.accessHash ).start()) } + + self.screencastBufferServerContext?.stopScreencast() } /*if let _ = self.screencastIpcContext { self.screencastIpcContext = nil diff --git a/submodules/TelegramVoip/Sources/IpcGroupCallContext.swift b/submodules/TelegramVoip/Sources/IpcGroupCallContext.swift index af9f2ce13c..ec758af9d2 100644 --- a/submodules/TelegramVoip/Sources/IpcGroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/IpcGroupCallContext.swift @@ -23,6 +23,11 @@ private struct KeepaliveInfo: Codable { var timestamp: Int32 } +private struct CutoffPayload: Codable { + var id: UInt32 + var timestamp: Int32 +} + private let checkInterval: Double = 0.2 private let keepaliveTimeout: Double = 2.0 @@ -42,273 +47,14 @@ private func keepaliveInfoPath(basePath: String) -> String { return basePath + "/keepaliveInfo.json" } +private func cutoffPayloadPath(basePath: String) -> String { + return basePath + "/cutoffPayload.json" +} + private func broadcastAppSocketPath(basePath: String) -> String { return basePath + "/0" } -public final class IpcGroupCallAppContext { - private let basePath: String - private let currentId: UInt32 - - private let joinPayloadPromise = Promise() - public var joinPayload: Signal { - return self.joinPayloadPromise.get() - } - private var joinPayloadCheckTimer: SwiftSignalKit.Timer? - - private let isActivePromise = ValuePromise(false, ignoreRepeated: true) - public var isActive: Signal { - return self.isActivePromise.get() - } - private var keepaliveCheckTimer: SwiftSignalKit.Timer? - - public init(basePath: String) { - self.basePath = basePath - self.currentId = UInt32.random(in: 0 ..< UInt32.max) - - let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) - - self.sendRequest() - } - - deinit { - self.joinPayloadCheckTimer?.invalidate() - self.keepaliveCheckTimer?.invalidate() - } - - private func sendRequest() { - let timestamp = Int32(Date().timeIntervalSince1970) - let payloadDescription = PayloadDescription( - id: self.currentId, - timestamp: timestamp - ) - guard let payloadDescriptionData = try? JSONEncoder().encode(payloadDescription) else { - preconditionFailure() - } - guard let _ = try? payloadDescriptionData.write(to: URL(fileURLWithPath: payloadDescriptionPath(basePath: self.basePath)), options: .atomic) else { - preconditionFailure() - } - - self.receiveJoinPayload() - } - - private func receiveJoinPayload() { - let joinPayloadCheckTimer = SwiftSignalKit.Timer(timeout: checkInterval, repeat: true, completion: { [weak self] in - self?.checkJoinPayload() - }, queue: .mainQueue()) - self.joinPayloadCheckTimer = joinPayloadCheckTimer - joinPayloadCheckTimer.start() - } - - private func checkJoinPayload() { - let filePath = joinPayloadPath(basePath: self.basePath) - guard let joinPayloadData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { - return - } - - self.joinPayloadCheckTimer?.invalidate() - let _ = try? FileManager.default.removeItem(atPath: filePath) - - guard let joinPayload = try? JSONDecoder().decode(JoinPayload.self, from: joinPayloadData) else { - return - } - - if joinPayload.id != self.currentId { - return - } - - self.joinPayloadPromise.set(.single(joinPayload.string)) - } - - public func setJoinResponsePayload(_ joinResponsePayload: String) { - let inputJoinResponsePayload = JoinResponsePayload( - id: self.currentId, - string: joinResponsePayload - ) - guard let inputJoinResponsePayloadData = try? JSONEncoder().encode(inputJoinResponsePayload) else { - preconditionFailure() - } - guard let _ = try? inputJoinResponsePayloadData.write(to: URL(fileURLWithPath: joinResponsePayloadPath(basePath: self.basePath)), options: .atomic) else { - preconditionFailure() - } - - self.beginCheckingKeepaliveInfo() - } - - private func beginCheckingKeepaliveInfo() { - let filePath = keepaliveInfoPath(basePath: self.basePath) - guard let keepaliveInfoData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { - return - } - guard let keepaliveInfo = try? JSONDecoder().decode(KeepaliveInfo.self, from: keepaliveInfoData) else { - return - } - if keepaliveInfo.id != self.currentId { - self.isActivePromise.set(false) - return - } - let timestamp = Int32(Date().timeIntervalSince1970) - if keepaliveInfo.timestamp < timestamp - Int32(keepaliveTimeout) { - self.isActivePromise.set(false) - return - } - - self.isActivePromise.set(true) - } -} - -public final class IpcGroupCallBroadcastContext { - public enum Request { - case request - case failed - } - - private let basePath: String - - private var currentId: UInt32? - - private var requestCheckTimer: SwiftSignalKit.Timer? - private let requestPromise = Promise() - public var request: Signal { - return self.requestPromise.get() - } - - private var joinResponsePayloadCheckTimer: SwiftSignalKit.Timer? - private let joinResponsePayloadPromise = Promise() - public var joinResponsePayload: Signal { - return self.joinResponsePayloadPromise.get() - } - - private var keepaliveTimer: SwiftSignalKit.Timer? - - public init(basePath: String) { - self.basePath = basePath - - let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) - - self.receiveRequest() - } - - deinit { - self.requestCheckTimer?.invalidate() - self.joinResponsePayloadCheckTimer?.invalidate() - self.keepaliveTimer?.invalidate() - self.endActiveIndication() - } - - private func receiveRequest() { - let requestCheckTimer = SwiftSignalKit.Timer(timeout: checkInterval, repeat: true, completion: { [weak self] in - self?.checkRequest() - }, queue: .mainQueue()) - self.requestCheckTimer = requestCheckTimer - requestCheckTimer.start() - } - - private func checkRequest() { - let filePath = payloadDescriptionPath(basePath: self.basePath) - guard let payloadDescriptionData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { - return - } - - let _ = try? FileManager.default.removeItem(atPath: filePath) - - guard let payloadDescription = try? JSONDecoder().decode(PayloadDescription.self, from: payloadDescriptionData) else { - self.requestCheckTimer?.invalidate() - self.requestPromise.set(.single(.failed)) - return - } - let timestamp = Int32(Date().timeIntervalSince1970) - if payloadDescription.timestamp < timestamp - 1 * 60 { - self.requestPromise.set(.single(.failed)) - return - } - - self.requestCheckTimer?.invalidate() - - self.currentId = payloadDescription.id - self.requestPromise.set(.single(.request)) - } - - public func setJoinPayload(_ joinPayload: String) { - guard let currentId = self.currentId else { - preconditionFailure() - } - let inputPayload = JoinPayload( - id: currentId, - string: joinPayload - ) - guard let inputPayloadData = try? JSONEncoder().encode(inputPayload) else { - preconditionFailure() - } - guard let _ = try? inputPayloadData.write(to: URL(fileURLWithPath: joinPayloadPath(basePath: self.basePath)), options: .atomic) else { - preconditionFailure() - } - - self.receiveJoinResponsePayload() - } - - private func receiveJoinResponsePayload() { - let joinResponsePayloadCheckTimer = SwiftSignalKit.Timer(timeout: checkInterval, repeat: true, completion: { [weak self] in - self?.checkJoinResponsePayload() - }, queue: .mainQueue()) - self.joinResponsePayloadCheckTimer = joinResponsePayloadCheckTimer - joinResponsePayloadCheckTimer.start() - } - - private func checkJoinResponsePayload() { - let filePath = joinResponsePayloadPath(basePath: self.basePath) - guard let joinResponsePayloadData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { - return - } - - self.joinResponsePayloadCheckTimer?.invalidate() - let _ = try? FileManager.default.removeItem(atPath: filePath) - - guard let joinResponsePayload = try? JSONDecoder().decode(JoinResponsePayload.self, from: joinResponsePayloadData) else { - return - } - if joinResponsePayload.id != self.currentId { - return - } - - self.joinResponsePayloadPromise.set(.single(joinResponsePayload.string)) - } - - public func beginActiveIndication() { - if self.keepaliveTimer != nil { - return - } - - self.writeKeepaliveInfo() - - let keepaliveTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in - self?.writeKeepaliveInfo() - }, queue: .mainQueue()) - self.keepaliveTimer = keepaliveTimer - keepaliveTimer.start() - } - - private func writeKeepaliveInfo() { - guard let currentId = self.currentId else { - preconditionFailure() - } - let keepaliveInfo = KeepaliveInfo( - id: currentId, - timestamp: Int32(Date().timeIntervalSince1970) - ) - guard let keepaliveInfoData = try? JSONEncoder().encode(keepaliveInfo) else { - preconditionFailure() - } - guard let _ = try? keepaliveInfoData.write(to: URL(fileURLWithPath: keepaliveInfoPath(basePath: self.basePath)), options: .atomic) else { - preconditionFailure() - } - } - - private func endActiveIndication() { - let _ = try? FileManager.default.removeItem(atPath: keepaliveInfoPath(basePath: self.basePath)) - } -} - private final class FdReadConnection { private final class PendingData { var data: Data @@ -732,11 +478,30 @@ public final class IpcGroupCallBufferAppContext { self.isActivePromise.set(true) } + + public func stopScreencast() { + let timestamp = Int32(Date().timeIntervalSince1970) + let cutoffPayload = CutoffPayload( + id: self.id, + timestamp: timestamp + ) + guard let cutoffPayloadData = try? JSONEncoder().encode(cutoffPayload) else { + return + } + guard let _ = try? cutoffPayloadData.write(to: URL(fileURLWithPath: cutoffPayloadPath(basePath: self.basePath)), options: .atomic) else { + return + } + } } public final class IpcGroupCallBufferBroadcastContext { public enum Status { - case finished + public enum FinishReason { + case screencastEnded + case callEnded + case error + } + case finished(FinishReason) } private let basePath: String @@ -752,9 +517,9 @@ public final class IpcGroupCallBufferBroadcastContext { private var currentId: UInt32? private var callActiveInfoTimer: SwiftSignalKit.Timer? - private var keepaliveInfoTimer: SwiftSignalKit.Timer? - + private var screencastCutoffTimer: SwiftSignalKit.Timer? + public init(basePath: String) { self.basePath = basePath let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) @@ -766,6 +531,12 @@ public final class IpcGroupCallBufferBroadcastContext { }, queue: .mainQueue()) self.callActiveInfoTimer = callActiveInfoTimer callActiveInfoTimer.start() + + let screencastCutoffTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + self?.updateScreencastCutoff() + }, queue: .mainQueue()) + self.screencastCutoffTimer = screencastCutoffTimer + screencastCutoffTimer.start() } deinit { @@ -773,28 +544,45 @@ public final class IpcGroupCallBufferBroadcastContext { self.callActiveInfoTimer?.invalidate() self.keepaliveInfoTimer?.invalidate() + self.screencastCutoffTimer?.invalidate() } + private func updateScreencastCutoff() { + let filePath = cutoffPayloadPath(basePath: self.basePath) + guard let cutoffPayloadData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + return + } + + guard let cutoffPayload = try? JSONDecoder().decode(CutoffPayload.self, from: cutoffPayloadData) else { + return + } + + if let currentId = self.currentId, currentId == cutoffPayload.id { + self.statusPromise.set(.single(.finished(.screencastEnded))) + return + } + } + private func updateCallIsActive() { let filePath = payloadDescriptionPath(basePath: self.basePath) guard let payloadDescriptionData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { - self.statusPromise.set(.single(.finished)) + self.statusPromise.set(.single(.finished(.error))) return } guard let payloadDescription = try? JSONDecoder().decode(PayloadDescription.self, from: payloadDescriptionData) else { - self.statusPromise.set(.single(.finished)) + self.statusPromise.set(.single(.finished(.error))) return } let timestamp = Int32(Date().timeIntervalSince1970) if payloadDescription.timestamp < timestamp - 4 { - self.statusPromise.set(.single(.finished)) + self.statusPromise.set(.single(.finished(.callEnded))) return } if let currentId = self.currentId { if currentId != payloadDescription.id { - self.statusPromise.set(.single(.finished)) + self.statusPromise.set(.single(.finished(.callEnded))) } } else { self.currentId = payloadDescription.id