diff --git a/submodules/DebugSettingsUI/BUILD b/submodules/DebugSettingsUI/BUILD index 60710882b0..553c5023c8 100644 --- a/submodules/DebugSettingsUI/BUILD +++ b/submodules/DebugSettingsUI/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/GZip:GZip", "//third-party/ZipArchive:ZipArchive", "//submodules/InAppPurchaseManager:InAppPurchaseManager", + "//submodules/TelegramVoip", ], visibility = [ "//visibility:public", diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 255acb2049..a6201959f4 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -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 = 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 { 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)) } diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index 934725f543..e8dca56706 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -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) { diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift index 819af62dcf..560455983c 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift @@ -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) + }) } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index 9474675578..d38cc2e68d 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 830af8f518..5c6b2df2fa 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -1552,6 +1552,48 @@ private func extractAccountManagerState(records: AccountRecordsView 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 = 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 } diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index fa3070c1e3..479963e575 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -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") } } diff --git a/submodules/TelegramVoip/Sources/ReflectorBenchmark.swift b/submodules/TelegramVoip/Sources/ReflectorBenchmark.swift new file mode 100644 index 0000000000..6a4e07d684 --- /dev/null +++ b/submodules/TelegramVoip/Sources/ReflectorBenchmark.swift @@ -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 + + 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) + } + } +}