Call improvements

This commit is contained in:
Isaac 2024-11-19 18:49:25 +04:00
parent a38e2e41ed
commit d33e03b64c
8 changed files with 640 additions and 77 deletions

View File

@ -25,6 +25,7 @@ swift_library(
"//submodules/GZip:GZip", "//submodules/GZip:GZip",
"//third-party/ZipArchive:ZipArchive", "//third-party/ZipArchive:ZipArchive",
"//submodules/InAppPurchaseManager:InAppPurchaseManager", "//submodules/InAppPurchaseManager:InAppPurchaseManager",
"//submodules/TelegramVoip",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -16,6 +16,7 @@ import AppBundle
import ZipArchive import ZipArchive
import WebKit import WebKit
import InAppPurchaseManager import InAppPurchaseManager
import TelegramVoip
@objc private final class DebugControllerMailComposeDelegate: NSObject, MFMailComposeViewControllerDelegate { @objc private final class DebugControllerMailComposeDelegate: NSObject, MFMailComposeViewControllerDelegate {
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
@ -104,8 +105,8 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case disableReloginTokens(Bool) case disableReloginTokens(Bool)
case disableCallV2(Bool) case disableCallV2(Bool)
case experimentalCallMute(Bool) case experimentalCallMute(Bool)
case liveStreamV2(Bool) case autoBenchmarkReflectors(Bool)
case dynamicStreaming(Bool) case benchmarkReflectors
case enableLocalTranslation(Bool) case enableLocalTranslation(Bool)
case preferredVideoCodec(Int, String, String?, Bool) case preferredVideoCodec(Int, String, String?, Bool)
case disableVideoAspectScaling(Bool) case disableVideoAspectScaling(Bool)
@ -131,7 +132,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.web.rawValue return DebugControllerSection.web.rawValue
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
return DebugControllerSection.experiments.rawValue 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 return DebugControllerSection.experiments.rawValue
case .logTranslationRecognition, .resetTranslationStates: case .logTranslationRecognition, .resetTranslationStates:
return DebugControllerSection.translation.rawValue return DebugControllerSection.translation.rawValue
@ -248,9 +249,9 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 51 return 51
case .experimentalCallMute: case .experimentalCallMute:
return 52 return 52
case .liveStreamV2: case .autoBenchmarkReflectors:
return 53 return 53
case .dynamicStreaming: case .benchmarkReflectors:
return 54 return 54
case .enableLocalTranslation: case .enableLocalTranslation:
return 55 return 55
@ -1344,25 +1345,70 @@ private enum DebugControllerEntry: ItemListNodeEntry {
}) })
}).start() }).start()
}) })
case let .liveStreamV2(value): case let .autoBenchmarkReflectors(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Live Stream V2", value: value, sectionId: self.section, style: .blocks, updated: { value in 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 let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
settings.liveStreamV2 = value settings.autoBenchmarkReflectors = value
return PreferencesEntry(settings) return PreferencesEntry(settings)
}) })
}).start() }).start()
}) })
case let .dynamicStreaming(value): case .benchmarkReflectors:
return ItemListSwitchItem(presentationData: presentationData, title: "Dynamic Streaming", value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListActionItem(presentationData: presentationData, title: "Benchmark Reflectors", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in guard let context = arguments.context else {
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in return
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings }
settings.dynamicStreaming = value
return PreferencesEntry(settings) 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): case let .enableLocalTranslation(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Local Translation", value: value, sectionId: self.section, style: .blocks, updated: { value in 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(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction))
entries.append(.disableCallV2(experimentalSettings.disableCallV2)) entries.append(.disableCallV2(experimentalSettings.disableCallV2))
entries.append(.experimentalCallMute(experimentalSettings.experimentalCallMute)) 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)) entries.append(.enableLocalTranslation(experimentalSettings.enableLocalTranslation))
} }

View File

@ -73,6 +73,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
private var audioLevelDisposable: Disposable? private var audioLevelDisposable: Disposable?
private var audioOutputCheckTimer: Foundation.Timer? private var audioOutputCheckTimer: Foundation.Timer?
private var applicationInForegroundDisposable: Disposable?
private var localVideo: AdaptedCallVideoSource? private var localVideo: AdaptedCallVideoSource?
private var remoteVideo: 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 { deinit {
@ -220,6 +253,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
self.audioLevelDisposable?.dispose() self.audioLevelDisposable?.dispose()
self.audioOutputCheckTimer?.invalidate() self.audioOutputCheckTimer?.invalidate()
self.signalQualityTimer?.invalidate() self.signalQualityTimer?.invalidate()
self.applicationInForegroundDisposable?.dispose()
} }
func updateAudioOutputs(availableOutputs: [AudioSessionOutput], currentOutput: AudioSessionOutput?) { func updateAudioOutputs(availableOutputs: [AudioSessionOutput], currentOutput: AudioSessionOutput?) {
@ -623,6 +657,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
self.containerView.layer.allowsGroupOpacity = false self.containerView.layer.allowsGroupOpacity = false
}) })
} }
let _ = self.callScreen.restoreFromPictureInPictureIfPossible()
} }
func animateOut(completion: @escaping () -> Void) { func animateOut(completion: @escaping () -> Void) {

View File

@ -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 { override static var layerClass: AnyClass {
return AVSampleBufferDisplayLayer.self return AVSampleBufferDisplayLayer.self
} }
@ -146,50 +164,11 @@ final class PrivateCallPictureInPictureView: UIView {
} }
} }
override func layoutSubviews() { func updateLayout(size: CGSize) {
super.layoutSubviews()
let size = self.bounds.size
if size.width.isZero || size.height.isZero { if size.width.isZero || size.height.isZero {
return 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 { if let videoMetrics = self.videoMetrics {
let resolvedRotationAngle = resolveCallVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: UIApplication.shared.statusBarOrientation) 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 let sublayers = self.sampleBufferView.layer.sublayers {
if sublayers.count > 1, !sublayers[0].bounds.isEmpty { 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].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()
apply()
} else {
UIView.performWithoutAnimation {
apply()
}
}
} }
} }
} }
@available(iOS 15.0, *) @available(iOS 15.0, *)
final class PrivateCallPictureInPictureController: AVPictureInPictureVideoCallViewController { 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) { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator) super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { context in
self.pipView?.frame = CGRect(origin: CGPoint(), size: size)
})
} }
} }

View File

@ -204,6 +204,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
private var isEmojiKeyExpanded: Bool = false private var isEmojiKeyExpanded: Bool = false
private var areControlsHidden: Bool = false private var areControlsHidden: Bool = false
private var swapLocalAndRemoteVideo: Bool = false private var swapLocalAndRemoteVideo: Bool = false
public private(set) var isPictureInPictureRequested: Bool = false
private var isPictureInPictureActive: Bool = false private var isPictureInPictureActive: Bool = false
private var hideEmojiTooltipTimer: Foundation.Timer? private var hideEmojiTooltipTimer: Foundation.Timer?
@ -324,15 +325,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
self.closeAction?() self.closeAction?()
} }
if !"".isEmpty { if #available(iOS 16.0, *) {
if #available(iOS 16.0, *) { let pipVideoCallViewController = PrivateCallPictureInPictureController()
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController() pipVideoCallViewController.pipView = self.pipView
pipVideoCallViewController.view.addSubview(self.pipView) pipVideoCallViewController.view.addSubview(self.pipView)
self.pipView.frame = pipVideoCallViewController.view.bounds self.pipView.frame = pipVideoCallViewController.view.bounds
self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.pipView.translatesAutoresizingMaskIntoConstraints = true self.pipView.translatesAutoresizingMaskIntoConstraints = true
self.pipVideoCallViewController = pipVideoCallViewController self.pipVideoCallViewController = pipVideoCallViewController
}
} }
if let blurFilter = makeBlurFilter() { if let blurFilter = makeBlurFilter() {
@ -366,13 +366,19 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
self.isPictureInPictureActive = true self.isPictureInPictureActive = true
self.isPictureInPictureRequested = true
if !self.isUpdating { if !self.isUpdating {
self.update(transition: .easeInOut(duration: 0.2)) self.update(transition: .easeInOut(duration: 0.2))
} }
} }
public func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
self.isPictureInPictureRequested = false
}
public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
self.isPictureInPictureActive = false self.isPictureInPictureActive = false
self.isPictureInPictureRequested = false
if !self.isUpdating { if !self.isUpdating {
let wereControlsHidden = self.areControlsHidden let wereControlsHidden = self.areControlsHidden
self.areControlsHidden = true self.areControlsHidden = true
@ -387,6 +393,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
} }
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
self.isPictureInPictureRequested = false
if self.activeLocalVideoSource != nil || self.activeRemoteVideoSource != nil { if self.activeLocalVideoSource != nil || self.activeRemoteVideoSource != nil {
if let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture { if let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture {
restoreUIForPictureInPicture(completionHandler) 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) { 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) let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, screenCornerRadius: screenCornerRadius, state: state)
if self.params == params { if self.params == params {

View File

@ -1552,6 +1552,48 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
//self.addBackgroundDownloadTask() //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 return true
} }

View File

@ -61,6 +61,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
public var liveStreamV2: Bool public var liveStreamV2: Bool
public var dynamicStreaming: Bool public var dynamicStreaming: Bool
public var enableLocalTranslation: Bool public var enableLocalTranslation: Bool
public var autoBenchmarkReflectors: Bool?
public static var defaultSettings: ExperimentalUISettings { public static var defaultSettings: ExperimentalUISettings {
return ExperimentalUISettings( return ExperimentalUISettings(
@ -99,7 +100,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
disableReloginTokens: false, disableReloginTokens: false,
liveStreamV2: false, liveStreamV2: false,
dynamicStreaming: false, dynamicStreaming: false,
enableLocalTranslation: false enableLocalTranslation: false,
autoBenchmarkReflectors: nil
) )
} }
@ -139,7 +141,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
disableReloginTokens: Bool, disableReloginTokens: Bool,
liveStreamV2: Bool, liveStreamV2: Bool,
dynamicStreaming: Bool, dynamicStreaming: Bool,
enableLocalTranslation: Bool enableLocalTranslation: Bool,
autoBenchmarkReflectors: Bool?
) { ) {
self.keepChatNavigationStack = keepChatNavigationStack self.keepChatNavigationStack = keepChatNavigationStack
self.skipReadHistory = skipReadHistory self.skipReadHistory = skipReadHistory
@ -177,6 +180,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.liveStreamV2 = liveStreamV2 self.liveStreamV2 = liveStreamV2
self.dynamicStreaming = dynamicStreaming self.dynamicStreaming = dynamicStreaming
self.enableLocalTranslation = enableLocalTranslation self.enableLocalTranslation = enableLocalTranslation
self.autoBenchmarkReflectors = autoBenchmarkReflectors
} }
public init(from decoder: Decoder) throws { 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.liveStreamV2 = try container.decodeIfPresent(Bool.self, forKey: "liveStreamV2") ?? false
self.dynamicStreaming = try container.decodeIfPresent(Bool.self, forKey: "dynamicStreaming_v2") ?? false self.dynamicStreaming = try container.decodeIfPresent(Bool.self, forKey: "dynamicStreaming_v2") ?? false
self.enableLocalTranslation = try container.decodeIfPresent(Bool.self, forKey: "enableLocalTranslation") ?? 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 { 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.liveStreamV2, forKey: "liveStreamV2")
try container.encode(self.dynamicStreaming, forKey: "dynamicStreaming") try container.encode(self.dynamicStreaming, forKey: "dynamicStreaming")
try container.encode(self.enableLocalTranslation, forKey: "enableLocalTranslation") try container.encode(self.enableLocalTranslation, forKey: "enableLocalTranslation")
try container.encodeIfPresent(self.autoBenchmarkReflectors, forKey: "autoBenchmarkReflectors")
} }
} }

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