Implement screencasting

This commit is contained in:
Ali 2021-05-04 22:29:03 +04:00
parent e0afab1f4a
commit 549e3642d5
33 changed files with 1461 additions and 462 deletions

View File

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

View File

@ -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[..<lastDotRange.lowerBound])
let appGroupName = "group.\(baseAppBundleId)"
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
guard let appGroupUrl = maybeAppGroupUrl else {
self.finishWithGenericError()
return
}
let rootPath = rootPathForBasePath(appGroupUrl.path)
let logsPath = rootPath + "/broadcast-logs"
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
let screencastBufferClientContext = IpcGroupCallBufferBroadcastContext(basePath: rootPath + "/broadcast-coordination")
self.screencastBufferClientContext = screencastBufferClientContext
self.statusDisposable = (screencastBufferClientContext.status
|> 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)*/
}
}

View File

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

View File

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

View File

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>${APP_NAME}</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>$(PRODUCT_BUNDLE_SHORT_VERSION)</string>
<key>CFBundleVersion</key>
<string>${BUILD_NUMBER}</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>IntentsRestrictedWhileLocked</key>
<array/>
<key>IntentsRestrictedWhileProtectedDataUnavailable</key>
<array/>
<key>IntentsSupported</key>
<array>
<string>INSendMessageIntent</string>
<string>INStartAudioCallIntent</string>
<string>INSearchForMessagesIntent</string>
<string>INSetMessageAttributeIntent</string>
<string>INSearchCallHistoryIntent</string>
</array>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.intents-service</string>
<key>NSExtensionPrincipalClass</key>
<string>IntentHandler</string>
</dict>
</dict>
</plist>

View File

@ -1 +0,0 @@

View File

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>${APP_NAME}</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>$(PRODUCT_BUNDLE_SHORT_VERSION)</string>
<key>CFBundleVersion</key>
<string>${BUILD_NUMBER}</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widget-extension</string>
<key>NSExtensionPrincipalClass</key>
<string>TodayViewController</string>
</dict>
</dict>
</plist>

View File

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

View File

@ -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[..<lastDotRange.lowerBound])
let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId)
self.buildConfig = buildConfig
let appGroupName = "group.\(baseAppBundleId)"
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
guard let appGroupUrl = maybeAppGroupUrl else {
return
}
let rootPath = rootPathForBasePath(appGroupUrl.path)
let presentationData: WidgetPresentationData
if let data = try? Data(contentsOf: URL(fileURLWithPath: widgetPresentationDataPath(rootPath: rootPath))), let value = try? JSONDecoder().decode(WidgetPresentationData.self, from: data) {
presentationData = value
} else {
presentationData = WidgetPresentationData(applicationLockedString: "Unlock the app to use the widget", applicationStartRequiredString: "Open the app to use the widget", widgetGalleryTitle: "", widgetGalleryDescription: "")
}
if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) {
self.setPlaceholderText(presentationData.applicationLockedString)
return
}
if self.initializedInterface {
return
}
self.initializedInterface = true
let dataPath = rootPath + "/widget-data"
if let data = try? Data(contentsOf: URL(fileURLWithPath: dataPath)), let widgetData = try? JSONDecoder().decode(WidgetData.self, from: data) {
self.setWidgetData(widgetData: widgetData, presentationData: presentationData)
}
}
private func setPlaceholderText(_ text: String) {
let fontSize = UIFont.preferredFont(forTextStyle: .body).pointSize
let placeholderLabel = UILabel()
if #available(iOSApplicationExtension 13.0, *) {
placeholderLabel.textColor = UIColor.label
} else {
placeholderLabel.textColor = self.primaryColor
}
placeholderLabel.font = UIFont.systemFont(ofSize: fontSize)
placeholderLabel.text = text
placeholderLabel.sizeToFit()
self.placeholderLabel = placeholderLabel
self.view.addSubview(placeholderLabel)
}
func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> 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
}
}
}

View File

@ -1,4 +0,0 @@
#ifndef Widget_Bridging_Header_h
#define Widget_Bridging_Header_h
#endif

View File

@ -1 +0,0 @@
"CFBundleDisplayName" = "الأشخاص";

View File

@ -1 +0,0 @@
"CFBundleDisplayName" = "Leute";

View File

@ -1 +0,0 @@
"CFBundleDisplayName" = "People";

View File

@ -1,2 +0,0 @@
"Widget.NoUsers" = "No users here yet...";
"Widget.AuthRequired" = "Open Telegram and log in.";

View File

@ -1 +0,0 @@
"CFBundleDisplayName" = "Personas";

View File

@ -1 +0,0 @@
"CFBundleDisplayName" = "Persone";

View File

@ -1 +0,0 @@
"CFBundleDisplayName" = "사람";

View File

@ -1 +0,0 @@
"CFBundleDisplayName" = "Mensen";

View File

@ -1 +0,0 @@
"CFBundleDisplayName" = "Pessoas";

View File

@ -1 +0,0 @@
"CFBundleDisplayName" = "Люди";

View File

@ -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<Bool, NoError>, applicationIsActive: Signal<Bool, NoError>, 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<Bool, NoError>, applicationIsActive: Signal<Bool, NoError>, 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 }

View File

@ -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<UInt32>()
@ -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<String>>(Set())
public var incomingVideoSources: Signal<Set<String>, 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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, 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<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, 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

View File

@ -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<String>()
public var joinPayload: Signal<String, NoError> {
return self.joinPayloadPromise.get()
}
private var joinPayloadCheckTimer: SwiftSignalKit.Timer?
private let isActivePromise = ValuePromise<Bool>(false, ignoreRepeated: true)
public var isActive: Signal<Bool, NoError> {
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<Request>()
public var request: Signal<Request, NoError> {
return self.requestPromise.get()
}
private var joinResponsePayloadCheckTimer: SwiftSignalKit.Timer?
private let joinResponsePayloadPromise = Promise<String>()
public var joinResponsePayload: Signal<String, NoError> {
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<NamedPipeReaderImpl>
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<NamedPipeWriterImpl>
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<Int>, from data: UnsafeRawPointer) {
memcpy(self.memory.advanced(by: range.lowerBound), data, range.count)
}
func read(at range: Range<Int>, 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<Bool>(false, ignoreRepeated: true)
public var isActive: Signal<Bool, NoError> {
return self.isActivePromise.get()
}
private var isActiveCheckTimer: SwiftSignalKit.Timer?
private let framesPipe = ValuePipe<CVPixelBuffer>()
public var frames: Signal<CVPixelBuffer, NoError> {
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<Status>()
public var status: Signal<Status, NoError> {
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
}
}
}

View File

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

View File

@ -5,6 +5,7 @@
#if TARGET_OS_IOS
#import <UIKit/UIKit.h>
#import <CoreMedia/CoreMedia.h>
#else
#import <AppKit/AppKit.h>
#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<OngoingCallThreadLocalContextWebrtcVideoView> * _Nullable))completion;
#if TARGET_OS_IOS
- (void)submitSampleBuffer:(CMSampleBufferRef _Nonnull)sampleBuffer;
- (void)submitPixelBuffer:(CVPixelBufferRef _Nonnull)pixelBuffer;
#endif
@end
@interface OngoingCallThreadLocalContextWebrtc : NSObject

View File

@ -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<std::vector<uint8_t>> _croppingBuffer;
}
@end
@implementation OngoingCallThreadLocalContextVideoCapturer
- (instancetype _Nonnull)initWithInterface:(std::shared_ptr<tgcalls::VideoCaptureInterface>)interface {
self = [super init];
if (self != nil) {
_interface = interface;
_croppingBuffer = std::make_shared<std::vector<uint8_t>>();
}
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<tgcalls::VideoCaptureInterfaceImpl*>(videoCapture)->object()->getSyncAssumingSameThread()
: nullptr;
}
+ (instancetype _Nonnull)capturerWithExternalSampleBufferProvider {
std::shared_ptr<tgcalls::VideoCaptureInterface> 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) {

@ -1 +1 @@
Subproject commit 94a9c7b4e49c943d1ca108e35779739ad99d695a
Subproject commit 5e0019224eaf9fb8ae432f2b39c84a591f59a5da