mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-16 08:43:17 +08:00
124 lines
4.4 KiB
Swift
124 lines
4.4 KiB
Swift
import Foundation
|
|
|
|
struct RemoteTrafficStatistics {
|
|
var inboundBitrate: UInt32 = 0
|
|
var outboundBitrate: UInt32 = 0
|
|
var lossRate: Float = 0
|
|
}
|
|
|
|
struct RemoteNetworkReport {
|
|
enum Mode { case unknown, direct, relay }
|
|
|
|
var video = RemoteTrafficStatistics()
|
|
var audio = RemoteTrafficStatistics()
|
|
var data = RemoteTrafficStatistics()
|
|
var total = RemoteTrafficStatistics()
|
|
var mode: Mode = .unknown
|
|
var srtpActive = false
|
|
var rttMilliseconds: Double = -1
|
|
}
|
|
|
|
struct RemoteNetworkSnapshot {
|
|
var report: RemoteNetworkReport?
|
|
var framesPerSecond = 0
|
|
var videoLatencyMilliseconds: Double?
|
|
var rttMilliseconds: Double?
|
|
|
|
var connectionMode: String {
|
|
switch report?.mode {
|
|
case .direct: return "P2P 直连"
|
|
case .relay: return "TURN 中继"
|
|
default: return "—"
|
|
}
|
|
}
|
|
|
|
static func bitrate(_ value: UInt32?) -> String {
|
|
guard let value else { return "—" }
|
|
if value >= 1_000_000 {
|
|
return String(format: "%.1f Mbps", Double(value) / 1_000_000)
|
|
}
|
|
if value >= 1_000 {
|
|
return String(format: "%.0f Kbps", Double(value) / 1_000)
|
|
}
|
|
return "\(value) bps"
|
|
}
|
|
|
|
static func lossRate(_ value: Float?) -> String {
|
|
guard let value, value.isFinite, value >= 0 else { return "—" }
|
|
// MiniRTC's total is the sum of media fractions, as on desktop.
|
|
return String(format: "%.1f%%", value * 100)
|
|
}
|
|
|
|
static func latency(_ value: Double?) -> String {
|
|
guard let value, value.isFinite, value >= 0 else { return "—" }
|
|
return value < 1 ? "<1 ms" : String(format: "%.0f ms", value)
|
|
}
|
|
}
|
|
|
|
/// Main-thread presentation statistics. Time is monotonic and injectable so
|
|
/// missing reports, idle video and reconnections have deterministic behavior.
|
|
struct RemoteNetworkStatistics {
|
|
private var report: RemoteNetworkReport?
|
|
private var reportTime: TimeInterval = 0
|
|
private var averageRTT: Double?
|
|
private var rttTime: TimeInterval = 0
|
|
private var lastFrameID: UInt64 = 0
|
|
private var frames: [(time: TimeInterval, latency: Double?)] = []
|
|
private var displayedSnapshot = RemoteNetworkSnapshot()
|
|
private var displayTime: TimeInterval?
|
|
|
|
mutating func receive(_ report: RemoteNetworkReport, at now: TimeInterval) {
|
|
self.report = report
|
|
reportTime = now
|
|
let rtt = report.rttMilliseconds
|
|
guard rtt.isFinite, rtt >= 0, rtt <= 2_000 else { return }
|
|
if let previous = averageRTT, now - rttTime < 3 {
|
|
averageRTT = previous + 0.25 * (rtt - previous)
|
|
} else {
|
|
averageRTT = rtt
|
|
}
|
|
rttTime = now
|
|
}
|
|
|
|
mutating func recordSubmittedFrame(id: UInt64, captureUptime: TimeInterval,
|
|
at now: TimeInterval) {
|
|
// Cached redraws and foreground resubmission are not new video frames.
|
|
guard id > lastFrameID else { return }
|
|
lastFrameID = id
|
|
let latency = (now - captureUptime) * 1_000
|
|
let valid = captureUptime > 0 && latency.isFinite && (0...5_000).contains(latency)
|
|
frames.removeAll { now - $0.time >= 1 }
|
|
frames.append((now, valid ? latency : nil))
|
|
if frames.count > 240 { frames.removeFirst(frames.count - 240) }
|
|
}
|
|
|
|
mutating func resetVideo() {
|
|
frames.removeAll(keepingCapacity: true)
|
|
displayedSnapshot.framesPerSecond = 0
|
|
displayedSnapshot.videoLatencyMilliseconds = nil
|
|
}
|
|
|
|
/// SwiftUI also redraws this menu for every video frame. A periodic view
|
|
/// schedules updates but does not throttle those redraws, so freeze the
|
|
/// displayed measurements between one-second samples.
|
|
mutating func displaySnapshot(at now: TimeInterval) -> RemoteNetworkSnapshot {
|
|
if let displayTime, now - displayTime < 1 {
|
|
return displayedSnapshot
|
|
}
|
|
displayedSnapshot = snapshot(at: now)
|
|
displayTime = now
|
|
return displayedSnapshot
|
|
}
|
|
|
|
func snapshot(at now: TimeInterval) -> RemoteNetworkSnapshot {
|
|
let recentFrames = frames.filter { now - $0.time < 1 }
|
|
let delays = recentFrames.compactMap(\.latency)
|
|
return RemoteNetworkSnapshot(
|
|
report: now - reportTime < 3 ? report : nil,
|
|
framesPerSecond: recentFrames.count,
|
|
videoLatencyMilliseconds: delays.isEmpty ? nil : delays.reduce(0, +) / Double(delays.count),
|
|
rttMilliseconds: now - rttTime < 3 ? averageRTT : nil
|
|
)
|
|
}
|
|
}
|