oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2
3/// What this app's own screen is doing on the way out, read from one
4/// connection's statistics.
5///
6/// It exists because of a report on 2026-09-24 that "the screenshare
7/// isn't keeping up with what I'm doing on the screen", and because
8/// nothing in the app could say why: a slow screen can be the encoder
9/// giving up frames to keep resolution, a thin or still-ramping uplink,
10/// a relay in the path, or a software encoder that cannot keep up. These
11/// fields tell those apart, and `summary` says which in one line.
12public struct ScreenSendReport: Equatable, Sendable {
13 public var width = 0
14 public var height = 0
15 /// Frames a second leaving the encoder, as WebRTC measures it.
16 public var framesPerSecond = 0.0
17 /// Frames a second arriving from the capture (the media source), and
18 /// its size before any scaling.
19 public var capturedPerSecond = 0.0
20 public var sourceWidth = 0
21 public var sourceHeight = 0
22 public var framesSent: UInt64 = 0
23 public var framesEncoded: UInt64 = 0
24 public var framesCaptured: UInt64 = 0
25 /// Keyframes the encoder has made, and the keyframe requests (PLI and
26 /// FIR) that have come back from the receiver for this stream.
27 public var keyFramesEncoded: UInt64 = 0
28 public var keyframeRequests: UInt64 = 0
29 /// "none", "bandwidth", "cpu" or "other", as WebRTC names them.
30 public var limitation = ""
31 /// Seconds spent under each limitation since the stream began.
32 public var limitationSeconds: [String: Double] = [:]
33 public var targetBitrate: UInt64 = 0
34 public var bytesSent: UInt64 = 0
35 /// When the counters were read, in seconds, for turning bytes into a rate.
36 public var timestamp = 0.0
37 /// The encoder behind the stream, e.g. "VideoToolbox" or "libvpx".
38 public var encoder = ""
39 /// The codec's MIME type, e.g. "video/H264".
40 public var codec = ""
41 public var totalEncodeTime = 0.0
42 public var packetsLost: Int64 = 0
43 public var roundTripTime = 0.0
44 /// The selected path: "host", "srflx", "prflx" or "relay". A relay on
45 /// either end is reported as relay, because that is the extra hop.
46 public var path = ""
47 /// Bits a second actually sent, from the byte count between two reads.
48 /// Zero until there have been two.
49 public var sentBitrate: UInt64 = 0
50
51 public init() {}
52
53 /// Milliseconds the encoder took per frame, on average.
54 public var encodeMsPerFrame: Double {
55 framesEncoded == 0 ? 0 : totalEncodeTime * 1000 / Double(framesEncoded)
56 }
57
58 /// The rate from `previous` to this read, when both are the same stream.
59 public func measured(since previous: ScreenSendReport?) -> ScreenSendReport {
60 var next = self
61 guard let previous, timestamp > previous.timestamp, bytesSent >= previous.bytesSent else {
62 return next
63 }
64 next.sentBitrate = UInt64(
65 (Double(bytesSent - previous.bytesSent) * 8 / (timestamp - previous.timestamp)).rounded())
66 return next
67 }
68
69 /// Of several connections, the one a person would notice: the slowest.
70 public static func worst(_ reports: [ScreenSendReport]) -> ScreenSendReport? {
71 reports.min { $0.framesPerSecond < $1.framesPerSecond }
72 }
73
74 /// One line for the device panel, e.g.
75 /// "Screen: 1920×1080 at 8 fps, limited by bandwidth, 1.2 Mbit/s, relay".
76 public var summary: String {
77 if width == 0 || height == 0 { return "Screen: starting…" }
78 var parts = ["Screen: \(width)×\(height) at \(Int(framesPerSecond.rounded())) fps"]
79 switch limitation {
80 case "bandwidth": parts.append("limited by bandwidth")
81 case "cpu": parts.append("limited by the processor")
82 case "other": parts.append("limited")
83 default: break
84 }
85 let rate = sentBitrate > 0 ? sentBitrate : targetBitrate
86 if rate > 0 { parts.append(Self.megabits(rate)) }
87 if !path.isEmpty { parts.append(path == "relay" ? "relay" : "direct") }
88 var line = parts.joined(separator: ", ")
89 if !encoder.isEmpty { line += " (\(Self.encoderName(encoder, codec: codec)))" }
90 return line
91 }
92
93 /// The second line, under `summary`, while sharing: what the encoder
94 /// and the watchdog are doing, e.g. "Encoded 29 fps, sent 29 fps,
95 /// keyframes 4 (3 asked for); watchdog: watching".
96 public func detail(since previous: ScreenSendReport?, watchdog: String) -> String {
97 var encodedRate = framesPerSecond
98 var sentRate = framesPerSecond
99 if let previous, timestamp > previous.timestamp, framesEncoded >= previous.framesEncoded,
100 framesSent >= previous.framesSent
101 {
102 let seconds = timestamp - previous.timestamp
103 encodedRate = Double(framesEncoded - previous.framesEncoded) / seconds
104 sentRate = Double(framesSent - previous.framesSent) / seconds
105 }
106 var line =
107 "Encoded \(Int(encodedRate.rounded())) fps, sent \(Int(sentRate.rounded())) fps, keyframes \(keyFramesEncoded)"
108 + " (\(keyframeRequests) asked for)"
109 if !watchdog.isEmpty { line += "; watchdog: \(watchdog)" }
110 return line
111 }
112
113 static func megabits(_ bps: UInt64) -> String {
114 let mbit = Double(bps) / 1_000_000
115 return mbit >= 1 ? String(format: "%.1f Mbit/s", mbit) : "\(Int((Double(bps) / 1000).rounded())) kbit/s"
116 }
117
118 /// "VideoToolbox" means the Mac's hardware encoder, which is the thing
119 /// worth saying; anything else is software.
120 static func encoderName(_ implementation: String, codec: String) -> String {
121 let name = codec.split(separator: "/").last.map(String.init) ?? ""
122 let hardware = implementation.contains("VideoToolbox")
123 return [name, hardware ? "hardware" : "software"].filter { !$0.isEmpty }.joined(separator: " ")
124 }
125
126 /// A limitation as a number, for the gate's integer statistics.
127 public var limitationCode: UInt64 {
128 ["none": 0, "cpu": 1, "bandwidth": 2, "other": 3][limitation] ?? 0
129 }
130 /// The path as a number, for the same: 1 host, 2 srflx, 3 prflx, 4 relay.
131 public var pathCode: UInt64 {
132 ["host": 1, "srflx": 2, "prflx": 3, "relay": 4][path] ?? 0
133 }
134 public var hardwareEncoder: Bool { encoder.contains("VideoToolbox") }
135}
136
137extension ScreenSendReport {
138 /// Reads one connection's statistics for the outbound stream on `mid`.
139 /// `stats` is the report as WebRTC gives it, flattened: id to type and
140 /// values. Nil when that stream is not in it (not negotiated yet).
141 static func read(_ stats: [String: (type: String, values: [String: Any])], mid: String)
142 -> ScreenSendReport?
143 {
144 guard !mid.isEmpty,
145 let (outboundId, outbound) = stats.first(where: {
146 $0.value.type == "outbound-rtp" && ($0.value.values["mid"] as? String) == mid
147 }).map({ ($0.key, $0.value.values) })
148 else { return nil }
149 var r = ScreenSendReport()
150 r.width = int(outbound["frameWidth"])
151 r.height = int(outbound["frameHeight"])
152 r.framesPerSecond = double(outbound["framesPerSecond"])
153 r.framesSent = UInt64(int(outbound["framesSent"]))
154 r.framesEncoded = UInt64(int(outbound["framesEncoded"]))
155 r.keyFramesEncoded = UInt64(int(outbound["keyFramesEncoded"]))
156 r.keyframeRequests = UInt64(int(outbound["pliCount"]) + int(outbound["firCount"]))
157 r.limitation = outbound["qualityLimitationReason"] as? String ?? ""
158 if let durations = outbound["qualityLimitationDurations"] as? [String: Any] {
159 r.limitationSeconds = durations.mapValues { double($0) }
160 }
161 r.targetBitrate = UInt64(int(outbound["targetBitrate"]))
162 r.bytesSent = UInt64(int(outbound["bytesSent"]))
163 r.encoder = outbound["encoderImplementation"] as? String ?? ""
164 r.totalEncodeTime = double(outbound["totalEncodeTime"])
165 if let source = outbound["mediaSourceId"] as? String, let media = stats[source]?.values {
166 r.framesCaptured = UInt64(int(media["frames"]))
167 r.capturedPerSecond = double(media["framesPerSecond"])
168 r.sourceWidth = int(media["width"])
169 r.sourceHeight = int(media["height"])
170 }
171 if let codec = outbound["codecId"] as? String {
172 r.codec = stats[codec]?.values["mimeType"] as? String ?? ""
173 }
174 if let remote = stats.first(where: {
175 $0.value.type == "remote-inbound-rtp" && ($0.value.values["localId"] as? String) == outboundId
176 })?.value.values {
177 r.packetsLost = Int64(int(remote["packetsLost"]))
178 r.roundTripTime = double(remote["roundTripTime"])
179 }
180 r.path = path(stats)
181 return r
182 }
183
184 /// The selected candidate pair's kind of path.
185 static func path(_ stats: [String: (type: String, values: [String: Any])]) -> String {
186 var pairId = stats.values.first { $0.type == "transport" }?.values["selectedCandidatePairId"]
187 as? String
188 if pairId == nil {
189 pairId = stats.first {
190 $0.value.type == "candidate-pair" && ($0.value.values["nominated"] as? Bool ?? false)
191 && ($0.value.values["state"] as? String) == "succeeded"
192 }?.key
193 }
194 guard let pairId, let pair = stats[pairId]?.values else { return "" }
195 let kinds = ["localCandidateId", "remoteCandidateId"].compactMap { key in
196 (pair[key] as? String).flatMap { stats[$0]?.values["candidateType"] as? String }
197 }
198 if kinds.contains("relay") { return "relay" }
199 return kinds.first ?? ""
200 }
201
202 private static func int(_ value: Any?) -> Int {
203 (value as? NSNumber)?.intValue ?? (value as? Int) ?? 0
204 }
205 private static func double(_ value: Any?) -> Double {
206 (value as? NSNumber)?.doubleValue ?? (value as? Double) ?? 0
207 }
208}
209