import Foundation /// What this app's own screen is doing on the way out, read from one /// connection's statistics. /// /// It exists because of a report on 2026-09-24 that "the screenshare /// isn't keeping up with what I'm doing on the screen", and because /// nothing in the app could say why: a slow screen can be the encoder /// giving up frames to keep resolution, a thin or still-ramping uplink, /// a relay in the path, or a software encoder that cannot keep up. These /// fields tell those apart, and `summary` says which in one line. public struct ScreenSendReport: Equatable, Sendable { public var width = 0 public var height = 0 /// Frames a second leaving the encoder, as WebRTC measures it. public var framesPerSecond = 0.0 /// Frames a second arriving from the capture (the media source), and /// its size before any scaling. public var capturedPerSecond = 0.0 public var sourceWidth = 0 public var sourceHeight = 0 public var framesSent: UInt64 = 0 public var framesEncoded: UInt64 = 0 public var framesCaptured: UInt64 = 0 /// Keyframes the encoder has made, and the keyframe requests (PLI and /// FIR) that have come back from the receiver for this stream. public var keyFramesEncoded: UInt64 = 0 public var keyframeRequests: UInt64 = 0 /// "none", "bandwidth", "cpu" or "other", as WebRTC names them. public var limitation = "" /// Seconds spent under each limitation since the stream began. public var limitationSeconds: [String: Double] = [:] public var targetBitrate: UInt64 = 0 public var bytesSent: UInt64 = 0 /// When the counters were read, in seconds, for turning bytes into a rate. public var timestamp = 0.0 /// The encoder behind the stream, e.g. "VideoToolbox" or "libvpx". public var encoder = "" /// The codec's MIME type, e.g. "video/H264". public var codec = "" public var totalEncodeTime = 0.0 public var packetsLost: Int64 = 0 public var roundTripTime = 0.0 /// The selected path: "host", "srflx", "prflx" or "relay". A relay on /// either end is reported as relay, because that is the extra hop. public var path = "" /// Bits a second actually sent, from the byte count between two reads. /// Zero until there have been two. public var sentBitrate: UInt64 = 0 public init() {} /// Milliseconds the encoder took per frame, on average. public var encodeMsPerFrame: Double { framesEncoded == 0 ? 0 : totalEncodeTime * 1000 / Double(framesEncoded) } /// The rate from `previous` to this read, when both are the same stream. public func measured(since previous: ScreenSendReport?) -> ScreenSendReport { var next = self guard let previous, timestamp > previous.timestamp, bytesSent >= previous.bytesSent else { return next } next.sentBitrate = UInt64( (Double(bytesSent - previous.bytesSent) * 8 / (timestamp - previous.timestamp)).rounded()) return next } /// Of several connections, the one a person would notice: the slowest. public static func worst(_ reports: [ScreenSendReport]) -> ScreenSendReport? { reports.min { $0.framesPerSecond < $1.framesPerSecond } } /// One line for the device panel, e.g. /// "Screen: 1920×1080 at 8 fps, limited by bandwidth, 1.2 Mbit/s, relay". public var summary: String { if width == 0 || height == 0 { return "Screen: starting…" } var parts = ["Screen: \(width)×\(height) at \(Int(framesPerSecond.rounded())) fps"] switch limitation { case "bandwidth": parts.append("limited by bandwidth") case "cpu": parts.append("limited by the processor") case "other": parts.append("limited") default: break } let rate = sentBitrate > 0 ? sentBitrate : targetBitrate if rate > 0 { parts.append(Self.megabits(rate)) } if !path.isEmpty { parts.append(path == "relay" ? "relay" : "direct") } var line = parts.joined(separator: ", ") if !encoder.isEmpty { line += " (\(Self.encoderName(encoder, codec: codec)))" } return line } /// The second line, under `summary`, while sharing: what the encoder /// and the watchdog are doing, e.g. "Encoded 29 fps, sent 29 fps, /// keyframes 4 (3 asked for); watchdog: watching". public func detail(since previous: ScreenSendReport?, watchdog: String) -> String { var encodedRate = framesPerSecond var sentRate = framesPerSecond if let previous, timestamp > previous.timestamp, framesEncoded >= previous.framesEncoded, framesSent >= previous.framesSent { let seconds = timestamp - previous.timestamp encodedRate = Double(framesEncoded - previous.framesEncoded) / seconds sentRate = Double(framesSent - previous.framesSent) / seconds } var line = "Encoded \(Int(encodedRate.rounded())) fps, sent \(Int(sentRate.rounded())) fps, keyframes \(keyFramesEncoded)" + " (\(keyframeRequests) asked for)" if !watchdog.isEmpty { line += "; watchdog: \(watchdog)" } return line } static func megabits(_ bps: UInt64) -> String { let mbit = Double(bps) / 1_000_000 return mbit >= 1 ? String(format: "%.1f Mbit/s", mbit) : "\(Int((Double(bps) / 1000).rounded())) kbit/s" } /// "VideoToolbox" means the Mac's hardware encoder, which is the thing /// worth saying; anything else is software. static func encoderName(_ implementation: String, codec: String) -> String { let name = codec.split(separator: "/").last.map(String.init) ?? "" let hardware = implementation.contains("VideoToolbox") return [name, hardware ? "hardware" : "software"].filter { !$0.isEmpty }.joined(separator: " ") } /// A limitation as a number, for the gate's integer statistics. public var limitationCode: UInt64 { ["none": 0, "cpu": 1, "bandwidth": 2, "other": 3][limitation] ?? 0 } /// The path as a number, for the same: 1 host, 2 srflx, 3 prflx, 4 relay. public var pathCode: UInt64 { ["host": 1, "srflx": 2, "prflx": 3, "relay": 4][path] ?? 0 } public var hardwareEncoder: Bool { encoder.contains("VideoToolbox") } } extension ScreenSendReport { /// Reads one connection's statistics for the outbound stream on `mid`. /// `stats` is the report as WebRTC gives it, flattened: id to type and /// values. Nil when that stream is not in it (not negotiated yet). static func read(_ stats: [String: (type: String, values: [String: Any])], mid: String) -> ScreenSendReport? { guard !mid.isEmpty, let (outboundId, outbound) = stats.first(where: { $0.value.type == "outbound-rtp" && ($0.value.values["mid"] as? String) == mid }).map({ ($0.key, $0.value.values) }) else { return nil } var r = ScreenSendReport() r.width = int(outbound["frameWidth"]) r.height = int(outbound["frameHeight"]) r.framesPerSecond = double(outbound["framesPerSecond"]) r.framesSent = UInt64(int(outbound["framesSent"])) r.framesEncoded = UInt64(int(outbound["framesEncoded"])) r.keyFramesEncoded = UInt64(int(outbound["keyFramesEncoded"])) r.keyframeRequests = UInt64(int(outbound["pliCount"]) + int(outbound["firCount"])) r.limitation = outbound["qualityLimitationReason"] as? String ?? "" if let durations = outbound["qualityLimitationDurations"] as? [String: Any] { r.limitationSeconds = durations.mapValues { double($0) } } r.targetBitrate = UInt64(int(outbound["targetBitrate"])) r.bytesSent = UInt64(int(outbound["bytesSent"])) r.encoder = outbound["encoderImplementation"] as? String ?? "" r.totalEncodeTime = double(outbound["totalEncodeTime"]) if let source = outbound["mediaSourceId"] as? String, let media = stats[source]?.values { r.framesCaptured = UInt64(int(media["frames"])) r.capturedPerSecond = double(media["framesPerSecond"]) r.sourceWidth = int(media["width"]) r.sourceHeight = int(media["height"]) } if let codec = outbound["codecId"] as? String { r.codec = stats[codec]?.values["mimeType"] as? String ?? "" } if let remote = stats.first(where: { $0.value.type == "remote-inbound-rtp" && ($0.value.values["localId"] as? String) == outboundId })?.value.values { r.packetsLost = Int64(int(remote["packetsLost"])) r.roundTripTime = double(remote["roundTripTime"]) } r.path = path(stats) return r } /// The selected candidate pair's kind of path. static func path(_ stats: [String: (type: String, values: [String: Any])]) -> String { var pairId = stats.values.first { $0.type == "transport" }?.values["selectedCandidatePairId"] as? String if pairId == nil { pairId = stats.first { $0.value.type == "candidate-pair" && ($0.value.values["nominated"] as? Bool ?? false) && ($0.value.values["state"] as? String) == "succeeded" }?.key } guard let pairId, let pair = stats[pairId]?.values else { return "" } let kinds = ["localCandidateId", "remoteCandidateId"].compactMap { key in (pair[key] as? String).flatMap { stats[$0]?.values["candidateType"] as? String } } if kinds.contains("relay") { return "relay" } return kinds.first ?? "" } private static func int(_ value: Any?) -> Int { (value as? NSNumber)?.intValue ?? (value as? Int) ?? 0 } private static func double(_ value: Any?) -> Double { (value as? NSNumber)?.doubleValue ?? (value as? Double) ?? 0 } }