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",
"//third-party/ZipArchive:ZipArchive",
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
"//submodules/TelegramVoip",
],
visibility = [
"//visibility:public",

View File

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

View File

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

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

View File

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

View File

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

View File

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

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