From 549e3642d58ec3f6bb23ddce5dddc7ed943fd342 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 4 May 2021 22:29:03 +0400 Subject: [PATCH] Implement screencasting --- Telegram/BUILD | 6 + .../BroadcastUploadExtension.swift | 192 ++++ .../NotificationViewController.swift | 2 +- Telegram/Share/ShareRootController.swift | 2 +- Telegram/SiriIntents/Info.plist | 46 - Telegram/SupportFiles/Empty.swift | 1 - Telegram/Widget/Info.plist | 31 - Telegram/Widget/PeerNode.swift | 172 --- Telegram/Widget/TodayViewController.swift | 171 --- Telegram/Widget/Widget-Bridging-Header.h | 4 - Telegram/Widget/ar.lproj/InfoPlist.strings | 1 - Telegram/Widget/de.lproj/InfoPlist.strings | 1 - Telegram/Widget/en.lproj/InfoPlist.strings | 1 - Telegram/Widget/en.lproj/Localizable.strings | 2 - Telegram/Widget/es.lproj/InfoPlist.strings | 1 - Telegram/Widget/it.lproj/InfoPlist.strings | 1 - Telegram/Widget/ko.lproj/InfoPlist.strings | 1 - Telegram/Widget/nl.lproj/InfoPlist.strings | 1 - Telegram/Widget/pt.lproj/InfoPlist.strings | 1 - Telegram/Widget/ru.lproj/InfoPlist.strings | 1 - .../Sources/AccountContext.swift | 5 +- .../Sources/PresentationGroupCall.swift | 151 ++- .../VoiceChatCameraPreviewController.swift | 34 +- .../Sources/VoiceChatController.swift | 4 + .../TelegramUI/Sources/AppDelegate.swift | 4 +- .../Sources/NotificationContentContext.swift | 8 +- .../Sources/ShareExtensionContext.swift | 8 +- .../Sources/SharedAccountContext.swift | 4 +- .../Sources/IpcGroupCallContext.swift | 981 ++++++++++++++++++ .../Sources/OngoingCallContext.swift | 16 +- .../OngoingCallThreadLocalContext.h | 10 + .../Sources/OngoingCallThreadLocalContext.mm | 58 ++ submodules/TgVoipWebrtc/tgcalls | 2 +- 33 files changed, 1461 insertions(+), 462 deletions(-) create mode 100644 Telegram/BroadcastUpload/BroadcastUploadExtension.swift delete mode 100644 Telegram/SiriIntents/Info.plist delete mode 100644 Telegram/SupportFiles/Empty.swift delete mode 100644 Telegram/Widget/Info.plist delete mode 100644 Telegram/Widget/PeerNode.swift delete mode 100644 Telegram/Widget/TodayViewController.swift delete mode 100644 Telegram/Widget/Widget-Bridging-Header.h delete mode 100644 Telegram/Widget/ar.lproj/InfoPlist.strings delete mode 100644 Telegram/Widget/de.lproj/InfoPlist.strings delete mode 100644 Telegram/Widget/en.lproj/InfoPlist.strings delete mode 100644 Telegram/Widget/en.lproj/Localizable.strings delete mode 100644 Telegram/Widget/es.lproj/InfoPlist.strings delete mode 100644 Telegram/Widget/it.lproj/InfoPlist.strings delete mode 100644 Telegram/Widget/ko.lproj/InfoPlist.strings delete mode 100644 Telegram/Widget/nl.lproj/InfoPlist.strings delete mode 100644 Telegram/Widget/pt.lproj/InfoPlist.strings delete mode 100644 Telegram/Widget/ru.lproj/InfoPlist.strings create mode 100644 submodules/TelegramVoip/Sources/IpcGroupCallContext.swift diff --git a/Telegram/BUILD b/Telegram/BUILD index 4b01091004..f3af41994c 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -1465,6 +1465,10 @@ swift_library( "BroadcastUpload/**/*.swift", ]), deps = [ + "//submodules/TelegramUI:TelegramUI", + "//submodules/TelegramVoip:TelegramVoip", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/BuildConfig:BuildConfig", ], ) @@ -1510,6 +1514,8 @@ ios_extension( }), deps = [":BroadcastUploadExtensionLib"], frameworks = [ + ":TelegramUIFramework", + ":SwiftSignalKitFramework", ], ) diff --git a/Telegram/BroadcastUpload/BroadcastUploadExtension.swift b/Telegram/BroadcastUpload/BroadcastUploadExtension.swift new file mode 100644 index 0000000000..9fcb952043 --- /dev/null +++ b/Telegram/BroadcastUpload/BroadcastUploadExtension.swift @@ -0,0 +1,192 @@ +import Foundation +import ReplayKit +import CoreVideo +import TelegramVoip +import SwiftSignalKit +import BuildConfig + +private func rootPathForBasePath(_ appGroupPath: String) -> String { + return appGroupPath + "/telegram-data" +} + +@available(iOS 10.0, *) +@objc(BroadcastUploadSampleHandler) class BroadcastUploadSampleHandler: RPBroadcastSampleHandler { + /*private var ipcContext: IpcGroupCallBroadcastContext? + private var callContext: OngoingGroupCallContext? + private var videoCapturer: OngoingCallVideoCapturer? + private var requestDisposable: Disposable? + private var joinPayloadDisposable: Disposable? + private var joinResponsePayloadDisposable: Disposable?*/ + + private var screencastBufferClientContext: IpcGroupCallBufferBroadcastContext? + private var statusDisposable: Disposable? + + deinit { + /*self.requestDisposable?.dispose() + self.joinPayloadDisposable?.dispose() + self.joinResponsePayloadDisposable?.dispose() + self.callContext?.stop()*/ + + self.statusDisposable?.dispose() + } + + public override func beginRequest(with context: NSExtensionContext) { + 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 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() + return + } + + let baseAppBundleId = String(appBundleIdentifier[.. deliverOnMainQueue).start(next: { [weak self] status in + guard let strongSelf = self else { + return + } + switch status { + case .finished: + strongSelf.finishWithNoBroadcast() + } + }) + + /*let ipcContext = IpcGroupCallBroadcastContext(basePath: rootPath + "/broadcast-coordination") + self.ipcContext = ipcContext + + self.requestDisposable = (ipcContext.request + |> timeout(3.0, queue: .mainQueue(), alternate: .single(.failed)) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] request in + guard let strongSelf = self else { + return + } + switch request { + case .request: + strongSelf.beginWithRequest() + case .failed: + strongSelf.finishWithGenericError() + } + })*/ + } + + /*private func beginWithRequest() { + let videoCapturer = OngoingCallVideoCapturer(isCustom: true) + self.videoCapturer = videoCapturer + + let callContext = OngoingGroupCallContext(video: videoCapturer, requestMediaChannelDescriptions: { _, _ in return EmptyDisposable }, audioStreamData: nil, rejoinNeeded: { + }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false) + self.callContext = callContext + + self.joinPayloadDisposable = (callContext.joinPayload + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] joinPayload in + guard let strongSelf = self, let ipcContext = strongSelf.ipcContext else { + return + } + ipcContext.setJoinPayload(joinPayload.0) + + strongSelf.joinResponsePayloadDisposable = (ipcContext.joinResponsePayload + |> take(1) + |> deliverOnMainQueue).start(next: { joinResponsePayload in + guard let strongSelf = self, let callContext = strongSelf.callContext, let ipcContext = strongSelf.ipcContext else { + return + } + + callContext.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false) + callContext.setJoinResponse(payload: joinResponsePayload) + + ipcContext.beginActiveIndication() + }) + }) + }*/ + + override public func broadcastPaused() { + } + + override public func broadcastResumed() { + } + + override public func broadcastFinished() { + } + + override public func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) { + switch sampleBufferType { + case RPSampleBufferType.video: + processVideoSampleBuffer(sampleBuffer: sampleBuffer) + case RPSampleBufferType.audioApp: + break + case RPSampleBufferType.audioMic: + break + @unknown default: + break + } + } + + private func processVideoSampleBuffer(sampleBuffer: CMSampleBuffer) { + guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { + return + } + + if let data = serializePixelBuffer(buffer: pixelBuffer) { + self.screencastBufferClientContext?.setCurrentFrame(data: data) + } + + //self.videoCapturer?.injectSampleBuffer(sampleBuffer) + /*if CMSampleBufferGetNumSamples(sampleBuffer) != 1 { + return + } + if !CMSampleBufferIsValid(sampleBuffer) { + return + } + if !CMSampleBufferDataIsReady(sampleBuffer) { + return + } + guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { + return + } + + let pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer) + + CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) + + CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)*/ + } +} diff --git a/Telegram/NotificationContent/NotificationViewController.swift b/Telegram/NotificationContent/NotificationViewController.swift index d61ef5ffa2..56301732f9 100644 --- a/Telegram/NotificationContent/NotificationViewController.swift +++ b/Telegram/NotificationContent/NotificationViewController.swift @@ -38,7 +38,7 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" - self.impl = NotificationViewControllerImpl(initializationData: NotificationViewControllerInitializationData(appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), setPreferredContentSize: { [weak self] size in + self.impl = NotificationViewControllerImpl(initializationData: NotificationViewControllerInitializationData(appBundleId: baseAppBundleId, appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), setPreferredContentSize: { [weak self] size in self?.preferredContentSize = size }) } diff --git a/Telegram/Share/ShareRootController.swift b/Telegram/Share/ShareRootController.swift index c0da95155d..1c53d48b3e 100644 --- a/Telegram/Share/ShareRootController.swift +++ b/Telegram/Share/ShareRootController.swift @@ -45,7 +45,7 @@ class ShareRootController: UIViewController { let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" - self.impl = ShareRootControllerImpl(initializationData: ShareRootControllerInitializationData(appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), getExtensionContext: { [weak self] in + self.impl = ShareRootControllerImpl(initializationData: ShareRootControllerInitializationData(appBundleId: baseAppBundleId, appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), getExtensionContext: { [weak self] in return self?.extensionContext }) } diff --git a/Telegram/SiriIntents/Info.plist b/Telegram/SiriIntents/Info.plist deleted file mode 100644 index d77820f815..0000000000 --- a/Telegram/SiriIntents/Info.plist +++ /dev/null @@ -1,46 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - ${APP_NAME} - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - XPC! - CFBundleShortVersionString - $(PRODUCT_BUNDLE_SHORT_VERSION) - CFBundleVersion - ${BUILD_NUMBER} - NSExtension - - NSExtensionAttributes - - IntentsRestrictedWhileLocked - - IntentsRestrictedWhileProtectedDataUnavailable - - IntentsSupported - - INSendMessageIntent - INStartAudioCallIntent - INSearchForMessagesIntent - INSetMessageAttributeIntent - INSearchCallHistoryIntent - - - NSExtensionPointIdentifier - com.apple.intents-service - NSExtensionPrincipalClass - IntentHandler - - - diff --git a/Telegram/SupportFiles/Empty.swift b/Telegram/SupportFiles/Empty.swift deleted file mode 100644 index 8b13789179..0000000000 --- a/Telegram/SupportFiles/Empty.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Telegram/Widget/Info.plist b/Telegram/Widget/Info.plist deleted file mode 100644 index 6917b6ac76..0000000000 --- a/Telegram/Widget/Info.plist +++ /dev/null @@ -1,31 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - ${APP_NAME} - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - XPC! - CFBundleShortVersionString - $(PRODUCT_BUNDLE_SHORT_VERSION) - CFBundleVersion - ${BUILD_NUMBER} - NSExtension - - NSExtensionPointIdentifier - com.apple.widget-extension - NSExtensionPrincipalClass - TodayViewController - - - diff --git a/Telegram/Widget/PeerNode.swift b/Telegram/Widget/PeerNode.swift deleted file mode 100644 index b89deba9e6..0000000000 --- a/Telegram/Widget/PeerNode.swift +++ /dev/null @@ -1,172 +0,0 @@ -import Foundation -import UIKit -import WidgetItems - -private extension UIColor { - convenience init(rgb: UInt32) { - self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0) - } -} - -private let UIScreenScale = UIScreen.main.scale -private func floorToScreenPixels(_ value: CGFloat) -> CGFloat { - return floor(value * UIScreenScale) / UIScreenScale -} - -private let gradientColors: [NSArray] = [ - [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], - [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], - [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], - [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], - [UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor], - [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], - [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], -] - -private func avatarRoundImage(size: CGSize, source: UIImage) -> UIImage? { - UIGraphicsBeginImageContextWithOptions(size, false, 0.0) - let context = UIGraphicsGetCurrentContext() - - context?.beginPath() - context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - context?.clip() - - source.draw(in: CGRect(origin: CGPoint(), size: size)) - - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return image -} - -private let deviceColorSpace: CGColorSpace = { - if #available(iOSApplicationExtension 9.3, *) { - if let colorSpace = CGColorSpace(name: CGColorSpace.displayP3) { - return colorSpace - } else { - return CGColorSpaceCreateDeviceRGB() - } - } else { - return CGColorSpaceCreateDeviceRGB() - } -}() - -private func avatarViewLettersImage(size: CGSize, peerId: Int64, accountPeerId: Int64, letters: [String]) -> UIImage? { - UIGraphicsBeginImageContextWithOptions(size, false, 0.0) - let context = UIGraphicsGetCurrentContext() - - context?.beginPath() - context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - context?.clip() - - let colorIndex = abs(Int(accountPeerId + peerId)) - - let colorsArray = gradientColors[colorIndex % gradientColors.count] - var locations: [CGFloat] = [1.0, 0.0] - let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! - - context?.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) - - context?.setBlendMode(.normal) - - let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) - let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20.0), NSAttributedString.Key.foregroundColor: UIColor.white]) - - let line = CTLineCreateWithAttributedString(attributedString) - let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) - - let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0) - let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0)) - - context?.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context?.scaleBy(x: 1.0, y: -1.0) - context?.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - - context?.translateBy(x: lineOrigin.x, y: lineOrigin.y) - if let context = context { - CTLineDraw(line, context) - } - context?.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) - - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return image -} - -private let avatarSize = CGSize(width: 50.0, height: 50.0) - -private final class AvatarView: UIImageView { - init(accountPeerId: Int64, peer: WidgetDataPeer, size: CGSize) { - super.init(frame: CGRect()) - - if let path = peer.avatarPath, let image = UIImage(contentsOfFile: path), let roundImage = avatarRoundImage(size: size, source: image) { - self.image = roundImage - } else { - self.image = avatarViewLettersImage(size: size, peerId: peer.id, accountPeerId: accountPeerId, letters: peer.letters) - } - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -final class PeerView: UIView { - let peer: WidgetDataPeer - private let avatarView: AvatarView - private let titleLabel: UILabel - - private let tapped: () -> Void - - init(primaryColor: UIColor, accountPeerId: Int64, peer: WidgetDataPeer, tapped: @escaping () -> Void) { - self.peer = peer - self.tapped = tapped - self.avatarView = AvatarView(accountPeerId: accountPeerId, peer: peer, size: avatarSize) - - self.titleLabel = UILabel() - var title = peer.name - if let lastName = peer.lastName, !lastName.isEmpty { - title.append("\n") - title.append(lastName) - } - - let systemFontSize = UIFont.preferredFont(forTextStyle: .body).pointSize - let fontSize = floor(systemFontSize * 11.0 / 17.0) - - self.titleLabel.text = title - if #available(iOSApplicationExtension 13.0, *) { - self.titleLabel.textColor = UIColor.label - } else { - self.titleLabel.textColor = primaryColor - } - self.titleLabel.font = UIFont.systemFont(ofSize: fontSize) - self.titleLabel.lineBreakMode = .byTruncatingTail - self.titleLabel.numberOfLines = 2 - self.titleLabel.textAlignment = .center - - super.init(frame: CGRect()) - - self.addSubview(self.avatarView) - self.addSubview(self.titleLabel) - - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func updateLayout(size: CGSize) { - self.avatarView.frame = CGRect(origin: CGPoint(x: floor((size.width - avatarSize.width) / 2.0), y: 0.0), size: avatarSize) - - var titleSize = self.titleLabel.sizeThatFits(size) - titleSize.width = min(size.width - 6.0, ceil(titleSize.width)) - titleSize.height = ceil(titleSize.height) - self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: avatarSize.height + 5.0), size: titleSize) - } - - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.tapped() - } - } -} diff --git a/Telegram/Widget/TodayViewController.swift b/Telegram/Widget/TodayViewController.swift deleted file mode 100644 index 454691e89d..0000000000 --- a/Telegram/Widget/TodayViewController.swift +++ /dev/null @@ -1,171 +0,0 @@ -import UIKit -import NotificationCenter -import BuildConfig -import WidgetItems -import AppLockState - -private func rootPathForBasePath(_ appGroupPath: String) -> String { - return appGroupPath + "/telegram-data" -} - -@objc(TodayViewController) -class TodayViewController: UIViewController, NCWidgetProviding { - private var initializedInterface = false - - private var buildConfig: BuildConfig? - - private var primaryColor: UIColor = .black - private var placeholderLabel: UILabel? - - override func viewDidLoad() { - super.viewDidLoad() - - let appBundleIdentifier = Bundle.main.bundleIdentifier! - guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else { - return - } - let baseAppBundleId = String(appBundleIdentifier[.. Void)) { - completionHandler(.newData) - } - - @available(iOSApplicationExtension 10.0, *) - func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) { - - } - - private var widgetData: WidgetData? - - private func setWidgetData(widgetData: WidgetData, presentationData: WidgetPresentationData) { - self.widgetData = widgetData - self.peerViews.forEach { - $0.removeFromSuperview() - } - self.peerViews = [] - switch widgetData { - case .notAuthorized, .disabled: - break - case let .peers(peers): - for peer in peers.peers { - let peerView = PeerView(primaryColor: self.primaryColor, accountPeerId: peers.accountPeerId, peer: peer, tapped: { [weak self] in - if let strongSelf = self, let buildConfig = strongSelf.buildConfig { - if let url = URL(string: "\(buildConfig.appSpecificUrlScheme)://localpeer?id=\(peer.id)") { - strongSelf.extensionContext?.open(url, completionHandler: nil) - } - } - }) - self.view.addSubview(peerView) - self.peerViews.append(peerView) - } - } - - if self.peerViews.isEmpty { - self.setPlaceholderText(presentationData.applicationStartRequiredString) - } else { - self.placeholderLabel?.removeFromSuperview() - self.placeholderLabel = nil - } - - if let size = self.validLayout { - self.updateLayout(size: size) - } - } - - private var validLayout: CGSize? - - private var peerViews: [PeerView] = [] - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - self.updateLayout(size: self.view.bounds.size) - } - - private func updateLayout(size: CGSize) { - self.validLayout = size - - if let placeholderLabel = self.placeholderLabel { - placeholderLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - placeholderLabel.bounds.width) / 2.0), y: floor((size.height - placeholderLabel.bounds.height) / 2.0)), size: placeholderLabel.bounds.size) - } - - let peerSize = CGSize(width: 70.0, height: 100.0) - - var peerFrames: [CGRect] = [] - - var offset: CGFloat = 0.0 - for _ in self.peerViews { - let peerFrame = CGRect(origin: CGPoint(x: offset, y: 10.0), size: peerSize) - offset += peerFrame.size.width - if peerFrame.maxX > size.width { - break - } - peerFrames.append(peerFrame) - } - - var totalSize: CGFloat = 0.0 - for i in 0 ..< peerFrames.count { - totalSize += peerFrames[i].width - } - - let spacing: CGFloat = floor((size.width - totalSize) / CGFloat(peerFrames.count)) - offset = floor(spacing / 2.0) - for i in 0 ..< peerFrames.count { - let peerView = self.peerViews[i] - peerView.frame = CGRect(origin: CGPoint(x: offset, y: 16.0), size: peerFrames[i].size) - peerView.updateLayout(size: peerFrames[i].size) - offset += peerFrames[i].width + spacing - } - } -} diff --git a/Telegram/Widget/Widget-Bridging-Header.h b/Telegram/Widget/Widget-Bridging-Header.h deleted file mode 100644 index 16747def3f..0000000000 --- a/Telegram/Widget/Widget-Bridging-Header.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef Widget_Bridging_Header_h -#define Widget_Bridging_Header_h - -#endif diff --git a/Telegram/Widget/ar.lproj/InfoPlist.strings b/Telegram/Widget/ar.lproj/InfoPlist.strings deleted file mode 100644 index 07394dd6c9..0000000000 --- a/Telegram/Widget/ar.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "الأشخاص"; diff --git a/Telegram/Widget/de.lproj/InfoPlist.strings b/Telegram/Widget/de.lproj/InfoPlist.strings deleted file mode 100644 index 1ad24433c2..0000000000 --- a/Telegram/Widget/de.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "Leute"; diff --git a/Telegram/Widget/en.lproj/InfoPlist.strings b/Telegram/Widget/en.lproj/InfoPlist.strings deleted file mode 100644 index b1ddd1ac39..0000000000 --- a/Telegram/Widget/en.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "People"; diff --git a/Telegram/Widget/en.lproj/Localizable.strings b/Telegram/Widget/en.lproj/Localizable.strings deleted file mode 100644 index c90696d0fb..0000000000 --- a/Telegram/Widget/en.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -"Widget.NoUsers" = "No users here yet..."; -"Widget.AuthRequired" = "Open Telegram and log in."; diff --git a/Telegram/Widget/es.lproj/InfoPlist.strings b/Telegram/Widget/es.lproj/InfoPlist.strings deleted file mode 100644 index 3d5094963a..0000000000 --- a/Telegram/Widget/es.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "Personas"; diff --git a/Telegram/Widget/it.lproj/InfoPlist.strings b/Telegram/Widget/it.lproj/InfoPlist.strings deleted file mode 100644 index f118d25a4d..0000000000 --- a/Telegram/Widget/it.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "Persone"; diff --git a/Telegram/Widget/ko.lproj/InfoPlist.strings b/Telegram/Widget/ko.lproj/InfoPlist.strings deleted file mode 100644 index e1bc831c53..0000000000 --- a/Telegram/Widget/ko.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "사람"; diff --git a/Telegram/Widget/nl.lproj/InfoPlist.strings b/Telegram/Widget/nl.lproj/InfoPlist.strings deleted file mode 100644 index a23cbfc4a2..0000000000 --- a/Telegram/Widget/nl.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "Mensen"; diff --git a/Telegram/Widget/pt.lproj/InfoPlist.strings b/Telegram/Widget/pt.lproj/InfoPlist.strings deleted file mode 100644 index a6c032d0ed..0000000000 --- a/Telegram/Widget/pt.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "Pessoas"; diff --git a/Telegram/Widget/ru.lproj/InfoPlist.strings b/Telegram/Widget/ru.lproj/InfoPlist.strings deleted file mode 100644 index 689e714f47..0000000000 --- a/Telegram/Widget/ru.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "Люди"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 4968ec27ed..7f2652cf5c 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -30,6 +30,7 @@ public enum AccessType { public final class TelegramApplicationBindings { public let isMainApp: Bool + public let appBundleId: String public let containerPath: String public let appSpecificScheme: String public let openUrl: (String) -> Void @@ -54,8 +55,9 @@ public final class TelegramApplicationBindings { public let requestSetAlternateIconName: (String?, @escaping (Bool) -> Void) -> Void public let forceOrientation: (UIInterfaceOrientation) -> Void - public init(isMainApp: Bool, containerPath: String, appSpecificScheme: String, openUrl: @escaping (String) -> Void, openUniversalUrl: @escaping (String, TelegramApplicationOpenUrlCompletion) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal, clearMessageNotifications: @escaping ([MessageId]) -> Void, pushIdleTimerExtension: @escaping () -> Disposable, openSettings: @escaping () -> Void, openAppStorePage: @escaping () -> Void, registerForNotifications: @escaping (@escaping (Bool) -> Void) -> Void, requestSiriAuthorization: @escaping (@escaping (Bool) -> Void) -> Void, siriAuthorization: @escaping () -> AccessType, getWindowHost: @escaping () -> WindowHost?, presentNativeController: @escaping (UIViewController) -> Void, dismissNativeController: @escaping () -> Void, getAvailableAlternateIcons: @escaping () -> [PresentationAppIcon], getAlternateIconName: @escaping () -> String?, requestSetAlternateIconName: @escaping (String?, @escaping (Bool) -> Void) -> Void, forceOrientation: @escaping (UIInterfaceOrientation) -> Void) { + public init(isMainApp: Bool, appBundleId: String, containerPath: String, appSpecificScheme: String, openUrl: @escaping (String) -> Void, openUniversalUrl: @escaping (String, TelegramApplicationOpenUrlCompletion) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal, clearMessageNotifications: @escaping ([MessageId]) -> Void, pushIdleTimerExtension: @escaping () -> Disposable, openSettings: @escaping () -> Void, openAppStorePage: @escaping () -> Void, registerForNotifications: @escaping (@escaping (Bool) -> Void) -> Void, requestSiriAuthorization: @escaping (@escaping (Bool) -> Void) -> Void, siriAuthorization: @escaping () -> AccessType, getWindowHost: @escaping () -> WindowHost?, presentNativeController: @escaping (UIViewController) -> Void, dismissNativeController: @escaping () -> Void, getAvailableAlternateIcons: @escaping () -> [PresentationAppIcon], getAlternateIconName: @escaping () -> String?, requestSetAlternateIconName: @escaping (String?, @escaping (Bool) -> Void) -> Void, forceOrientation: @escaping (UIInterfaceOrientation) -> Void) { self.isMainApp = isMainApp + self.appBundleId = appBundleId self.containerPath = containerPath self.appSpecificScheme = appSpecificScheme self.openUrl = openUrl @@ -544,6 +546,7 @@ public protocol RecentSessionsController: class { } public protocol SharedAccountContext: class { + var sharedContainerPath: String { get } var basePath: String { get } var mainWindow: Window1? { get } var accountManager: AccountManager { get } diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index c98e57d1d8..dfed87e9b2 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -450,7 +450,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private var genericCallContext: OngoingGroupCallContext? private var currentConnectionMode: OngoingGroupCallContext.ConnectionMode = .none + private var screencastCallContext: OngoingGroupCallContext? + private var screencastBufferServerContext: IpcGroupCallBufferAppContext? + private var screencastCapturer: OngoingCallVideoCapturer? + + //private var screencastIpcContext: IpcGroupCallAppContext? + private var ssrcMapping: [UInt32: PeerId] = [:] private var requestedSsrcs = Set() @@ -620,8 +626,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private var videoCapturer: OngoingCallVideoCapturer? private var useFrontCamera: Bool = true - private var screenCapturer: OngoingCallVideoCapturer? - private let incomingVideoSourcePromise = Promise>(Set()) public var incomingVideoSources: Signal, NoError> { return self.incomingVideoSourcePromise.get() @@ -632,6 +636,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { public private(set) var schedulePending = false private var isScheduled = false private var isScheduledStarted = false + + private var screencastFramesDisposable: Disposable? + private var screencastStateDisposable: Disposable? init( accountContext: AccountContext, @@ -891,6 +898,38 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if let _ = self.initialCall { self.requestCall(movingFromBroadcastToRtc: false) } + + let basePath = self.accountContext.sharedContext.basePath + "/broadcast-coordination" + let screencastBufferServerContext = IpcGroupCallBufferAppContext(basePath: basePath) + 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 + } + screencastCapturer.injectPixelBuffer(screencastFrame) + }) + self.screencastStateDisposable = (screencastBufferServerContext.isActive + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] isActive in + guard let strongSelf = self else { + return + } + if isActive { + strongSelf.requestScreencast() + } else { + strongSelf.disableScreencast() + } + }) + + /*Queue.mainQueue().after(2.0, { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.screencastBufferClientContext = IpcGroupCallBufferBroadcastContext(basePath: basePath) + })*/ } deinit { @@ -929,6 +968,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.removedChannelMembersDisposable?.dispose() self.peerUpdatesSubscription?.dispose() + + self.screencastFramesDisposable?.dispose() + self.screencastStateDisposable?.dispose() } private func switchToTemporaryParticipantsContext(sourceContext: GroupCallParticipantsContext?, oldMyPeerId: PeerId) { @@ -2139,7 +2181,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.markedAsCanBeRemoved = true self.genericCallContext?.stop() + + //self.screencastIpcContext = nil self.screencastCallContext?.stop() + self._canBeRemoved.set(.single(true)) if self.didConnectOnce { @@ -2493,7 +2538,87 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return } - if self.screenCapturer == nil { + let screencastCallContext = OngoingGroupCallContext(video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, audioStreamData: nil, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false) + self.screencastCallContext = screencastCallContext + + self.screencastJoinDisposable.set((screencastCallContext.joinPayload + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] joinPayload in + guard let strongSelf = self else { + return + } + + strongSelf.requestDisposable.set((joinGroupCallAsScreencast( + account: strongSelf.account, + peerId: strongSelf.peerId, + callId: callInfo.id, + accessHash: callInfo.accessHash, + joinPayload: joinPayload.0 + ) + |> deliverOnMainQueue).start(next: { joinCallResult in + guard let strongSelf = self, let screencastCallContext = strongSelf.screencastCallContext else { + return + } + let clientParams = joinCallResult.jsonParams + + //screencastIpcContext.setJoinResponsePayload(clientParams) + + screencastCallContext.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false) + screencastCallContext.setJoinResponse(payload: clientParams) + + strongSelf.genericCallContext?.setIgnoreVideoEndpointIds(endpointIds: [joinCallResult.endpointId]) + }, error: { error in + guard let _ = self else { + return + } + })) + })) + + /*if self.screencastIpcContext != nil { + return + } + + let maybeCallInfo: GroupCallInfo? = self.internalState.callInfo + + guard let callInfo = maybeCallInfo else { + return + } + + let screencastIpcContext = IpcGroupCallAppContext(basePath: self.accountContext.sharedContext.basePath + "/broadcast-coordination") + self.screencastIpcContext = screencastIpcContext + self.hasScreencast = true + + self.screencastJoinDisposable.set((screencastIpcContext.joinPayload + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] joinPayload in + guard let strongSelf = self else { + return + } + + strongSelf.requestDisposable.set((joinGroupCallAsScreencast( + account: strongSelf.account, + peerId: strongSelf.peerId, + callId: callInfo.id, + accessHash: callInfo.accessHash, + joinPayload: joinPayload + ) + |> deliverOnMainQueue).start(next: { joinCallResult in + guard let strongSelf = self, let screencastIpcContext = strongSelf.screencastIpcContext else { + return + } + let clientParams = joinCallResult.jsonParams + + screencastIpcContext.setJoinResponsePayload(clientParams) + + strongSelf.genericCallContext?.setIgnoreVideoEndpointIds(endpointIds: [joinCallResult.endpointId]) + }, error: { error in + guard let _ = self else { + return + } + })) + })) + + /*if self.screenCapturer == nil { let screenCapturer = OngoingCallVideoCapturer() self.screenCapturer = screenCapturer } @@ -2512,7 +2637,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { ) self.screencastCallContext = screencastCallContext - self.hasScreencast = true self.screencastJoinDisposable.set((screencastCallContext.joinPayload |> distinctUntilChanged(isEqual: { lhs, rhs in @@ -2551,7 +2675,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return } })) - })) + }))*/*/ } public func disableScreencast() { @@ -2570,10 +2694,19 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { ).start()) } } - if let _ = self.screenCapturer { - self.screenCapturer = nil - self.screencastCallContext?.disableVideo() - } + /*if let _ = self.screencastIpcContext { + self.screencastIpcContext = nil + + let maybeCallInfo: GroupCallInfo? = self.internalState.callInfo + + if let callInfo = maybeCallInfo { + self.screencastJoinDisposable.set(leaveGroupCallAsScreencast( + account: self.account, + callId: callInfo.id, + accessHash: callInfo.accessHash + ).start()) + } + }*/ } public func setVolume(peerId: PeerId, volume: Int32, sync: Bool) { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift index a0b59a8edf..0970ec6cd9 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift @@ -11,6 +11,7 @@ import TelegramPresentationData import SolidRoundedButtonNode import PresentationDataUtils import UIKitRuntimeUtils +import ReplayKit final class VoiceChatCameraPreviewController: ViewController { private var controllerNode: VoiceChatCameraPreviewControllerNode { @@ -60,7 +61,7 @@ final class VoiceChatCameraPreviewController: ViewController { } override public func loadDisplayNode() { - self.displayNode = VoiceChatCameraPreviewControllerNode(context: self.context, cameraNode: self.cameraNode) + self.displayNode = VoiceChatCameraPreviewControllerNode(controller: self, context: self.context, cameraNode: self.cameraNode) self.controllerNode.shareCamera = { [weak self] in if let strongSelf = self { strongSelf.shareCamera(strongSelf.cameraNode) @@ -107,6 +108,7 @@ final class VoiceChatCameraPreviewController: ViewController { } private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { + private weak var controller: VoiceChatCameraPreviewController? private let context: AccountContext private var presentationData: PresentationData @@ -121,6 +123,7 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U private let previewContainerNode: ASDisplayNode private let cameraButton: SolidRoundedButtonNode private let screenButton: SolidRoundedButtonNode + private var broadcastPickerView: UIView? private let cancelButton: SolidRoundedButtonNode private let switchCameraButton: HighlightTrackingButtonNode @@ -128,6 +131,8 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U private let switchCameraIconNode: ASImageNode private var containerLayout: (ContainerViewLayout, CGFloat)? + + private var applicationStateDisposable: Disposable? var shareCamera: (() -> Void)? var switchCamera: (() -> Void)? @@ -135,7 +140,8 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U var dismiss: (() -> Void)? var cancel: (() -> Void)? - init(context: AccountContext, cameraNode: GroupVideoNode) { + init(controller: VoiceChatCameraPreviewController, context: AccountContext, cameraNode: GroupVideoNode) { + self.controller = controller self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -179,6 +185,14 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U self.screenButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: buttonTextColor), font: .bold, height: 52.0, cornerRadius: 11.0, gloss: false) self.screenButton.title = self.presentationData.strings.VoiceChat_VideoPreviewShareScreen + + if #available(iOS 12.0, *) { + let broadcastPickerView = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 50, height: 52.0)) + broadcastPickerView.alpha = 0.1 + broadcastPickerView.preferredExtension = "\(self.context.sharedContext.applicationBindings.appBundleId).BroadcastUpload" + broadcastPickerView.showsMicrophoneButton = false + self.broadcastPickerView = broadcastPickerView + } self.cancelButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: buttonTextColor), font: .regular, height: 52.0, cornerRadius: 11.0, gloss: false) self.cancelButton.title = self.presentationData.strings.Common_Cancel @@ -217,6 +231,9 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U self.contentContainerNode.addSubnode(self.titleNode) self.contentContainerNode.addSubnode(self.cameraButton) self.contentContainerNode.addSubnode(self.screenButton) + if let broadcastPickerView = self.broadcastPickerView { + self.contentContainerNode.view.addSubview(broadcastPickerView) + } self.contentContainerNode.addSubnode(self.cancelButton) self.contentContainerNode.addSubnode(self.previewContainerNode) @@ -292,6 +309,16 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U transition.animateView({ self.bounds = targetBounds }) + + self.applicationStateDisposable = (self.context.sharedContext.applicationBindings.applicationIsActive + |> filter { !$0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.controller?.dismiss() + }) } func animateOut(completion: (() -> Void)? = nil) { @@ -390,6 +417,9 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U let screenButtonHeight = self.screenButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) transition.updateFrame(node: self.screenButton, frame: CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - 8.0 - screenButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: screenButtonHeight)) + if let broadcastPickerView = self.broadcastPickerView { + broadcastPickerView.frame = CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - 8.0 - screenButtonHeight - insets.bottom - 16.0, width: contentFrame.width + 1000.0, height: screenButtonHeight) + } let cancelButtonHeight = self.cancelButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) transition.updateFrame(node: self.cancelButton, frame: CGRect(x: buttonInset, y: contentHeight - cancelButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: cancelButtonHeight)) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 1f174c34cd..6be70413bf 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -3258,6 +3258,10 @@ public final class VoiceChatController: ViewController { }, switchCamera: { [weak self] in self?.call.switchVideoCamera() }, shareScreen: { [weak self] in + guard let strongSelf = self else { + return + } + self?.call.requestScreencast() }) strongSelf.controller?.present(controller, in: .window(.root)) diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 9b35f62a39..91b4141efd 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -501,7 +501,7 @@ final class SharedApplicationContext { initializeAccountManagement() - let applicationBindings = TelegramApplicationBindings(isMainApp: true, containerPath: appGroupUrl.path, appSpecificScheme: buildConfig.appSpecificUrlScheme, openUrl: { url in + let applicationBindings = TelegramApplicationBindings(isMainApp: true, appBundleId: baseAppBundleId, containerPath: appGroupUrl.path, appSpecificScheme: buildConfig.appSpecificUrlScheme, openUrl: { url in var parsedUrl = URL(string: url) if let parsed = parsedUrl { if parsed.scheme == nil || parsed.scheme!.isEmpty { @@ -793,7 +793,7 @@ final class SharedApplicationContext { }) var setPresentationCall: ((PresentationCall?) -> Void)? - let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, rootPath: rootPath, legacyBasePath: legacyBasePath, legacyCache: legacyCache, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), setNotificationCall: { call in + let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, rootPath: rootPath, legacyBasePath: legacyBasePath, legacyCache: legacyCache, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), setNotificationCall: { call in setPresentationCall?(call) }, navigateToChat: { accountId, peerId, messageId in self.openChatWhenReady(accountId: accountId, peerId: peerId, messageId: messageId) diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index abaf8b3c3a..200f8467b0 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -33,6 +33,7 @@ private func setupSharedLogger(rootPath: String, path: String) { } public struct NotificationViewControllerInitializationData { + public let appBundleId: String public let appGroupPath: String public let apiId: Int32 public let apiHash: String @@ -41,7 +42,8 @@ public struct NotificationViewControllerInitializationData { public let appVersion: String public let bundleData: Data? - public init(appGroupPath: String, apiId: Int32, apiHash: String, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?) { + public init(appBundleId: String, appGroupPath: String, apiId: Int32, apiHash: String, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?) { + self.appBundleId = appBundleId self.appGroupPath = appGroupPath self.apiId = apiId self.apiHash = apiHash @@ -103,7 +105,7 @@ public final class NotificationViewControllerImpl { }) semaphore.wait() - let applicationBindings = TelegramApplicationBindings(isMainApp: false, containerPath: self.initializationData.appGroupPath, appSpecificScheme: "tgapp", openUrl: { _ in + let applicationBindings = TelegramApplicationBindings(isMainApp: false, appBundleId: self.initializationData.appBundleId, containerPath: self.initializationData.appGroupPath, appSpecificScheme: "tgapp", openUrl: { _ in }, openUniversalUrl: { _, completion in completion.completion(false) return @@ -135,7 +137,7 @@ public final class NotificationViewControllerImpl { return nil }) - sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), rootPath: rootPath, legacyBasePath: nil, legacyCache: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) + sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), rootPath: rootPath, legacyBasePath: nil, legacyCache: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) presentationDataPromise.set(sharedAccountContext!.presentationData) } diff --git a/submodules/TelegramUI/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Sources/ShareExtensionContext.swift index e33b4a8d7f..29b1b991cd 100644 --- a/submodules/TelegramUI/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Sources/ShareExtensionContext.swift @@ -55,6 +55,7 @@ private enum ShareAuthorizationError { } public struct ShareRootControllerInitializationData { + public let appBundleId: String public let appGroupPath: String public let apiId: Int32 public let apiHash: String @@ -63,7 +64,8 @@ public struct ShareRootControllerInitializationData { public let appVersion: String public let bundleData: Data? - public init(appGroupPath: String, apiId: Int32, apiHash: String, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?) { + public init(appBundleId: String, appGroupPath: String, apiId: Int32, apiHash: String, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?) { + self.appBundleId = appBundleId self.appGroupPath = appGroupPath self.apiId = apiId self.apiHash = apiHash @@ -176,7 +178,7 @@ public class ShareRootControllerImpl { setupSharedLogger(rootPath: rootPath, path: logsPath) - let applicationBindings = TelegramApplicationBindings(isMainApp: false, containerPath: self.initializationData.appGroupPath, appSpecificScheme: "tg", openUrl: { _ in + let applicationBindings = TelegramApplicationBindings(isMainApp: false, appBundleId: self.initializationData.appBundleId, containerPath: self.initializationData.appGroupPath, appSpecificScheme: "tg", openUrl: { _ in }, openUniversalUrl: { _, completion in completion.completion(false) return @@ -230,7 +232,7 @@ public class ShareRootControllerImpl { return nil }) - let sharedContext = SharedAccountContextImpl(mainWindow: nil, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), rootPath: rootPath, legacyBasePath: nil, legacyCache: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) + let sharedContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), rootPath: rootPath, legacyBasePath: nil, legacyCache: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) presentationDataPromise.set(sharedContext.presentationData) internalContext = InternalContext(sharedContext: sharedContext) globalInternalContext = internalContext diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 61bed30c32..d1e543cb46 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -55,6 +55,7 @@ private var testHasInstance = false public final class SharedAccountContextImpl: SharedAccountContext { public let mainWindow: Window1? public let applicationBindings: TelegramApplicationBindings + public let sharedContainerPath: String public let basePath: String public let accountManager: AccountManager public let appLockContext: AppLockContext @@ -160,7 +161,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { private var spotlightDataContext: SpotlightDataContext? private var widgetDataContext: WidgetDataContext? - public init(mainWindow: Window1?, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, rootPath: String, legacyBasePath: String?, legacyCache: LegacyCache?, apsNotificationToken: Signal, voipNotificationToken: Signal, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }) { + public init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, rootPath: String, legacyBasePath: String?, legacyCache: LegacyCache?, apsNotificationToken: Signal, voipNotificationToken: Signal, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }) { assert(Queue.mainQueue().isCurrent()) precondition(!testHasInstance) @@ -168,6 +169,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.mainWindow = mainWindow self.applicationBindings = applicationBindings + self.sharedContainerPath = sharedContainerPath self.basePath = basePath self.accountManager = accountManager self.navigateToChatImpl = navigateToChat diff --git a/submodules/TelegramVoip/Sources/IpcGroupCallContext.swift b/submodules/TelegramVoip/Sources/IpcGroupCallContext.swift new file mode 100644 index 0000000000..3820f40490 --- /dev/null +++ b/submodules/TelegramVoip/Sources/IpcGroupCallContext.swift @@ -0,0 +1,981 @@ +import Foundation +import SwiftSignalKit +import CoreMedia + +private struct PayloadDescription: Codable { + var id: UInt32 + var timestamp: Int32 +} + +private struct JoinPayload: Codable { + var id: UInt32 + var string: String +} + +private struct JoinResponsePayload: Codable { + var id: UInt32 + var string: String +} + +private struct KeepaliveInfo: Codable { + var id: UInt32 + var timestamp: Int32 +} + +private let checkInterval: Double = 0.2 +private let keepaliveTimeout: Double = 2.0 + +private func payloadDescriptionPath(basePath: String) -> String { + return basePath + "/currentPayloadDescription.json" +} + +private func joinPayloadPath(basePath: String) -> String { + return basePath + "/joinPayload.json" +} + +private func joinResponsePayloadPath(basePath: String) -> String { + return basePath + "/joinResponsePayload.json" +} + +private func keepaliveInfoPath(basePath: String) -> String { + return basePath + "/keepaliveInfo.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 + var offset: Int = 0 + + init(count: Int) { + self.data = Data(bytesNoCopy: malloc(count)!, count: count, deallocator: .free) + } + } + + private let queue: Queue + let fd: Int32 + private let didRead: ((Data) -> Void)? + private let channel: DispatchSourceRead + + private var currendData: PendingData? + + init(queue: Queue, fd: Int32, didRead: ((Data) -> Void)?) { + assert(queue.isCurrent()) + self.queue = queue + self.fd = fd + self.didRead = didRead + + self.channel = DispatchSource.makeReadSource(fileDescriptor: fd, queue: queue.queue) + self.channel.setEventHandler(handler: { [weak self] in + guard let strongSelf = self else { + return + } + + while true { + if let currendData = strongSelf.currendData { + let offset = currendData.offset + let count = currendData.data.count - offset + let bytesRead = currendData.data.withUnsafeMutableBytes { bytes -> Int in + return Darwin.read(fd, bytes.baseAddress!.advanced(by: offset), min(8129, count)) + } + if bytesRead <= 0 { + break + } else { + currendData.offset += bytesRead + if currendData.offset == currendData.data.count { + strongSelf.currendData = nil + strongSelf.didRead?(currendData.data) + } + } + } else { + var length: Int32 = 0 + let bytesRead = read(fd, &length, 4) + if bytesRead < 0 { + break + } else { + assert(bytesRead == 4) + assert(length > 0 && length <= 30 * 1024 * 1024) + strongSelf.currendData = PendingData(count: Int(length)) + } + } + } + }) + self.channel.resume() + } + + deinit { + assert(self.queue.isCurrent()) + self.channel.cancel() + } +} + +private final class FdWriteConnection { + private final class PendingData { + let data: Data + var didWriteHeader: Bool = false + var offset: Int = 0 + + init(data: Data) { + self.data = data + } + } + + private let queue: Queue + let fd: Int32 + private let channel: DispatchSourceWrite + private var isResumed = false + + private let bufferSize: Int + private let buffer: UnsafeMutableRawPointer + + private var currentData: PendingData? + private var nextData: Data? + + init(queue: Queue, fd: Int32) { + assert(queue.isCurrent()) + self.queue = queue + self.fd = fd + + self.bufferSize = 8192 + self.buffer = malloc(self.bufferSize) + + self.channel = DispatchSource.makeWriteSource(fileDescriptor: fd, queue: queue.queue) + self.channel.setEventHandler(handler: { [weak self] in + guard let strongSelf = self else { + return + } + + while true { + if let currentData = strongSelf.currentData { + if !currentData.didWriteHeader { + var length: Int32 = Int32(currentData.data.count) + let writtenBytes = Darwin.write(fd, &length, 4) + if writtenBytes > 0 { + assert(writtenBytes == 4) + currentData.didWriteHeader = true + } else { + strongSelf.channel.suspend() + strongSelf.isResumed = false + break + } + } else { + let offset = currentData.offset + let count = currentData.data.count - offset + let writtenBytes = currentData.data.withUnsafeBytes { bytes -> Int in + return Darwin.write(fd, bytes.baseAddress!.advanced(by: offset), min(count, strongSelf.bufferSize)) + } + if writtenBytes > 0 { + currentData.offset += writtenBytes + if currentData.offset == currentData.data.count { + strongSelf.currentData = nil + + if let nextData = strongSelf.nextData { + strongSelf.nextData = nil + strongSelf.currentData = PendingData(data: nextData) + } else { + strongSelf.channel.suspend() + strongSelf.isResumed = false + break + } + } + } else { + strongSelf.channel.suspend() + strongSelf.isResumed = false + break + } + } + } else { + strongSelf.channel.suspend() + strongSelf.isResumed = false + break + } + } + }) + } + + deinit { + assert(self.queue.isCurrent()) + + if !self.isResumed { + self.channel.resume() + } + self.channel.cancel() + + free(self.buffer) + } + + func replaceData(data: Data) { + if self.currentData == nil { + self.currentData = PendingData(data: data) + } else { + self.nextData = data + } + + if !self.isResumed { + self.isResumed = true + self.channel.resume() + } + } +} + +private final class NamedPipeReaderImpl { + private let queue: Queue + private var connection: FdReadConnection? + + init(queue: Queue, path: String, didRead: @escaping (Data) -> Void) { + self.queue = queue + + unlink(path) + mkfifo(path, 0o666) + let fd = open(path, O_RDONLY | O_NONBLOCK, S_IRUSR | S_IWUSR) + if fd != -1 { + self.connection = FdReadConnection(queue: self.queue, fd: fd, didRead: { data in + didRead(data) + }) + } + } +} + +private final class NamedPipeReader { + private let queue = Queue() + let impl: QueueLocalObject + + init(path: String, didRead: @escaping (Data) -> Void) { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return NamedPipeReaderImpl(queue: queue, path: path, didRead: didRead) + }) + } +} + +private final class NamedPipeWriterImpl { + private let queue: Queue + private var connection: FdWriteConnection? + + init(queue: Queue, path: String) { + self.queue = queue + + let fd = open(path, O_WRONLY | O_NONBLOCK, S_IRUSR | S_IWUSR) + if fd != -1 { + self.connection = FdWriteConnection(queue: self.queue, fd: fd) + } + } + + func replaceData(data: Data) { + guard let connection = self.connection else { + return + } + connection.replaceData(data: data) + } +} + +private final class NamedPipeWriter { + private let queue = Queue() + private let impl: QueueLocalObject + + init(path: String) { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return NamedPipeWriterImpl(queue: queue, path: path) + }) + } + + func replaceData(data: Data) { + self.impl.with { impl in + impl.replaceData(data: data) + } + } +} + +private final class MappedFile { + let path: String + private var handle: Int32 + private var currentSize: Int + private(set) var memory: UnsafeMutableRawPointer + + init?(path: String, createIfNotExists: Bool) { + self.path = path + + var flags: Int32 = O_RDWR | O_APPEND + if createIfNotExists { + flags |= O_CREAT + } + self.handle = open(path, flags, S_IRUSR | S_IWUSR) + + if self.handle < 0 { + return nil + } + + var value = stat() + stat(path, &value) + self.currentSize = Int(value.st_size) + + self.memory = mmap(nil, self.currentSize, PROT_READ | PROT_WRITE, MAP_SHARED, self.handle, 0) + } + + deinit { + munmap(self.memory, self.currentSize) + close(self.handle) + } + + var size: Int { + get { + return self.currentSize + } set(value) { + if value != self.currentSize { + munmap(self.memory, self.currentSize) + ftruncate(self.handle, off_t(value)) + self.currentSize = value + self.memory = mmap(nil, self.currentSize, PROT_READ | PROT_WRITE, MAP_SHARED, self.handle, 0) + } + } + } + + func synchronize() { + msync(self.memory, self.currentSize, MS_ASYNC) + } + + func write(at range: Range, from data: UnsafeRawPointer) { + memcpy(self.memory.advanced(by: range.lowerBound), data, range.count) + } + + func read(at range: Range, to data: UnsafeMutableRawPointer) { + memcpy(data, self.memory.advanced(by: range.lowerBound), range.count) + } + + func clear() { + memset(self.memory, 0, self.currentSize) + } +} + +public final class IpcGroupCallBufferAppContext { + private let basePath: String + private let server: NamedPipeReader + + private let id: UInt32 + + private let isActivePromise = ValuePromise(false, ignoreRepeated: true) + public var isActive: Signal { + return self.isActivePromise.get() + } + private var isActiveCheckTimer: SwiftSignalKit.Timer? + + private let framesPipe = ValuePipe() + public var frames: Signal { + return self.framesPipe.signal() + } + + private var framePollTimer: SwiftSignalKit.Timer? + private var mappedFile: MappedFile? + + private var callActiveInfoTimer: SwiftSignalKit.Timer? + + public init(basePath: String) { + self.basePath = basePath + let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) + + self.id = UInt32.random(in: 0 ..< UInt32.max) + + let framesPipe = self.framesPipe + self.server = NamedPipeReader(path: broadcastAppSocketPath(basePath: basePath), didRead: { data in + //framesPipe.putNext(data) + }) + + let dataPath = broadcastAppSocketPath(basePath: basePath) + "-data-\(self.id)" + + if let mappedFile = MappedFile(path: dataPath, createIfNotExists: true) { + self.mappedFile = mappedFile + if mappedFile.size < 10 * 1024 * 1024 { + mappedFile.size = 10 * 1024 * 1024 + } + } + + let framePollTimer = SwiftSignalKit.Timer(timeout: 1.0 / 30.0, repeat: true, completion: { [weak self] in + guard let strongSelf = self, let mappedFile = strongSelf.mappedFile else { + return + } + + let data = Data(bytesNoCopy: mappedFile.memory, count: mappedFile.size, deallocator: .none) + if let frame = deserializePixelBuffer(data: data) { + strongSelf.framesPipe.putNext(frame) + } + }, queue: .mainQueue()) + self.framePollTimer = framePollTimer + framePollTimer.start() + + self.updateCallIsActive() + + let callActiveInfoTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + self?.updateCallIsActive() + }, queue: .mainQueue()) + self.callActiveInfoTimer = callActiveInfoTimer + callActiveInfoTimer.start() + + let isActiveCheckTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + self?.updateKeepaliveInfo() + }, queue: .mainQueue()) + self.isActiveCheckTimer = isActiveCheckTimer + isActiveCheckTimer.start() + } + + deinit { + self.framePollTimer?.invalidate() + self.callActiveInfoTimer?.invalidate() + self.isActiveCheckTimer?.invalidate() + if let mappedFile = self.mappedFile { + self.mappedFile = nil + let _ = try? FileManager.default.removeItem(atPath: mappedFile.path) + } + } + + private func updateCallIsActive() { + let timestamp = Int32(Date().timeIntervalSince1970) + let payloadDescription = PayloadDescription( + id: self.id, + timestamp: timestamp + ) + guard let payloadDescriptionData = try? JSONEncoder().encode(payloadDescription) else { + return + } + guard let _ = try? payloadDescriptionData.write(to: URL(fileURLWithPath: payloadDescriptionPath(basePath: self.basePath)), options: .atomic) else { + return + } + } + + private func updateKeepaliveInfo() { + 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.id { + 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 IpcGroupCallBufferBroadcastContext { + public enum Status { + case finished + } + + private let basePath: String + private let client: NamedPipeWriter + private var timer: SwiftSignalKit.Timer? + + private let statusPromise = Promise() + public var status: Signal { + return self.statusPromise.get() + } + + private var mappedFile: MappedFile? + private var currentId: UInt32? + + private var callActiveInfoTimer: SwiftSignalKit.Timer? + + private var keepaliveInfoTimer: SwiftSignalKit.Timer? + + public init(basePath: String) { + self.basePath = basePath + let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) + + self.client = NamedPipeWriter(path: broadcastAppSocketPath(basePath: basePath)) + + let callActiveInfoTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + self?.updateCallIsActive() + }, queue: .mainQueue()) + self.callActiveInfoTimer = callActiveInfoTimer + callActiveInfoTimer.start() + } + + deinit { + self.endActiveIndication() + + self.callActiveInfoTimer?.invalidate() + self.keepaliveInfoTimer?.invalidate() + } + + private func updateCallIsActive() { + let filePath = payloadDescriptionPath(basePath: self.basePath) + guard let payloadDescriptionData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + self.statusPromise.set(.single(.finished)) + return + } + + guard let payloadDescription = try? JSONDecoder().decode(PayloadDescription.self, from: payloadDescriptionData) else { + self.statusPromise.set(.single(.finished)) + return + } + let timestamp = Int32(Date().timeIntervalSince1970) + if payloadDescription.timestamp < timestamp - 4 { + self.statusPromise.set(.single(.finished)) + return + } + + if let currentId = self.currentId { + if currentId != payloadDescription.id { + self.statusPromise.set(.single(.finished)) + } + } else { + self.currentId = payloadDescription.id + + let dataPath = broadcastAppSocketPath(basePath: basePath) + "-data-\(payloadDescription.id)" + + if let mappedFile = MappedFile(path: dataPath, createIfNotExists: false) { + self.mappedFile = mappedFile + if mappedFile.size < 10 * 1024 * 1024 { + mappedFile.size = 10 * 1024 * 1024 + } + } + + self.writeKeepaliveInfo() + + let keepaliveInfoTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + self?.writeKeepaliveInfo() + }, queue: .mainQueue()) + self.keepaliveInfoTimer = keepaliveInfoTimer + keepaliveInfoTimer.start() + } + } + + public func setCurrentFrame(data: Data) { + //let _ = try? data.write(to: URL(fileURLWithPath: dataPath), options: []) + + if let mappedFile = self.mappedFile, mappedFile.size >= data.count { + let _ = data.withUnsafeBytes { bytes in + memcpy(mappedFile.memory, bytes.baseAddress!, data.count) + } + } + + //self.client.replaceData(data: data) + } + + 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)) + } +} + +public func serializePixelBuffer(buffer: CVPixelBuffer) -> Data? { + let pixelFormat = CVPixelBufferGetPixelFormatType(buffer) + switch pixelFormat { + case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange: + let status = CVPixelBufferLockBaseAddress(buffer, .readOnly) + if status != kCVReturnSuccess { + return nil + } + defer { + CVPixelBufferUnlockBaseAddress(buffer, .readOnly) + } + + let width = CVPixelBufferGetWidth(buffer) + let height = CVPixelBufferGetHeight(buffer) + + guard let yPlane = CVPixelBufferGetBaseAddressOfPlane(buffer, 0) else { + return nil + } + let yStride = CVPixelBufferGetBytesPerRowOfPlane(buffer, 0) + let yPlaneSize = yStride * height + + guard let uvPlane = CVPixelBufferGetBaseAddressOfPlane(buffer, 1) else { + return nil + } + let uvStride = CVPixelBufferGetBytesPerRowOfPlane(buffer, 1) + let uvPlaneSize = uvStride * (height / 2) + + let headerSize: Int = 4 + 4 + 4 + 4 + 4 + + let dataSize = headerSize + yPlaneSize + uvPlaneSize + let resultBytes = malloc(dataSize)! + + var pixelFormatValue = pixelFormat + memcpy(resultBytes.advanced(by: 0), &pixelFormatValue, 4) + var widthValue = Int32(width) + memcpy(resultBytes.advanced(by: 4), &widthValue, 4) + var heightValue = Int32(height) + memcpy(resultBytes.advanced(by: 4 + 4), &heightValue, 4) + var yStrideValue = Int32(yStride) + memcpy(resultBytes.advanced(by: 4 + 4 + 4), &yStrideValue, 4) + var uvStrideValue = Int32(uvStride) + memcpy(resultBytes.advanced(by: 4 + 4 + 4 + 4), &uvStrideValue, 4) + + memcpy(resultBytes.advanced(by: headerSize), yPlane, yPlaneSize) + memcpy(resultBytes.advanced(by: headerSize + yPlaneSize), uvPlane, uvPlaneSize) + + return Data(bytesNoCopy: resultBytes, count: dataSize, deallocator: .free) + default: + return nil + } +} + +public func deserializePixelBuffer(data: Data) -> CVPixelBuffer? { + if data.count < 4 + 4 + 4 + 4 { + return nil + } + let count = data.count + return data.withUnsafeBytes { bytes -> CVPixelBuffer? in + let dataBytes = bytes.baseAddress! + + var pixelFormat: UInt32 = 0 + memcpy(&pixelFormat, dataBytes.advanced(by: 0), 4) + + switch pixelFormat { + case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange: + break + default: + return nil + } + + var width: Int32 = 0 + memcpy(&width, dataBytes.advanced(by: 4), 4) + var height: Int32 = 0 + memcpy(&height, dataBytes.advanced(by: 4 + 4), 4) + var yStride: Int32 = 0 + memcpy(&yStride, dataBytes.advanced(by: 4 + 4 + 4), 4) + var uvStride: Int32 = 0 + memcpy(&uvStride, dataBytes.advanced(by: 4 + 4 + 4 + 4), 4) + + if width < 0 || width > 8192 { + return nil + } + if height < 0 || height > 8192 { + return nil + } + + let headerSize: Int = 4 + 4 + 4 + 4 + 4 + + let yPlaneSize = Int(yStride * height) + let uvPlaneSize = Int(uvStride * height / 2) + let dataSize = headerSize + yPlaneSize + uvPlaneSize + + if dataSize > count { + return nil + } + + var buffer: CVPixelBuffer? = nil + CVPixelBufferCreate(nil, Int(width), Int(height), pixelFormat, nil, &buffer) + if let buffer = buffer { + let status = CVPixelBufferLockBaseAddress(buffer, []) + if status != kCVReturnSuccess { + return nil + } + defer { + CVPixelBufferUnlockBaseAddress(buffer, []) + } + + guard let destYPlane = CVPixelBufferGetBaseAddressOfPlane(buffer, 0) else { + return nil + } + let destYStride = CVPixelBufferGetBytesPerRowOfPlane(buffer, 0) + if destYStride != Int(yStride) { + return nil + } + + guard let destUvPlane = CVPixelBufferGetBaseAddressOfPlane(buffer, 1) else { + return nil + } + let destUvStride = CVPixelBufferGetBytesPerRowOfPlane(buffer, 1) + if destUvStride != Int(uvStride) { + return nil + } + + memcpy(destYPlane, dataBytes.advanced(by: headerSize), yPlaneSize) + memcpy(destUvPlane, dataBytes.advanced(by: headerSize + yPlaneSize), uvPlaneSize) + + return buffer + } else { + return nil + } + } +} diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index 98e71dc325..7606426e38 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -335,8 +335,12 @@ extension OngoingCallThreadLocalContext: OngoingCallThreadLocalContextProtocol { public final class OngoingCallVideoCapturer { internal let impl: OngoingCallThreadLocalContextVideoCapturer - public init(keepLandscape: Bool = false) { - self.impl = OngoingCallThreadLocalContextVideoCapturer(deviceId: "", keepLandscape: keepLandscape) + public init(keepLandscape: Bool = false, isCustom: Bool = false) { + if isCustom { + self.impl = OngoingCallThreadLocalContextVideoCapturer.withExternalSampleBufferProvider() + } else { + self.impl = OngoingCallThreadLocalContextVideoCapturer(deviceId: "", keepLandscape: keepLandscape) + } } public func switchVideoInput(isFront: Bool) { @@ -383,6 +387,14 @@ public final class OngoingCallVideoCapturer { public func setIsVideoEnabled(_ value: Bool) { self.impl.setIsVideoEnabled(value) } + + public func injectSampleBuffer(_ sampleBuffer: CMSampleBuffer) { + self.impl.submitSampleBuffer(sampleBuffer) + } + + public func injectPixelBuffer(_ pixelBuffer: CVPixelBuffer) { + self.impl.submitPixelBuffer(pixelBuffer) + } } extension OngoingCallThreadLocalContextWebrtc: OngoingCallThreadLocalContextProtocol { diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index a57eccd559..0b389edfc9 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -5,6 +5,7 @@ #if TARGET_OS_IOS #import +#import #else #import #define UIView NSView @@ -110,11 +111,20 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) { - (instancetype _Nonnull)initWithDeviceId:(NSString * _Nonnull)deviceId keepLandscape:(bool)keepLandscape; +#if TARGET_OS_IOS ++ (instancetype _Nonnull)capturerWithExternalSampleBufferProvider; +#endif + - (void)switchVideoInput:(NSString * _Nonnull)deviceId; - (void)setIsVideoEnabled:(bool)isVideoEnabled; - (void)makeOutgoingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion; +#if TARGET_OS_IOS +- (void)submitSampleBuffer:(CMSampleBufferRef _Nonnull)sampleBuffer; +- (void)submitPixelBuffer:(CVPixelBufferRef _Nonnull)pixelBuffer; +#endif + @end @interface OngoingCallThreadLocalContextWebrtc : NSObject diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index ddb753346f..57ac098144 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -25,6 +25,9 @@ #import "group/GroupInstanceImpl.h" #import "group/GroupInstanceCustomImpl.h" +#import "VideoCaptureInterfaceImpl.h" +#import "platform/darwin/CustomExternalCapturer.h" + @implementation OngoingCallConnectionDescriptionWebrtc - (instancetype _Nonnull)initWithConnectionId:(int64_t)connectionId hasStun:(bool)hasStun hasTurn:(bool)hasTurn ip:(NSString * _Nonnull)ip port:(int32_t)port username:(NSString * _Nonnull)username password:(NSString * _Nonnull)password { @@ -144,12 +147,22 @@ @interface OngoingCallThreadLocalContextVideoCapturer () { bool _keepLandscape; + std::shared_ptr> _croppingBuffer; } @end @implementation OngoingCallThreadLocalContextVideoCapturer +- (instancetype _Nonnull)initWithInterface:(std::shared_ptr)interface { + self = [super init]; + if (self != nil) { + _interface = interface; + _croppingBuffer = std::make_shared>(); + } + return self; +} + - (instancetype _Nonnull)initWithDeviceId:(NSString * _Nonnull)deviceId keepLandscape:(bool)keepLandscape { self = [super init]; if (self != nil) { @@ -164,10 +177,55 @@ return self; } +#if TARGET_OS_IOS + +tgcalls::VideoCaptureInterfaceObject *GetVideoCaptureAssumingSameThread(tgcalls::VideoCaptureInterface *videoCapture) { + return videoCapture + ? static_cast(videoCapture)->object()->getSyncAssumingSameThread() + : nullptr; +} + ++ (instancetype _Nonnull)capturerWithExternalSampleBufferProvider { + std::shared_ptr interface = tgcalls::VideoCaptureInterface::Create(tgcalls::StaticThreads::getThreads(), ":ios_custom"); + return [[OngoingCallThreadLocalContextVideoCapturer alloc] initWithInterface:interface]; +} +#endif - (void)dealloc { } +#if TARGET_OS_IOS +- (void)submitSampleBuffer:(CMSampleBufferRef _Nonnull)sampleBuffer { + if (!sampleBuffer) { + return; + } + tgcalls::StaticThreads::getThreads()->getMediaThread()->PostTask(RTC_FROM_HERE, [interface = _interface, sampleBuffer = CFRetain(sampleBuffer)]() { + auto capture = GetVideoCaptureAssumingSameThread(interface.get()); + auto source = capture->source(); + if (source) { + [CustomExternalCapturer passSampleBuffer:(CMSampleBufferRef)sampleBuffer toSource:source]; + } + CFRelease(sampleBuffer); + }); +} + +- (void)submitPixelBuffer:(CVPixelBufferRef _Nonnull)pixelBuffer { + if (!pixelBuffer) { + return; + } + + tgcalls::StaticThreads::getThreads()->getMediaThread()->PostTask(RTC_FROM_HERE, [interface = _interface, pixelBuffer = CFRetain(pixelBuffer), croppingBuffer = _croppingBuffer]() { + auto capture = GetVideoCaptureAssumingSameThread(interface.get()); + auto source = capture->source(); + if (source) { + [CustomExternalCapturer passPixelBuffer:(CVPixelBufferRef)pixelBuffer toSource:source croppingBuffer:*croppingBuffer]; + } + CFRelease(pixelBuffer); + }); +} + +#endif + - (void)switchVideoInput:(NSString * _Nonnull)deviceId { std::string resolvedId = deviceId.UTF8String; if (_keepLandscape) { diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 94a9c7b4e4..5e0019224e 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 94a9c7b4e49c943d1ca108e35779739ad99d695a +Subproject commit 5e0019224eaf9fb8ae432f2b39c84a591f59a5da