mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Implement screencasting
This commit is contained in:
parent
e0afab1f4a
commit
549e3642d5
@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
|
192
Telegram/BroadcastUpload/BroadcastUploadExtension.swift
Normal file
192
Telegram/BroadcastUpload/BroadcastUploadExtension.swift
Normal 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)*/
|
||||
}
|
||||
}
|
@ -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
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
@ -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>
|
@ -1 +0,0 @@
|
||||
|
@ -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>
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
#ifndef Widget_Bridging_Header_h
|
||||
#define Widget_Bridging_Header_h
|
||||
|
||||
#endif
|
@ -1 +0,0 @@
|
||||
"CFBundleDisplayName" = "الأشخاص";
|
@ -1 +0,0 @@
|
||||
"CFBundleDisplayName" = "Leute";
|
@ -1 +0,0 @@
|
||||
"CFBundleDisplayName" = "People";
|
@ -1,2 +0,0 @@
|
||||
"Widget.NoUsers" = "No users here yet...";
|
||||
"Widget.AuthRequired" = "Open Telegram and log in.";
|
@ -1 +0,0 @@
|
||||
"CFBundleDisplayName" = "Personas";
|
@ -1 +0,0 @@
|
||||
"CFBundleDisplayName" = "Persone";
|
@ -1 +0,0 @@
|
||||
"CFBundleDisplayName" = "사람";
|
@ -1 +0,0 @@
|
||||
"CFBundleDisplayName" = "Mensen";
|
@ -1 +0,0 @@
|
||||
"CFBundleDisplayName" = "Pessoas";
|
@ -1 +0,0 @@
|
||||
"CFBundleDisplayName" = "Люди";
|
@ -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 }
|
||||
|
@ -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) {
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
981
submodules/TelegramVoip/Sources/IpcGroupCallContext.swift
Normal file
981
submodules/TelegramVoip/Sources/IpcGroupCallContext.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user