mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Call improvements
This commit is contained in:
parent
a38e2e41ed
commit
d33e03b64c
@ -25,6 +25,7 @@ swift_library(
|
||||
"//submodules/GZip:GZip",
|
||||
"//third-party/ZipArchive:ZipArchive",
|
||||
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
|
||||
"//submodules/TelegramVoip",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -16,6 +16,7 @@ import AppBundle
|
||||
import ZipArchive
|
||||
import WebKit
|
||||
import InAppPurchaseManager
|
||||
import TelegramVoip
|
||||
|
||||
@objc private final class DebugControllerMailComposeDelegate: NSObject, MFMailComposeViewControllerDelegate {
|
||||
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
@ -104,8 +105,8 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
case disableReloginTokens(Bool)
|
||||
case disableCallV2(Bool)
|
||||
case experimentalCallMute(Bool)
|
||||
case liveStreamV2(Bool)
|
||||
case dynamicStreaming(Bool)
|
||||
case autoBenchmarkReflectors(Bool)
|
||||
case benchmarkReflectors
|
||||
case enableLocalTranslation(Bool)
|
||||
case preferredVideoCodec(Int, String, String?, Bool)
|
||||
case disableVideoAspectScaling(Bool)
|
||||
@ -131,7 +132,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return DebugControllerSection.web.rawValue
|
||||
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .disableCallV2, .experimentalCallMute, .liveStreamV2, .dynamicStreaming, .enableLocalTranslation:
|
||||
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .disableCallV2, .experimentalCallMute, .autoBenchmarkReflectors, .benchmarkReflectors, .enableLocalTranslation:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .logTranslationRecognition, .resetTranslationStates:
|
||||
return DebugControllerSection.translation.rawValue
|
||||
@ -248,9 +249,9 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return 51
|
||||
case .experimentalCallMute:
|
||||
return 52
|
||||
case .liveStreamV2:
|
||||
case .autoBenchmarkReflectors:
|
||||
return 53
|
||||
case .dynamicStreaming:
|
||||
case .benchmarkReflectors:
|
||||
return 54
|
||||
case .enableLocalTranslation:
|
||||
return 55
|
||||
@ -1344,25 +1345,70 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .liveStreamV2(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Live Stream V2", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
case let .autoBenchmarkReflectors(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Auto-Benchmark Reflectors [Restart]", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
|
||||
settings.liveStreamV2 = value
|
||||
settings.autoBenchmarkReflectors = value
|
||||
return PreferencesEntry(settings)
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .dynamicStreaming(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Dynamic Streaming", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
|
||||
settings.dynamicStreaming = value
|
||||
return PreferencesEntry(settings)
|
||||
case .benchmarkReflectors:
|
||||
return ItemListActionItem(presentationData: presentationData, title: "Benchmark Reflectors", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
guard let context = arguments.context else {
|
||||
return
|
||||
}
|
||||
|
||||
var signal: Signal<ReflectorBenchmark.Results, NoError> = Signal { subscriber in
|
||||
var reflectorBenchmark: ReflectorBenchmark? = ReflectorBenchmark(address: "91.108.13.35", port: 599)
|
||||
reflectorBenchmark?.start(completion: { results in
|
||||
subscriber.putNext(results)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
}).start()
|
||||
|
||||
return ActionDisposable {
|
||||
reflectorBenchmark = nil
|
||||
}
|
||||
}
|
||||
|> runOn(.mainQueue())
|
||||
|
||||
var cancelImpl: (() -> Void)?
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
arguments.presentController(controller, nil)
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.15, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
|
||||
let reindexDisposable = MetaDisposable()
|
||||
|
||||
signal = signal
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
cancelImpl = {
|
||||
reindexDisposable.set(nil)
|
||||
}
|
||||
reindexDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { results in
|
||||
if let context = arguments.context {
|
||||
let controller = textAlertController(context: context, title: nil, text: "Bandwidth: \(results.bandwidthBytesPerSecond * 8 / 1024) kbit/s (expected \(results.expectedBandwidthBytesPerSecond * 8 / 1024) kbit/s)\nAvg latency: \(Int(results.averageDelay * 1000.0)) ms", actions: [TextAlertAction(type: .genericAction, title: "OK", action: {})])
|
||||
arguments.presentController(controller, nil)
|
||||
}
|
||||
}))
|
||||
})
|
||||
case let .enableLocalTranslation(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Local Translation", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
@ -1530,8 +1576,14 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
||||
entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction))
|
||||
entries.append(.disableCallV2(experimentalSettings.disableCallV2))
|
||||
entries.append(.experimentalCallMute(experimentalSettings.experimentalCallMute))
|
||||
entries.append(.liveStreamV2(experimentalSettings.liveStreamV2))
|
||||
entries.append(.dynamicStreaming(experimentalSettings.dynamicStreaming))
|
||||
|
||||
var defaultAutoBenchmarkReflectors = false
|
||||
if case .internal = sharedContext.applicationBindings.appBuildType {
|
||||
defaultAutoBenchmarkReflectors = true
|
||||
}
|
||||
entries.append(.autoBenchmarkReflectors(experimentalSettings.autoBenchmarkReflectors ?? defaultAutoBenchmarkReflectors))
|
||||
|
||||
entries.append(.benchmarkReflectors)
|
||||
entries.append(.enableLocalTranslation(experimentalSettings.enableLocalTranslation))
|
||||
}
|
||||
|
||||
|
@ -73,6 +73,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
private var audioLevelDisposable: Disposable?
|
||||
private var audioOutputCheckTimer: Foundation.Timer?
|
||||
|
||||
private var applicationInForegroundDisposable: Disposable?
|
||||
|
||||
private var localVideo: AdaptedCallVideoSource?
|
||||
private var remoteVideo: AdaptedCallVideoSource?
|
||||
|
||||
@ -212,6 +214,37 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.applicationInForegroundDisposable = (self.sharedContext.applicationBindings.applicationInForeground
|
||||
|> filter { $0 }
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.callScreen.isPictureInPictureRequested {
|
||||
Queue.mainQueue().after(0.5, { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.callScreen.isPictureInPictureRequested && !self.callScreen.restoreFromPictureInPictureIfPossible() {
|
||||
Queue.mainQueue().after(0.2, { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.callScreen.isPictureInPictureRequested && !self.callScreen.restoreFromPictureInPictureIfPossible() {
|
||||
Queue.mainQueue().after(0.3, { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.callScreen.isPictureInPictureRequested && !self.callScreen.restoreFromPictureInPictureIfPossible() {
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -220,6 +253,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
self.audioLevelDisposable?.dispose()
|
||||
self.audioOutputCheckTimer?.invalidate()
|
||||
self.signalQualityTimer?.invalidate()
|
||||
self.applicationInForegroundDisposable?.dispose()
|
||||
}
|
||||
|
||||
func updateAudioOutputs(availableOutputs: [AudioSessionOutput], currentOutput: AudioSessionOutput?) {
|
||||
@ -623,6 +657,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
self.containerView.layer.allowsGroupOpacity = false
|
||||
})
|
||||
}
|
||||
|
||||
let _ = self.callScreen.restoreFromPictureInPictureIfPossible()
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
|
@ -104,6 +104,24 @@ final class PrivateCallPictureInPictureView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
override var frame: CGRect {
|
||||
didSet {
|
||||
if !self.bounds.isEmpty {
|
||||
/*if let testView = self.viewWithTag(123) {
|
||||
testView.frame = self.bounds.insetBy(dx: 10.0, dy: 10.0)
|
||||
} else {
|
||||
let testView = AnimationTrackingView(frame: self.bounds.insetBy(dx: 10.0, dy: 10.0))
|
||||
testView.layer.borderColor = UIColor.red.cgColor
|
||||
testView.layer.borderWidth = 2.0
|
||||
testView.tag = 123
|
||||
self.addSubview(testView)
|
||||
}*/
|
||||
|
||||
self.updateLayout(size: self.bounds.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override static var layerClass: AnyClass {
|
||||
return AVSampleBufferDisplayLayer.self
|
||||
}
|
||||
@ -146,50 +164,11 @@ final class PrivateCallPictureInPictureView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
let size = self.bounds.size
|
||||
func updateLayout(size: CGSize) {
|
||||
if size.width.isZero || size.height.isZero {
|
||||
return
|
||||
}
|
||||
|
||||
var animationTemplate: CAAnimation?
|
||||
self.animationTrackingView.onAnimation = { animation in
|
||||
animationTemplate = animation
|
||||
}
|
||||
self.animationTrackingView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.animationTrackingView.onAnimation = nil
|
||||
|
||||
let _ = animationTemplate
|
||||
|
||||
let animationDuration = CATransaction.animationDuration()
|
||||
let timingFunction = CATransaction.animationTimingFunction()
|
||||
|
||||
let mappedTransition: ComponentTransition
|
||||
if self.sampleBufferView.bounds.isEmpty {
|
||||
mappedTransition = .immediate
|
||||
} else if animationDuration > 0.0 && !CATransaction.disableActions() {
|
||||
let mappedCurve: ComponentTransition.Animation.Curve
|
||||
if let timingFunction {
|
||||
var controlPoint0: [Float] = [0.0, 0.0]
|
||||
var controlPoint1: [Float] = [0.0, 0.0]
|
||||
timingFunction.getControlPoint(at: 1, values: &controlPoint0)
|
||||
timingFunction.getControlPoint(at: 2, values: &controlPoint1)
|
||||
mappedCurve = .custom(controlPoint0[0], controlPoint0[1], controlPoint1[0], controlPoint1[1])
|
||||
} else if animationDuration >= 0.5 {
|
||||
mappedCurve = .spring
|
||||
} else {
|
||||
mappedCurve = .easeInOut
|
||||
}
|
||||
mappedTransition = ComponentTransition(animation: .curve(
|
||||
duration: animationDuration,
|
||||
curve: mappedCurve
|
||||
))
|
||||
} else {
|
||||
mappedTransition = .immediate
|
||||
}
|
||||
|
||||
if let videoMetrics = self.videoMetrics {
|
||||
let resolvedRotationAngle = resolveCallVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: UIApplication.shared.statusBarOrientation)
|
||||
|
||||
@ -223,26 +202,34 @@ final class PrivateCallPictureInPictureView: UIView {
|
||||
|
||||
if let sublayers = self.sampleBufferView.layer.sublayers {
|
||||
if sublayers.count > 1, !sublayers[0].bounds.isEmpty {
|
||||
sublayers[0].position = CGPoint(x: videoFrame.width * 0.5, y: videoFrame.height * 0.5)
|
||||
sublayers[0].bounds = CGRect(origin: CGPoint(), size: videoFrame.size)
|
||||
sublayers[0].position = CGPoint(x: videoFrame.width * 0.5, y: videoFrame.height * 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mappedTransition.animation.isImmediate {
|
||||
apply()
|
||||
} else {
|
||||
UIView.performWithoutAnimation {
|
||||
apply()
|
||||
}
|
||||
}
|
||||
apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
final class PrivateCallPictureInPictureController: AVPictureInPictureVideoCallViewController {
|
||||
var pipView: PrivateCallPictureInPictureView?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
if let pipView = self.pipView {
|
||||
pipView.updateLayout(size: self.view.bounds.size)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
coordinator.animate(alongsideTransition: { context in
|
||||
self.pipView?.frame = CGRect(origin: CGPoint(), size: size)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -204,6 +204,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
private var isEmojiKeyExpanded: Bool = false
|
||||
private var areControlsHidden: Bool = false
|
||||
private var swapLocalAndRemoteVideo: Bool = false
|
||||
public private(set) var isPictureInPictureRequested: Bool = false
|
||||
private var isPictureInPictureActive: Bool = false
|
||||
|
||||
private var hideEmojiTooltipTimer: Foundation.Timer?
|
||||
@ -324,15 +325,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
self.closeAction?()
|
||||
}
|
||||
|
||||
if !"".isEmpty {
|
||||
if #available(iOS 16.0, *) {
|
||||
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
|
||||
pipVideoCallViewController.view.addSubview(self.pipView)
|
||||
self.pipView.frame = pipVideoCallViewController.view.bounds
|
||||
self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.pipView.translatesAutoresizingMaskIntoConstraints = true
|
||||
self.pipVideoCallViewController = pipVideoCallViewController
|
||||
}
|
||||
if #available(iOS 16.0, *) {
|
||||
let pipVideoCallViewController = PrivateCallPictureInPictureController()
|
||||
pipVideoCallViewController.pipView = self.pipView
|
||||
pipVideoCallViewController.view.addSubview(self.pipView)
|
||||
self.pipView.frame = pipVideoCallViewController.view.bounds
|
||||
self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.pipView.translatesAutoresizingMaskIntoConstraints = true
|
||||
self.pipVideoCallViewController = pipVideoCallViewController
|
||||
}
|
||||
|
||||
if let blurFilter = makeBlurFilter() {
|
||||
@ -366,13 +366,19 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
|
||||
public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
self.isPictureInPictureActive = true
|
||||
self.isPictureInPictureRequested = true
|
||||
if !self.isUpdating {
|
||||
self.update(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
}
|
||||
|
||||
public func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
self.isPictureInPictureRequested = false
|
||||
}
|
||||
|
||||
public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
self.isPictureInPictureActive = false
|
||||
self.isPictureInPictureRequested = false
|
||||
if !self.isUpdating {
|
||||
let wereControlsHidden = self.areControlsHidden
|
||||
self.areControlsHidden = true
|
||||
@ -387,6 +393,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
}
|
||||
|
||||
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
|
||||
self.isPictureInPictureRequested = false
|
||||
if self.activeLocalVideoSource != nil || self.activeRemoteVideoSource != nil {
|
||||
if let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture {
|
||||
restoreUIForPictureInPicture(completionHandler)
|
||||
@ -472,6 +479,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
}
|
||||
}
|
||||
|
||||
public func restoreFromPictureInPictureIfPossible() -> Bool {
|
||||
if let pipController = self.pipController, pipController.isPictureInPictureActive {
|
||||
pipController.stopPictureInPicture()
|
||||
return !self.isPictureInPictureRequested
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, screenCornerRadius: CGFloat, state: State, transition: ComponentTransition) {
|
||||
let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, screenCornerRadius: screenCornerRadius, state: state)
|
||||
if self.params == params {
|
||||
|
@ -1552,6 +1552,48 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
|
||||
//self.addBackgroundDownloadTask()
|
||||
|
||||
let reflectorBenchmarkDisposable = MetaDisposable()
|
||||
let runReflectorBenchmarkDisposable = MetaDisposable()
|
||||
let _ = (self.context.get()
|
||||
|> deliverOnMainQueue).startStandalone(next: { context in
|
||||
reflectorBenchmarkDisposable.set(nil)
|
||||
runReflectorBenchmarkDisposable.set(nil)
|
||||
|
||||
guard let context = context?.context else {
|
||||
return
|
||||
}
|
||||
var defaultAutoBenchmarkReflectors = false
|
||||
if case .internal = context.sharedContext.applicationBindings.appBuildType {
|
||||
defaultAutoBenchmarkReflectors = true
|
||||
}
|
||||
if context.sharedContext.immediateExperimentalUISettings.autoBenchmarkReflectors ?? defaultAutoBenchmarkReflectors {
|
||||
reflectorBenchmarkDisposable.set((context.sharedContext.applicationBindings.applicationInForeground
|
||||
|> distinctUntilChanged
|
||||
|> deliverOnMainQueue).startStrict(next: { value in
|
||||
if value {
|
||||
let signal: Signal<ReflectorBenchmark.Results, NoError> = Signal { subscriber in
|
||||
var reflectorBenchmark: ReflectorBenchmark? = ReflectorBenchmark(address: "91.108.13.35", port: 599)
|
||||
reflectorBenchmark?.start(completion: { results in
|
||||
subscriber.putNext(results)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
reflectorBenchmark = nil
|
||||
}
|
||||
}
|
||||
|> runOn(.mainQueue())
|
||||
|> delay(Double.random(in: 1.0 ..< 5.0), queue: Queue.mainQueue())
|
||||
runReflectorBenchmarkDisposable.set(signal.startStrict(next: { results in
|
||||
print("Reflector banchmark:\nBandwidth: \(results.bandwidthBytesPerSecond * 8 / 1024) kbit/s (expected \(results.expectedBandwidthBytesPerSecond * 8 / 1024) kbit/s)\nAvg latency: \(Int(results.averageDelay * 1000.0)) ms")
|
||||
}))
|
||||
} else {
|
||||
runReflectorBenchmarkDisposable.set(nil)
|
||||
}
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -61,6 +61,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
public var liveStreamV2: Bool
|
||||
public var dynamicStreaming: Bool
|
||||
public var enableLocalTranslation: Bool
|
||||
public var autoBenchmarkReflectors: Bool?
|
||||
|
||||
public static var defaultSettings: ExperimentalUISettings {
|
||||
return ExperimentalUISettings(
|
||||
@ -99,7 +100,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
disableReloginTokens: false,
|
||||
liveStreamV2: false,
|
||||
dynamicStreaming: false,
|
||||
enableLocalTranslation: false
|
||||
enableLocalTranslation: false,
|
||||
autoBenchmarkReflectors: nil
|
||||
)
|
||||
}
|
||||
|
||||
@ -139,7 +141,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
disableReloginTokens: Bool,
|
||||
liveStreamV2: Bool,
|
||||
dynamicStreaming: Bool,
|
||||
enableLocalTranslation: Bool
|
||||
enableLocalTranslation: Bool,
|
||||
autoBenchmarkReflectors: Bool?
|
||||
) {
|
||||
self.keepChatNavigationStack = keepChatNavigationStack
|
||||
self.skipReadHistory = skipReadHistory
|
||||
@ -177,6 +180,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
self.liveStreamV2 = liveStreamV2
|
||||
self.dynamicStreaming = dynamicStreaming
|
||||
self.enableLocalTranslation = enableLocalTranslation
|
||||
self.autoBenchmarkReflectors = autoBenchmarkReflectors
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@ -218,6 +222,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
self.liveStreamV2 = try container.decodeIfPresent(Bool.self, forKey: "liveStreamV2") ?? false
|
||||
self.dynamicStreaming = try container.decodeIfPresent(Bool.self, forKey: "dynamicStreaming_v2") ?? false
|
||||
self.enableLocalTranslation = try container.decodeIfPresent(Bool.self, forKey: "enableLocalTranslation") ?? false
|
||||
self.autoBenchmarkReflectors = try container.decodeIfPresent(Bool.self, forKey: "autoBenchmarkReflectors")
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@ -259,6 +264,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
try container.encode(self.liveStreamV2, forKey: "liveStreamV2")
|
||||
try container.encode(self.dynamicStreaming, forKey: "dynamicStreaming")
|
||||
try container.encode(self.enableLocalTranslation, forKey: "enableLocalTranslation")
|
||||
try container.encodeIfPresent(self.autoBenchmarkReflectors, forKey: "autoBenchmarkReflectors")
|
||||
}
|
||||
}
|
||||
|
||||
|
423
submodules/TelegramVoip/Sources/ReflectorBenchmark.swift
Normal file
423
submodules/TelegramVoip/Sources/ReflectorBenchmark.swift
Normal file
@ -0,0 +1,423 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Network
|
||||
|
||||
public final class ReflectorBenchmark {
|
||||
public struct Results {
|
||||
public let bandwidthBytesPerSecond: Int
|
||||
public let expectedBandwidthBytesPerSecond: Int
|
||||
public let averageDelay: Double
|
||||
|
||||
public init(bandwidthBytesPerSecond: Int, expectedBandwidthBytesPerSecond: Int, averageDelay: Double) {
|
||||
self.bandwidthBytesPerSecond = bandwidthBytesPerSecond
|
||||
self.expectedBandwidthBytesPerSecond = expectedBandwidthBytesPerSecond
|
||||
self.averageDelay = averageDelay
|
||||
}
|
||||
}
|
||||
|
||||
private final class Impl {
|
||||
let queue: Queue
|
||||
let address: String
|
||||
let port: Int
|
||||
|
||||
let incomingTag: Data
|
||||
let outgoingTag: Data
|
||||
let outgoingRandomTag: Data
|
||||
|
||||
let targetBandwidthBytesPerSecond: Int
|
||||
let sendPacketInterval: Double
|
||||
let maxPacketCount: Int
|
||||
|
||||
var outgoingConnection: NWConnection?
|
||||
var incomingConnection: NWConnection?
|
||||
|
||||
var completion: ((Results) -> Void)?
|
||||
|
||||
var incomingPingSendTimestamp: Double?
|
||||
var didReceiveIncomingPing: Bool = false
|
||||
var outgoingPingSendTimestamp: Double?
|
||||
var didReceiveOutgoingPing: Bool = false
|
||||
|
||||
var sentPacketCount: Int = 0
|
||||
var receivedPacketCount: Int = 0
|
||||
var firstReceiveTimestamp: Double?
|
||||
var lastReceiveTimestamp: Double?
|
||||
var unconfirmedPacketSendTimestamp: [Data: Double] = [:]
|
||||
var packetSizeAndTimeToReceive: [(Int, Double)] = []
|
||||
|
||||
var pingTimer: SwiftSignalKit.Timer?
|
||||
var sendPacketTimer: SwiftSignalKit.Timer?
|
||||
var bandwidthTimer: SwiftSignalKit.Timer?
|
||||
|
||||
init(queue: Queue, address: String, port: Int) {
|
||||
self.queue = queue
|
||||
self.address = address
|
||||
self.port = port
|
||||
|
||||
self.targetBandwidthBytesPerSecond = 700 * 1024 / 8
|
||||
self.sendPacketInterval = 1.0 / 30.0
|
||||
self.maxPacketCount = Int(5.0 / self.sendPacketInterval)
|
||||
|
||||
var incomingTag = Data(count: 16)
|
||||
var outgoingTag = Data(count: 16)
|
||||
|
||||
incomingTag.withUnsafeMutableBytes { incomingBuffer -> Void in
|
||||
outgoingTag.withUnsafeMutableBytes { outgoingBuffer -> Void in
|
||||
let incoming = incomingBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
let outgoing = outgoingBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
arc4random_buf(incoming, incomingBuffer.count)
|
||||
memcpy(outgoing, incoming, incomingBuffer.count)
|
||||
|
||||
incoming[0] = 0
|
||||
outgoing[0] = 1
|
||||
}
|
||||
}
|
||||
|
||||
var outgoingRandomTag = Data(count: 4)
|
||||
outgoingRandomTag.withUnsafeMutableBytes { buffer -> Void in
|
||||
arc4random_buf(buffer.baseAddress!, buffer.count)
|
||||
}
|
||||
|
||||
self.incomingTag = incomingTag
|
||||
self.outgoingTag = outgoingTag
|
||||
self.outgoingRandomTag = outgoingRandomTag
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.incomingConnection?.cancel()
|
||||
self.outgoingConnection?.cancel()
|
||||
self.pingTimer?.invalidate()
|
||||
self.sendPacketTimer?.invalidate()
|
||||
self.bandwidthTimer?.invalidate()
|
||||
}
|
||||
|
||||
func start(completion: @escaping (Results) -> Void) {
|
||||
self.completion = completion
|
||||
|
||||
let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(self.address), port: NWEndpoint.Port(integerLiteral: UInt16(self.port)))
|
||||
let incomingConnection = NWConnection(to: endpoint, using: .udp)
|
||||
self.incomingConnection = incomingConnection
|
||||
|
||||
let outgoingConnection = NWConnection(to: endpoint, using: .udp)
|
||||
self.outgoingConnection = outgoingConnection
|
||||
|
||||
incomingConnection.start(queue: self.queue.queue)
|
||||
outgoingConnection.start(queue: self.queue.queue)
|
||||
|
||||
self.receiveIncomingPacket()
|
||||
self.receiveOutgoingPacket()
|
||||
self.sendIncomingPingPackets()
|
||||
|
||||
self.pingTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.sendIncomingPingPackets()
|
||||
self.sendOutgoingPingPackets()
|
||||
}, queue: self.queue)
|
||||
self.pingTimer?.start()
|
||||
|
||||
self.sendPacketTimer = SwiftSignalKit.Timer(timeout: self.sendPacketInterval, repeat: true, completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.sendOutgoingPacket()
|
||||
}, queue: self.queue)
|
||||
self.sendPacketTimer?.start()
|
||||
|
||||
self.bandwidthTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.calculateStats()
|
||||
}, queue: self.queue)
|
||||
self.bandwidthTimer?.start()
|
||||
}
|
||||
|
||||
private func calculateStats() {
|
||||
guard let firstReceiveTimestamp = self.firstReceiveTimestamp, let lastReceiveTimestamp = self.lastReceiveTimestamp else {
|
||||
return
|
||||
}
|
||||
if self.sentPacketCount < self.maxPacketCount {
|
||||
return
|
||||
}
|
||||
var totalSize = 0
|
||||
var totalDelay: Double = 0.0
|
||||
let totalTime: Double = lastReceiveTimestamp - firstReceiveTimestamp
|
||||
|
||||
for item in self.packetSizeAndTimeToReceive {
|
||||
totalSize += item.0
|
||||
totalDelay += item.1
|
||||
}
|
||||
|
||||
let averageDelay: Double
|
||||
if !self.packetSizeAndTimeToReceive.isEmpty {
|
||||
averageDelay = totalDelay / Double(self.packetSizeAndTimeToReceive.count)
|
||||
} else {
|
||||
averageDelay = 0.0
|
||||
}
|
||||
|
||||
if totalTime != 0.0 {
|
||||
let bandwidthBytesPerSecond = Int(Double(totalSize) / totalTime)
|
||||
if let completion = self.completion {
|
||||
self.completion = nil
|
||||
completion(Results(
|
||||
bandwidthBytesPerSecond: bandwidthBytesPerSecond,
|
||||
expectedBandwidthBytesPerSecond: self.targetBandwidthBytesPerSecond,
|
||||
averageDelay: averageDelay
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendIncomingPingPackets() {
|
||||
guard let connection = self.incomingConnection else {
|
||||
return
|
||||
}
|
||||
|
||||
var packetData = Data()
|
||||
packetData.append(self.incomingTag)
|
||||
|
||||
var controlByte1: UInt8 = 0xff
|
||||
for _ in 0 ..< 12 {
|
||||
packetData.append(&controlByte1, count: 1)
|
||||
}
|
||||
|
||||
var controlByte2: UInt8 = 0xfe
|
||||
packetData.append(&controlByte2, count: 1)
|
||||
for _ in 0 ..< 3 {
|
||||
packetData.append(&controlByte1, count: 1)
|
||||
}
|
||||
|
||||
var testValue: UInt64 = 123
|
||||
withUnsafeBytes(of: &testValue, { buffer -> Void in
|
||||
packetData.append(buffer.assumingMemoryBound(to: UInt8.self).baseAddress!, count: 8)
|
||||
})
|
||||
|
||||
var zeroByte: UInt8 = 0
|
||||
while packetData.count % 4 != 0 {
|
||||
packetData.append(&zeroByte, count: 1)
|
||||
}
|
||||
|
||||
if self.incomingPingSendTimestamp == nil {
|
||||
self.incomingPingSendTimestamp = CFAbsoluteTimeGetCurrent()
|
||||
}
|
||||
connection.send(content: packetData, completion: .contentProcessed({ _ in }))
|
||||
}
|
||||
|
||||
private func sendOutgoingPingPackets() {
|
||||
guard let connection = self.outgoingConnection else {
|
||||
return
|
||||
}
|
||||
|
||||
var packetData = Data()
|
||||
packetData.append(self.outgoingTag)
|
||||
|
||||
var controlByte1: UInt8 = 0xff
|
||||
for _ in 0 ..< 12 {
|
||||
packetData.append(&controlByte1, count: 1)
|
||||
}
|
||||
|
||||
var controlByte2: UInt8 = 0xfe
|
||||
packetData.append(&controlByte2, count: 1)
|
||||
for _ in 0 ..< 3 {
|
||||
packetData.append(&controlByte1, count: 1)
|
||||
}
|
||||
|
||||
var testValue: UInt64 = 123
|
||||
withUnsafeBytes(of: &testValue, { buffer -> Void in
|
||||
packetData.append(buffer.assumingMemoryBound(to: UInt8.self).baseAddress!, count: 8)
|
||||
})
|
||||
|
||||
var zeroByte: UInt8 = 0
|
||||
while packetData.count % 4 != 0 {
|
||||
packetData.append(&zeroByte, count: 1)
|
||||
}
|
||||
|
||||
if self.outgoingPingSendTimestamp == nil {
|
||||
self.outgoingPingSendTimestamp = CFAbsoluteTimeGetCurrent()
|
||||
}
|
||||
connection.send(content: packetData, completion: .contentProcessed({ _ in }))
|
||||
}
|
||||
|
||||
private func sendOutgoingPacket() {
|
||||
let timestamp = CFAbsoluteTimeGetCurrent()
|
||||
var timedOutPacketIds: [Data] = []
|
||||
for (packetId, packetTimestamp) in self.unconfirmedPacketSendTimestamp {
|
||||
let packetDelay = timestamp - packetTimestamp
|
||||
if packetDelay > 2.0 {
|
||||
timedOutPacketIds.append(packetId)
|
||||
self.receivedPacketCount += 1
|
||||
self.lastReceiveTimestamp = timestamp
|
||||
}
|
||||
}
|
||||
for packetId in timedOutPacketIds {
|
||||
self.unconfirmedPacketSendTimestamp.removeValue(forKey: packetId)
|
||||
}
|
||||
if let outgoingPingSendTimestamp = self.outgoingPingSendTimestamp, !self.didReceiveOutgoingPing {
|
||||
if outgoingPingSendTimestamp < timestamp - 2.0 {
|
||||
self.didReceiveOutgoingPing = true
|
||||
self.sentPacketCount = self.maxPacketCount
|
||||
}
|
||||
}
|
||||
if let incomingPingSendTimestamp = self.incomingPingSendTimestamp, !self.didReceiveIncomingPing {
|
||||
if incomingPingSendTimestamp < timestamp - 2.0 {
|
||||
self.didReceiveIncomingPing = true
|
||||
self.sentPacketCount = self.maxPacketCount
|
||||
}
|
||||
}
|
||||
|
||||
guard let connection = self.outgoingConnection else {
|
||||
return
|
||||
}
|
||||
if self.sentPacketCount >= self.maxPacketCount {
|
||||
return
|
||||
}
|
||||
if !self.didReceiveIncomingPing && self.didReceiveOutgoingPing {
|
||||
return
|
||||
}
|
||||
|
||||
let bandwidthAdjustedPacketLength: Int32 = Int32(Double(self.targetBandwidthBytesPerSecond) * self.sendPacketInterval) + Int32.random(in: 0 ..< 1 * 1024)
|
||||
var remainingPacketLength = bandwidthAdjustedPacketLength
|
||||
|
||||
while remainingPacketLength > 0 {
|
||||
var packetData = Data()
|
||||
|
||||
packetData.append(self.outgoingTag)
|
||||
packetData.append(self.outgoingRandomTag)
|
||||
|
||||
let packetLength = min(remainingPacketLength, 1 * 1024)
|
||||
var dataLength: Int32 = 8 + bandwidthAdjustedPacketLength
|
||||
withUnsafeBytes(of: &dataLength, { buffer -> Void in
|
||||
packetData.append(buffer.assumingMemoryBound(to: UInt8.self).baseAddress!, count: buffer.count)
|
||||
})
|
||||
|
||||
var packetId = Data(count: 8)
|
||||
packetId.withUnsafeMutableBytes { buffer -> Void in
|
||||
arc4random_buf(buffer.baseAddress!, buffer.count)
|
||||
}
|
||||
packetData.append(packetId)
|
||||
|
||||
var innerData = Data(count: Int(packetLength))
|
||||
innerData.withUnsafeMutableBytes { buffer -> Void in
|
||||
arc4random_buf(buffer.baseAddress!, buffer.count)
|
||||
}
|
||||
packetData.append(innerData)
|
||||
|
||||
var zeroByte: UInt8 = 0
|
||||
while packetData.count % 4 != 0 {
|
||||
packetData.append(&zeroByte, count: 1)
|
||||
}
|
||||
|
||||
self.unconfirmedPacketSendTimestamp[packetId] = timestamp
|
||||
remainingPacketLength -= packetLength
|
||||
self.sentPacketCount += 1
|
||||
|
||||
if self.firstReceiveTimestamp == nil {
|
||||
self.firstReceiveTimestamp = timestamp
|
||||
}
|
||||
|
||||
connection.send(content: packetData, completion: .contentProcessed({ _ in }))
|
||||
}
|
||||
}
|
||||
|
||||
private func receiveIncomingPacket() {
|
||||
guard let connection = self.incomingConnection else {
|
||||
return
|
||||
}
|
||||
connection.receive(minimumIncompleteLength: 1, maximumLength: 32 * 1024, completion: { [weak self] content, _, _, error in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let content {
|
||||
if content.count >= 16 + 4 + 4 + 8 {
|
||||
let tag = content.subdata(in: 0 ..< 16)
|
||||
if tag == self.incomingTag {
|
||||
let packetId = content.subdata(in: (16 + 4 + 4) ..< (16 + 4 + 4 + 8))
|
||||
if let sentTimestamp = self.unconfirmedPacketSendTimestamp.removeValue(forKey: packetId) {
|
||||
let timestamp = CFAbsoluteTimeGetCurrent()
|
||||
let packetSendReceiveDuration = timestamp - sentTimestamp
|
||||
self.lastReceiveTimestamp = timestamp
|
||||
self.receivedPacketCount += 1
|
||||
self.packetSizeAndTimeToReceive.append((content.count, packetSendReceiveDuration))
|
||||
} else {
|
||||
var pingHeaderData = Data()
|
||||
var controlByte1: UInt8 = 0xff
|
||||
for _ in 0 ..< 8 {
|
||||
pingHeaderData.append(&controlByte1, count: 1)
|
||||
}
|
||||
let pingPacketId = content.subdata(in: 16 ..< (16 + 8))
|
||||
if pingPacketId == pingHeaderData {
|
||||
self.didReceiveIncomingPing = true
|
||||
} else {
|
||||
//print("Unknown incoming packet id")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("Invalid incoming tag")
|
||||
}
|
||||
} else {
|
||||
print("Invalid content length: \(content.count)")
|
||||
}
|
||||
} else {
|
||||
print("Incoming data receive error")
|
||||
}
|
||||
|
||||
self.receiveIncomingPacket()
|
||||
})
|
||||
}
|
||||
|
||||
private func receiveOutgoingPacket() {
|
||||
guard let connection = self.outgoingConnection else {
|
||||
return
|
||||
}
|
||||
connection.receive(minimumIncompleteLength: 1, maximumLength: 32 * 1024, completion: { [weak self] content, _, _, error in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let content {
|
||||
if content.count >= 16 + 8 {
|
||||
let tag = content.subdata(in: 0 ..< 16)
|
||||
if tag == self.outgoingTag {
|
||||
let packetId = content.subdata(in: 16 ..< (16 + 8))
|
||||
var pingHeaderData = Data()
|
||||
var controlByte1: UInt8 = 0xff
|
||||
for _ in 0 ..< 8 {
|
||||
pingHeaderData.append(&controlByte1, count: 1)
|
||||
}
|
||||
if packetId == pingHeaderData {
|
||||
self.didReceiveOutgoingPing = true
|
||||
} else {
|
||||
print("Unknown outgoing packet id")
|
||||
}
|
||||
} else {
|
||||
print("Invalid outgoing tag")
|
||||
}
|
||||
} else {
|
||||
print("Invalid content length: \(content.count)")
|
||||
}
|
||||
}
|
||||
|
||||
self.receiveOutgoingPacket()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private static let sharedQueue = Queue(name: "ReflectorBenchmark")
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
|
||||
public init(address: String, port: Int) {
|
||||
let queue = ReflectorBenchmark.sharedQueue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, address: address, port: port)
|
||||
})
|
||||
}
|
||||
|
||||
public func start(completion: @escaping (Results) -> Void) {
|
||||
self.impl.with { impl in
|
||||
impl.start(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user