oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1import AVFoundation
2import CoreVideo
3import Foundation
4import LiveKitWebRTC
5
6@MainActor final class MediaEngine {
7 weak var call: Call?
8 let factory: RTCPeerConnectionFactory
9 var localVideo: RTCVideoTrack?
10 var localAudio: RTCAudioTrack?
11 /// §5.4: the display track while this app is sharing, and nothing
12 /// otherwise. Every connection carries it on a transceiver of its own.
13 private(set) var localScreen: RTCVideoTrack?
14 private var screenCapture: ScreenCapture?
15 // Bumped by everything that ends a share, so a capture whose picker
16 // was still open when the share or the call ended is stopped when it
17 // finally answers, rather than sent.
18 private var screenAttempt = 0
19 private var capture: VideoCapture?
20 private var peers: [String: MediaPeer] = [:]
21 /// What the screen is optimised for (ScreenSendPolicy), and the size
22 /// its source was last seen at, which motion mode scales down from.
23 var screenMode = ScreenShareMode.balanced {
24 didSet { applySendPolicy() }
25 }
26 private var screenSize = (width: 0, height: 0)
27
28 /// The audio device module and processing behind the factory. See CallAudio.
29 let audio: CallAudio
30
31 /// Builds on the calling thread: only for a call touched before it
32 /// joined. A join uses `make`.
33 convenience init(call: Call) {
34 RTCInitializeSSL()
35 #if os(iOS)
36 self.init(call: call, audio: CallAudio(routes: call.audio, audioDevice: call.customAudioDevice))
37 #else
38 self.init(call: call, audio: CallAudio(routes: call.audio, audioDevice: nil, input: call.audioInput))
39 #endif
40 }
41
42 init(call: Call, audio: CallAudio) {
43 self.call = call
44 self.audio = audio
45 factory = audio.factory
46 }
47
48 /// The media for a call, with its factory and audio device module built
49 /// on the audio queue (CallAudio.make): 100–200 ms the main thread no
50 /// longer spends when someone joins.
51 static func make(call: Call) async -> MediaEngine {
52 await AudioWork.run { RTCInitializeSSL() }
53 #if os(iOS)
54 let audio = await CallAudio.make(routes: call.audio, audioDevice: call.customAudioDevice)
55 #else
56 let audio = await CallAudio.make(routes: call.audio, audioDevice: nil, input: call.audioInput)
57 #endif
58 return MediaEngine(call: call, audio: audio)
59 }
60 func prepare(capture supplied: VideoCapture?) async throws {
61 close()
62 let source = factory.videoSource()
63 localVideo = factory.videoTrack(with: source, trackId: "camera")
64 localAudio = audio.makeTrack()
65 let capture = supplied ?? CameraCapture.make()
66 self.capture = capture
67 offersCameras = capture.offersCameras
68 capture.observe { [weak self] in self?.call?.cameraChanged() }
69 try await capture.start(source: source)
70 #if os(iOS)
71 // A supplied audio device owns its session configuration (SDK contract).
72 if call?.customAudioDevice == nil {
73 let audio = RTCAudioSession.sharedInstance()
74 audio.lockForConfiguration()
75 defer { audio.unlockForConfiguration() }
76 // Not .allowBluetoothHFP: see AudioRoutes.options. At join nobody
77 // has chosen a Bluetooth microphone, so the call starts wideband
78 // and only drops to the hands-free profile if someone asks for a
79 // Bluetooth input by name.
80 try audio.setCategory(
81 AVAudioSession.Category.playAndRecord,
82 mode: AVAudioSession.Mode.videoChat,
83 options: AudioRoutes.options(bluetoothInput: false))
84 // WebRTC owns activation and interruption recovery as peers start/stop.
85 }
86 #endif
87 }
88 /// The cameras the running capture can offer, and the one it is on.
89 /// Empty before prepare, and empty for the interop executable's
90 /// synthetic frames, which is how the picker knows not to appear.
91 ///
92 /// With real cameras the list stays live after the capture has gone
93 /// (the call ended under the person), so the panel can still say what
94 /// is connected and a choice there is remembered for next time.
95 var cameras: [CameraDevice] {
96 if let capture { return capture.cameras }
97 return offersCameras ? CameraWatch.shared.cameras : []
98 }
99 var currentCamera: String? { capture?.currentCamera }
100 /// Whether this call's camera is a real one, whose row the panel always
101 /// shows. Kept past the capture, for the same reason as `cameras`.
102 private(set) var offersCameras = false
103 var cameraStatus: CameraStatus? { capture?.cameraStatus }
104 var cameraNote: String { capture?.cameraNote ?? "" }
105 var cameraToKeep: CameraDevice? { capture?.cameraToKeep }
106 func use(camera id: String) async throws {
107 if let capture {
108 try await capture.use(camera: id)
109 } else if offersCameras, let device = CameraWatch.shared.cameras.first(where: { $0.id == id }) {
110 CameraPreference(device).save()
111 }
112 }
113 func retryCamera() async {
114 CameraWatch.shared.refresh()
115 await capture?.retryCamera()
116 }
117
118 func closePeers() {
119 for (_, p) in peers { p.close() }
120 peers.removeAll()
121 }
122
123 /// Starts sending a screen (§5.4). The capture is asked first: a person
124 /// who changes their mind at the system's picker has shared nothing,
125 /// and nothing is added to any connection.
126 ///
127 /// What gives way under pressure — resolution or frame rate — is the
128 /// "Optimise for" mode's, through sender parameters (ScreenSendPolicy).
129 func startScreen(_ capture: ScreenCapture, ended: @escaping @MainActor () -> Void) async throws {
130 guard localScreen == nil else { return }
131 screenAttempt += 1
132 let attempt = screenAttempt
133 screenSize = (0, 0)
134 screenIsScreencast = ScreenSendPolicy.screencast(screenMode)
135 let source = factory.videoSource(forScreenCast: screenIsScreencast)
136 try await capture.start(source: source, ended: ended)
137 guard attempt == screenAttempt, localScreen == nil else {
138 capture.stop()
139 throw CancellationError()
140 }
141 screenCapture = capture
142 screenSourceInUse = source
143 if let size = capture.frameSize, size.width > 0, size.height > 0 { screenSize = size }
144 localScreen = factory.videoTrack(with: source, trackId: "screen")
145 for peer in peers.values { peer.addScreen() }
146 }
147 /// The source the screen's capture is writing into, for a share again
148 /// from the same capture.
149 private var screenSourceInUse: RTCVideoSource?
150 private var reshares = 0
151
152 /// Stop and Share, without the picker (spec §15.22): every connection's
153 /// screen transceiver stopped and renegotiated, a fresh track on the
154 /// same source, the capture started again on the same content, and each
155 /// connection given the new track as soon as it settles. New
156 /// transceivers mean new encoders here and new receivers and decoders at
157 /// every far end, which is what the hand-made stop and share did.
158 func reshareScreen() async {
159 guard let capture = screenCapture, let source = screenSourceInUse, localScreen != nil else { return }
160 reshares += 1
161 for peer in peers.values { peer.removeScreen() }
162 localScreen = factory.videoTrack(with: source, trackId: "screen-\(reshares)")
163 if !(await capture.restart()) { ScreenLog.info("share again: the capture carries on as it was") }
164 guard screenCapture === capture else { return }
165 // Any connection already stable takes it now; the rest as they settle.
166 for peer in peers.values { peer.addScreen() }
167 }
168
169 /// Stops sending the screen: every connection's screen transceiver is
170 /// stopped and renegotiated, so the receivers' tracks *end*, and the
171 /// capture goes last.
172 func stopScreen() {
173 screenAttempt += 1
174 guard let capture = screenCapture else { return }
175 screenCapture = nil
176 screenSourceInUse = nil
177 localScreen = nil
178 for peer in peers.values {
179 peer.removeScreen()
180 peer.screenStopped()
181 }
182 capture.stop()
183 }
184
185 // -------------------------------------------------------------
186 // The screen watchdog's hands (ScreenWatchdog, run by Call)
187 // -------------------------------------------------------------
188
189 /// What the capture has produced so far; nothing while not sharing.
190 var screenCaptureCounts: ScreenCaptureCounts? { screenCapture?.counts }
191
192 /// A fresh capture of the same content, into the same source.
193 func restartScreenCapture() async -> Bool {
194 guard let capture = screenCapture else { return false }
195 return await capture.restart()
196 }
197 func forceScreenKeyframe(_ peer: String) { peers[peer]?.forceScreenKeyframe() }
198 func recreateScreenEncoder(_ peer: String) { peers[peer]?.recreateScreenEncoder() }
199 func reAddScreen(_ peer: String) { peers[peer]?.reAddScreen() }
200
201 /// For the gate: every screen encoding switched off behind the
202 /// watchdog's back, which is what an encoder that has stopped looks
203 /// like from outside it — frames go in, and nothing comes out.
204 func stallScreenEncoderForTest() {
205 for peer in peers.values { peer.setScreenActive(false) }
206 }
207
208 /// Each peer's screen as it arrives here, for the peers whose screen is
209 /// arriving (the receiver's watchdog, and "Copy call diagnostics").
210 func remoteScreenReports() async -> [String: ScreenReceiveReport] {
211 var out: [String: ScreenReceiveReport] = [:]
212 for (id, peer) in peers {
213 guard let pc = peer.pc, peer.receivingScreen else { continue }
214 let report = await PeerCalls.statistics(pc)
215 var reading = ScreenReceiveReport()
216 for stat in report.statistics.values where stat.type == "inbound-rtp" {
217 let mid = stat.values["mid"] as? String ?? ""
218 guard peer.isScreen(mid: mid) else { continue }
219 reading.add(stat.values)
220 }
221 reading.mid = peer.screenMidHere
222 out[id] = reading
223 }
224 return out
225 }
226
227 /// Frames decoded from screens here, never going backwards: a rebuilt
228 /// screen arrives on a new receiver whose own count starts again, and
229 /// the gate's "decoding again" has to see its frames as more.
230 private var screenDecodedSeen: [String: UInt64] = [:]
231 private(set) var screenDecodedTotal: UInt64 = 0
232 private func tallyScreenDecoded(_ peer: String, _ now: UInt64) {
233 let last = screenDecodedSeen[peer] ?? 0
234 screenDecodedTotal += now >= last ? now - last : now
235 screenDecodedSeen[peer] = now
236 }
237
238 /// Puts every connection's screen and camera senders where the policy
239 /// wants them; a no-op for any that already are.
240 func applySendPolicy() {
241 for peer in peers.values { peer.applySendPolicy() }
242 }
243 /// The screen's source size, as its statistics report it.
244 func screenSource(width: Int, height: Int) {
245 guard width > 0, height > 0, (width, height) != screenSize else { return }
246 screenSize = (width, height)
247 applySendPolicy()
248 }
249 var screenPlan: EncodingPlan {
250 ScreenSendPolicy.screen(
251 screenMode, width: screenSize.width, height: screenSize.height, screencast: screenIsScreencast)
252 }
253 /// Whether this share's source was made as a screencast: fixed when the
254 /// share starts, whatever the mode is changed to afterwards.
255 private var screenIsScreencast = true
256
257 func close() {
258 stopScreen()
259 closePeers()
260 capture?.stop()
261 capture = nil
262 // On the audio queue: a track's switch waits out an engine start.
263 let video = localVideo
264 let audio = localAudio
265 AudioWork.post {
266 video?.isEnabled = false
267 audio?.isEnabled = false
268 }
269 localVideo = nil
270 localAudio = nil
271 }
272 func reconcile(state: RoomState, ice: ICEConfig) {
273 let wanted = Set(state.peers.filter { $0.peer != state.peer }.map(\.peer))
274 for id in Array(peers.keys) where !wanted.contains(id) {
275 peers.removeValue(forKey: id)?.close()
276 call?.removeRemote(id)
277 }
278 for person in state.peers where person.peer != state.peer {
279 let gen = person.gen ?? 1
280 if let old = peers[person.peer], old.generation == gen { continue }
281 peers.removeValue(forKey: person.peer)?.close()
282 call?.removeRemote(person.peer)
283 let peer = MediaPeer(
284 id: person.peer, generation: gen, polite: state.peer < person.peer, engine: self)
285 peers[person.peer] = peer
286 peer.open(ice: ice)
287 }
288 }
289 func receive(_ message: Message) { if let from = message.from { peers[from]?.receive(message) } }
290 /// §5.4's label. A peer this app has no connection to yet is ignored,
291 /// as the browser ignores it: its screen will be told apart from its
292 /// camera by the m-line when the connection comes, and a caption with
293 /// no connection behind it is a tile nothing will ever fill or clear.
294 func receiveShare(_ message: Message) {
295 guard let from = message.from, let peer = peers[from] else { return }
296 peer.share(on: message.payload?.on == true)
297 }
298 /// ICE restarts this app has started for a renewal, for the gate.
299 var iceRestarts: UInt64 = 0
300 func updateICE(_ config: ICEConfig) {
301 let now = Int64(Date().timeIntervalSince1970)
302 for p in peers.values { p.updateICE(config, now: now) }
303 }
304 /// The five audio-health fields, gathered alongside the byte counts.
305 ///
306 /// Bytes say audio arrived; they say nothing about what it sounded like.
307 /// These five separate the two ways received audio goes wrong, which is
308 /// the distinction a person reporting "it growled" cannot make for us:
309 ///
310 /// - `concealedSamples` against `totalSamplesReceived` is loss. The
311 /// decoder invented that fraction of the audio because nothing
312 /// arrived to decode.
313 /// - `insertedSamplesForDeceleration` and
314 /// `removedSamplesForAcceleration` are clock drift. The jitter
315 /// buffer is stretching or compressing the signal to keep up with a
316 /// sender whose clock runs at a different rate than this device's —
317 /// and stretching a voice is what a growl *is*.
318 ///
319 /// With the device pickers beside it, a suspect device can be ruled out
320 /// by switching away from it while watching these.
321 static let audioHealthFields = [
322 "concealedSamples", "totalSamplesReceived",
323 "insertedSamplesForDeceleration", "removedSamplesForAcceleration",
324 ]
325
326 func stats() async -> [String: UInt64] {
327 var values: [String: UInt64] = [
328 "framesDecoded": 0, "audioBytesReceived": 0, "videoBytesSent": 0, "audioBytesSent": 0,
329 "screenFramesDecoded": 0, "screenBytesSent": 0,
330 "peerConnections": UInt64(peers.count),
331 "concealedSamples": 0, "totalSamplesReceived": 0,
332 "insertedSamplesForDeceleration": 0, "removedSamplesForAcceleration": 0,
333 "jitterBufferDelayMs": 0,
334 ]
335 for (id, peer) in peers {
336 guard let pc = peer.pc else { continue }
337 let ownScreen = await peer.ownScreenMid()
338 let report = await PeerCalls.statistics(pc)
339 var screenDecoded: UInt64 = 0
340 defer { tallyScreenDecoded(id, screenDecoded) }
341 for stat in report.statistics.values {
342 let kind = (stat.values["kind"] as? String) ?? (stat.values["mediaType"] as? String) ?? ""
343 let mid = stat.values["mid"] as? String ?? ""
344 if stat.type == "inbound-rtp" {
345 if kind == "video" {
346 // A peer sharing its screen sends two video streams, and a
347 // count that added them together would let a screen stand in
348 // for a camera that had stopped. Told apart by mid, which is
349 // what the browser's own gate does.
350 let field = peer.isScreen(mid: mid) ? "screenFramesDecoded" : "framesDecoded"
351 let decoded = (stat.values["framesDecoded"] as? NSNumber)?.uint64Value ?? 0
352 values[field, default: 0] += decoded
353 if peer.isScreen(mid: mid) { screenDecoded += decoded }
354 }
355 if kind == "audio" {
356 values["audioBytesReceived", default: 0] +=
357 (stat.values["bytesReceived"] as? NSNumber)?.uint64Value ?? 0
358 for field in MediaEngine.audioHealthFields {
359 values[field, default: 0] += (stat.values[field] as? NSNumber)?.uint64Value ?? 0
360 }
361 // jitterBufferDelay is seconds as a double, and is cumulative
362 // against jitterBufferEmittedCount rather than a gauge. It is
363 // carried in milliseconds because everything else here is an
364 // integer, and a sub-millisecond buffer is not a fact anyone
365 // reading this is going to act on.
366 let delay = (stat.values["jitterBufferDelay"] as? NSNumber)?.doubleValue ?? 0
367 values["jitterBufferDelayMs", default: 0] += UInt64((delay * 1000).rounded())
368 }
369 }
370 if stat.type == "outbound-rtp" {
371 if kind == "video" {
372 let field = !mid.isEmpty && mid == ownScreen ? "screenBytesSent" : "videoBytesSent"
373 values[field, default: 0] +=
374 (stat.values["bytesSent"] as? NSNumber)?.uint64Value ?? 0
375 }
376 if kind == "audio" {
377 values["audioBytesSent", default: 0] +=
378 (stat.values["bytesSent"] as? NSNumber)?.uint64Value ?? 0
379 }
380 }
381 }
382 }
383 values["screenFramesDecodedTotal"] = screenDecodedTotal
384 return values
385 }
386
387 /// Each remote person's sound, for the meters (spec §15.20): the level
388 /// their receive tap has seen since the last look, whether the
389 /// connection is up, and — when `stats` is set, or for the people in
390 /// `detail` — the audio bytes and packets received so far and whether
391 /// the receiver is live, one `statistics(for:)` per audio receiver rather
392 /// than the whole report. For the people in `acks`, whether their
393 /// receiver reports say this app's audio reaches them (spec §15.24's
394 /// fallback for a client that does not say who it hears).
395 func peerAudio(stats: Bool, detail: Set<String> = [], acks: Set<String> = []) async -> [String: PeerAudioReading] {
396 var out: [String: PeerAudioReading] = [:]
397 for (id, peer) in peers {
398 guard let pc = peer.pc else { continue }
399 let (peak, buffers) = peer.tap.take()
400 let deep = stats || detail.contains(id)
401 let ack = acks.contains(id)
402 // Read on the audio queue, as every call into the connection is
403 // (PeerCalls): five times a second, and each one a blocking hop to
404 // WebRTC's signalling thread.
405 let (state, receivers, senders) = await PeerCalls.read(pc) { pc in
406 (
407 pc.iceConnectionState, deep ? pc.receivers.filter { $0.track?.kind == "audio" } : [],
408 ack ? pc.senders.filter { $0.track?.kind == "audio" } : []
409 )
410 }
411 var reading = PeerAudioReading(
412 peak: peak, buffers: buffers, connected: state == .connected || state == .completed,
413 connectionBad: state == .disconnected || state == .failed)
414 reading.gone = state == .failed || state == .closed
415 if deep {
416 var bytes: UInt64 = 0
417 var packets: UInt64 = 0
418 var live = false
419 for receiver in receivers {
420 if receiver.track?.readyState == .live { live = true }
421 let report = await PeerCalls.statistics(pc, for: receiver)
422 for stat in report.statistics.values where stat.type == "inbound-rtp" {
423 bytes += (stat.values["bytesReceived"] as? NSNumber)?.uint64Value ?? 0
424 packets += (stat.values["packetsReceived"] as? NSNumber)?.uint64Value ?? 0
425 }
426 }
427 reading.bytes = bytes
428 reading.packets = packets
429 reading.hasReceiver = !receivers.isEmpty
430 reading.receiverLive = live
431 }
432 if ack {
433 var sent: UInt64 = 0
434 var acked = false
435 for sender in senders {
436 let report = await PeerCalls.statistics(pc, for: sender)
437 for stat in report.statistics.values {
438 if stat.type == "outbound-rtp" {
439 sent += (stat.values["packetsSent"] as? NSNumber)?.uint64Value ?? 0
440 } else if stat.type == "remote-inbound-rtp" {
441 // Exists only once a receiver report has come back.
442 let received = (stat.values["packetsReceived"] as? NSNumber)?.uint64Value ?? 0
443 let measured = (stat.values["roundTripTimeMeasurements"] as? NSNumber)?.uint64Value ?? 0
444 if received > 0 || measured > 0 || stat.values["roundTripTime"] != nil { acked = true }
445 }
446 }
447 }
448 reading.acked = sent > 0 && acked
449 }
450 out[id] = reading
451 }
452 return out
453 }
454
455 /// Every connection's statistics at one look, for the quality report
456 /// (spec §15.26): the whole report per connection, with its generation
457 /// and which mids carry screens, read as the other statistics are.
458 func qualitySamples() async -> [String: QualityPeerSample] {
459 var out: [String: QualityPeerSample] = [:]
460 for (id, peer) in peers {
461 guard let pc = peer.pc else { continue }
462 let ownScreen = await peer.ownScreenMid()
463 let report = await PeerCalls.statistics(pc)
464 out[id] = QualityPeerSample(
465 generation: peer.generation, ownScreenMid: ownScreen, screenMid: peer.screenMidHere,
466 stats: report.statistics.mapValues { QualityStat(type: $0.type, values: $0.values) })
467 }
468 return out
469 }
470
471 /// Caps every connection's bandwidth estimate, which is what a thin
472 /// uplink does to it. For the gate, which has no thin uplink to test on;
473 /// nil takes the cap off.
474 func limitUplink(_ bps: Int?) {
475 uplinkLimit = bps
476 for peer in peers.values { peer.limitUplink(bps) }
477 }
478 private(set) var uplinkLimit: Int?
479
480 /// This app's screen on the way out, one report per connection sending
481 /// it (see ScreenSendReport).
482 func screenReports() async -> [String: ScreenSendReport] {
483 var reports: [String: ScreenSendReport] = [:]
484 for (id, peer) in peers {
485 guard let pc = peer.pc, let mid = await peer.ownScreenMid() else { continue }
486 let report = await PeerCalls.statistics(pc)
487 let flat = report.statistics.mapValues { (type: $0.type, values: $0.values as [String: Any]) }
488 if var read = ScreenSendReport.read(flat, mid: mid) {
489 read.timestamp = report.timestamp_us / 1_000_000
490 reports[id] = read
491 }
492 }
493 return reports
494 }
495}
496
497/// A transceiver direction as SDP spells it, which is how the classifier,
498/// and the browser it was ported from, compare them.
499private func sdpDirection(_ direction: RTCRtpTransceiverDirection) -> String {
500 switch direction {
501 case .sendRecv: return "sendrecv"
502 case .sendOnly: return "sendonly"
503 case .recvOnly: return "recvonly"
504 case .inactive: return "inactive"
505 case .stopped: return "stopped"
506 @unknown default: return ""
507 }
508}
509
510/// One look at a remote person's audio (see MediaEngine.peerAudio).
511struct PeerAudioReading: Equatable, Sendable {
512 var peak: Float
513 var buffers: Int
514 var connected: Bool
515 /// ICE `disconnected` or `failed` (spec §15.23's three seconds).
516 var connectionBad = false
517 /// ICE `failed` or `closed`: whatever was heard on it is gone (§15.24).
518 var gone = false
519 var bytes: UInt64?
520 /// Audio packets received, the receiver there at all, and its track
521 /// live (§15.24); read with `bytes`.
522 var packets: UInt64?
523 var hasReceiver = false
524 var receiverLive = false
525 /// Their receiver reports say this app's audio reaches them; nil when
526 /// not asked.
527 var acked: Bool?
528}
529
530@MainActor private final class MediaPeer: NSObject, RTCPeerConnectionDelegate {
531 let id: String
532 /// Measures every audio track that arrives on this connection (their
533 /// microphone; a browser's shared tab audio would add to it).
534 nonisolated let tap = RemoteAudioTap()
535 private var tapped: [RTCAudioTrack] = []
536 let generation: Int64
537 let polite: Bool
538 nonisolated let playAudio: Bool
539 weak var engine: MediaEngine?
540 var pc: RTCPeerConnection?
541 private var work: Task<Void, Never>?
542 private var closed = false
543 private var ignoreOffer = false
544 private var candidates: [Candidate] = []
545 private var lastICE: ICEConfig?
546 /// The list this connection last gathered on — built with, or last
547 /// restarted onto — whose `renewAt` says when it needs a restart.
548 private var gatheredICE: ICEConfig?
549 // §5.4, receiving. What this peer's camera and microphone came in on,
550 // and whether it has said it is sharing; the receiver its screen
551 // arrived on, so that only *that* receiver going away takes the tile
552 // with it; and the mid, so the statistics can count the two video
553 // streams apart.
554 private var clues = ScreenClues()
555 private var screenReceiver: String?
556 private var screenMid = ""
557 // Whether this connection carries a camera of ours: the classifier's
558 // direction rule turns on it.
559 private var sentVideo = false
560 // §5.4, sending: the transceiver this connection carries our screen on,
561 // while we are sharing. And an offer this connection owes the far side
562 // but could not make yet, because it was in the middle of another.
563 private var screenTx: RTCRtpTransceiver?
564 private var owesOffer = false
565
566 init(id: String, generation: Int64, polite: Bool, engine: MediaEngine) {
567 self.id = id
568 self.generation = generation
569 self.polite = polite
570 self.engine = engine
571 self.playAudio = engine.call?.playAudio ?? true
572 }
573 private func configuration(_ ice: ICEConfig) -> RTCConfiguration {
574 let c = RTCConfiguration()
575 c.sdpSemantics = .unifiedPlan
576 c.iceServers = ice.iceServers.map {
577 RTCIceServer(urlStrings: $0.urls, username: $0.username, credential: $0.credential)
578 }
579 return c
580 }
581 /// Makes the connection on the audio queue, as the first thing this
582 /// peer's work does: everything that arrives for it waits behind it.
583 /// Making one waits on WebRTC's worker thread, which can be busy
584 /// starting the audio engine for someone else (846 ms measured on the
585 /// main thread with a Bluetooth switch simulated).
586 func open(ice: ICEConfig) {
587 guard let engine else { return }
588 lastICE = ice
589 gatheredICE = ice
590 let factory = engine.factory
591 let config = configuration(ice)
592 let audio = engine.localAudio
593 let video = engine.localVideo
594 let delegate: RTCPeerConnectionDelegate = self
595 enqueue { [weak self] in
596 let (made, sent) = await AudioWork.run { () -> (RTCPeerConnection?, Bool) in
597 guard
598 let pc = factory.peerConnection(
599 with: config, constraints: RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil),
600 delegate: delegate)
601 else { return (nil, false) }
602 if let audio { _ = pc.add(audio, streamIds: ["calls"]) }
603 let sent = video.map { pc.add($0, streamIds: ["calls"]) != nil } ?? false
604 return (pc, sent)
605 }
606 guard let self, !self.closed, let made else {
607 if let made { PeerCalls.close(made) }
608 return
609 }
610 self.pc = made
611 self.sentVideo = sent
612 if let limit = self.engine?.uplinkLimit { self.limitUplink(limit) }
613 // A connection built while this app is already sharing gets the
614 // screen with everything else: someone joining mid-share sees it.
615 // Last, as in the browser, so the camera and microphone take the
616 // m-lines the far side built for them and the screen a place of its
617 // own after them.
618 self.addScreen()
619 }
620 }
621
622 /// Gives this connection the screen on a `sendonly` transceiver in a
623 /// stream of its own, which is what keeps it clear of the camera at the
624 /// far end. Adding it asks for negotiation, and perfect negotiation
625 /// carries the offer with no help from here.
626 ///
627 /// Only on a stable connection. A transceiver added mid-negotiation is
628 /// unassociated when the far side's offer arrives, and m-line matching
629 /// will hand it that offer's m-line — so two people sharing at once end
630 /// up each sending a screen in the place the other built for something
631 /// else. The browser's mutualshare gate found that; the deferred ones
632 /// are added when the connection settles.
633 ///
634 /// Added on the audio queue, as every call into the connection is; a
635 /// share that ended while it was being added is taken away again.
636 func addScreen() {
637 guard let pc, !closed, screenTx == nil, !addingScreen, let track = engine?.localScreen, let engine
638 else { return }
639 addingScreen = true
640 let factory = engine.factory
641 Task { @MainActor [weak self] in
642 let added = await PeerCalls.read(pc) { pc -> RTCRtpTransceiver? in
643 guard pc.signalingState == .stable else { return nil }
644 let setup = RTCRtpTransceiverInit()
645 setup.direction = .sendOnly
646 setup.streamIds = ["screen"]
647 guard let tx = pc.addTransceiver(with: track, init: setup) else { return nil }
648 try? tx.setCodecPreferences(
649 ScreenSendPolicy.preferringH264(factory.rtpSenderCapabilities(forKind: "video").codecs), error: ())
650 return tx
651 }
652 guard let self else { return }
653 self.addingScreen = false
654 guard let added else { return }
655 self.screenTx = added
656 self.screenMidCache = nil
657 guard !self.closed, self.pc === pc, self.engine?.localScreen === track else {
658 self.removeScreen()
659 return
660 }
661 self.applySendPolicy()
662 }
663 }
664 private var addingScreen = false
665
666 /// The screen at its mode's parameters and the camera capped beside
667 /// it, or the camera back at its defaults when there is no screen.
668 /// Sender parameters only: nothing here renegotiates.
669 ///
670 /// Applied on the audio queue: a sender's parameters are read and set on
671 /// WebRTC's worker thread, and this runs each time a negotiation settles
672 /// — just when the worker may be starting the audio engine.
673 func applySendPolicy() {
674 guard let pc, !closed, let engine else { return }
675 let screen = screenTx?.sender
676 var screenPlan = engine.screenPlan
677 if nudge > 1 { screenPlan.scaleResolutionDownBy = (screenPlan.scaleResolutionDownBy ?? 1) * nudge }
678 let cameraTrack = engine.localVideo
679 let camera = ScreenSendPolicy.camera(sharing: screenTx != nil)
680 AudioWork.post {
681 if let screen { screen.apply(screenPlan) }
682 let id = cameraTrack?.trackId
683 if let sender = pc.senders.first(where: { $0.track != nil && $0.track?.trackId == id }) {
684 sender.apply(camera)
685 }
686 }
687 }
688
689 /// Ends this connection's screen m-line. Stopping the transceiver is
690 /// §5.4's own instruction and the only thing that makes the receiver's
691 /// track end rather than freeze on its last frame.
692 ///
693 /// The framework's stop is the immediate one: it stops the transceiver
694 /// and asks for nothing, where the browser's `stop()` also raises
695 /// negotiation-needed. So the offer that tells the far side is asked
696 /// for here, by hand.
697 func removeScreen() {
698 guard let tx = screenTx else { return }
699 screenTx = nil
700 screenMidCache = nil
701 AudioWork.post { tx.stopInternal() }
702 applySendPolicy()
703 renegotiate()
704 }
705
706 /// An offer this side has to make for a reason negotiation-needed will
707 /// not announce. Made now if the connection is quiet, and otherwise
708 /// owed, and made the next time it settles.
709 private func renegotiate() {
710 guard let pc, !closed else { return }
711 Task { @MainActor [weak self] in
712 guard await PeerCalls.read(pc, { $0.signalingState }) == .stable else {
713 self?.owesOffer = true
714 return
715 }
716 guard let self else { return }
717 if self.polite { try? await Task.sleep(nanoseconds: 200_000_000) }
718 self.enqueue { [weak self] in
719 guard let self, let pc = self.pc else { return }
720 if await PeerCalls.read(pc, { $0.signalingState }) != .stable {
721 self.owesOffer = true
722 return
723 }
724 try await self.offer()
725 }
726 }
727 }
728
729 /// The connection is back at stable: the one moment a deferred screen
730 /// can be added, and an owed offer made.
731 private func settled() {
732 guard let pc, !closed else { return }
733 Task { @MainActor [weak self] in
734 // Read on the audio queue, as every call into the connection is.
735 let stable = await PeerCalls.read(pc) { $0.signalingState } == .stable
736 guard let self, stable, !self.closed, self.pc === pc else { return }
737 self.addScreen()
738 self.applySendPolicy()
739 if self.owesOffer {
740 self.owesOffer = false
741 self.renegotiate()
742 }
743 }
744 }
745 func isScreen(mid: String) -> Bool { !mid.isEmpty && mid == screenMid }
746 var screenMidHere: String { screenMid }
747 /// The mid our screen went out on, once negotiated: read on the audio
748 /// queue (a transceiver's mid is a hop to WebRTC's signalling thread),
749 /// and kept once known.
750 func ownScreenMid() async -> String? {
751 if let screenMidCache { return screenMidCache }
752 guard let pc, let tx = screenTx else { return nil }
753 let mid = await PeerCalls.read(pc) { _ in tx.mid }
754 guard !mid.isEmpty, screenTx === tx else { return nil }
755 screenMidCache = mid
756 return mid
757 }
758 private var screenMidCache: String?
759 func limitUplink(_ bps: Int?) {
760 guard let pc else { return }
761 AudioWork.post {
762 _ = pc.setBweMinBitrateBps(nil, currentBitrateBps: nil, maxBitrateBps: bps.map { NSNumber(value: $0) })
763 }
764 }
765
766 /// The `share` message: on draws the captioned tile ahead of the media,
767 /// off is one of §5.4's two endings.
768 func share(on: Bool) {
769 if on {
770 clues.sharing = true
771 engine?.call?.announceScreen(id)
772 } else {
773 dropScreen()
774 }
775 }
776 private func dropScreen() {
777 clues.sharing = false
778 screenReceiver = nil
779 screenMid = ""
780 engine?.call?.dropScreen(id)
781 }
782
783 /// One track arriving, put in its place. A camera goes on the peer's
784 /// tile exactly as it always did; a screen gets a tile of its own and
785 /// leaves the camera alone.
786 private func arrived(
787 _ track: RTCMediaStreamTrack, receiver: String, info: ArrivingTrack
788 ) {
789 let role = classifyTrack(clues, info, hasVideo: sentVideo)
790 if role == .screen, let video = track as? RTCVideoTrack {
791 clues.sharing = true
792 screenReceiver = receiver
793 if !info.mid.isEmpty { screenMid = info.mid }
794 engine?.call?.setScreen(video, peer: id)
795 return
796 }
797 if role == .camera {
798 if !info.mid.isEmpty { clues.cameraMid = info.mid }
799 if !info.streamId.isEmpty { clues.cameraStream = info.streamId }
800 }
801 // Recorded from the first track of any kind: it is what places a
802 // video track for a peer that has no camera.
803 if clues.peerStream.isEmpty { clues.peerStream = info.streamId }
804 if role == .camera, let video = track as? RTCVideoTrack {
805 engine?.call?.setRemote(video, peer: id)
806 }
807 if let audio = track as? RTCAudioTrack, !tapped.contains(where: { $0 === audio }) {
808 audio.add(tap)
809 tapped.append(audio)
810 }
811 }
812
813 func close() {
814 closed = true
815 for audio in tapped { audio.remove(tap) }
816 tapped = []
817 screenTx = nil
818 work?.cancel()
819 work = nil
820 // Closing can stop the audio engine (the last connection), which
821 // takes as long as starting it: on the audio queue.
822 if let pc { PeerCalls.close(pc) }
823 pc = nil
824 candidates = []
825 }
826 private func enqueue(_ action: @escaping @MainActor () async throws -> Void) {
827 let previous = work
828 work = Task { [weak self] in
829 await previous?.value
830 guard let self, !self.closed, !Task.isCancelled else { return }
831 do { try await action() } catch { if !self.closed { self.engine?.call?.mediaFailed() } }
832 }
833 }
834 private func send(_ type: String, _ payload: [String: Any]) {
835 guard !closed, let body = json(payload) else { return }
836 engine?.call?.send(type: type, to: id, gen: generation, payload: body)
837 }
838 // Descriptions and candidates are applied from the audio queue
839 // (PeerCalls). Applying one waits for WebRTC's worker thread, which
840 // starts the audio engine for the first stream in each direction — a
841 // Bluetooth headset's profile switch included — and up to 0.3.8 the
842 // main thread waited with it.
843 private func setLocal(_ desc: RTCSessionDescription) async throws {
844 guard let pc, !closed else { return }
845 try await PeerCalls.set(pc, local: true, desc)
846 }
847 private func setRemote(_ desc: RTCSessionDescription) async throws {
848 guard let pc, !closed else { return }
849 try await PeerCalls.set(pc, local: false, desc)
850 }
851 private func makeDescription(answer: Bool, restart: Bool = false) async throws
852 -> RTCSessionDescription
853 {
854 guard let pc else { throw CancellationError() }
855 let constraints = RTCMediaConstraints(
856 mandatoryConstraints: restart ? ["IceRestart": "true"] : nil, optionalConstraints: nil)
857 return try await PeerCalls.describe(pc, answer: answer, constraints)
858 }
859 private func offer(restart: Bool = false) async throws {
860 guard let pc, !closed, await PeerCalls.read(pc, { $0.signalingState }) == .stable, !closed else { return }
861 let desc = try await makeDescription(answer: false, restart: restart)
862 guard !closed else { return }
863 try await setLocal(desc)
864 send("offer", ["description": ["type": "offer", "sdp": desc.sdp]])
865 }
866 func receive(_ message: Message) {
867 guard message.gen == generation else { return }
868 enqueue { [weak self] in
869 guard let self, let pc = self.pc, !self.closed else { return }
870 if message.type == "candidate", let candidate = message.payload?.candidate {
871 if self.ignoreOffer { return }
872 if await PeerCalls.read(pc, { $0.remoteDescription == nil }) {
873 guard self.candidates.count < 256 else { throw CancellationError() }
874 self.candidates.append(candidate)
875 } else {
876 try await self.add(candidate)
877 }
878 return
879 }
880 guard let desc = message.payload?.description else { return }
881 let signaling = await PeerCalls.read(pc) { $0.signalingState }
882 guard !self.closed else { return }
883 if message.type == "offer", desc.type == "offer" {
884 let collision = signaling != .stable
885 self.ignoreOffer = collision && !self.polite
886 if self.ignoreOffer { return }
887 if collision { try await self.setLocal(RTCSessionDescription(type: .rollback, sdp: "")) }
888 try await self.setRemote(RTCSessionDescription(type: .offer, sdp: desc.sdp))
889 self.ignoreOffer = false
890 try await self.drain()
891 let answer = try await self.makeDescription(answer: true)
892 try await self.setLocal(answer)
893 self.send("answer", ["description": ["type": "answer", "sdp": answer.sdp]])
894 } else if message.type == "answer", desc.type == "answer", signaling == .haveLocalOffer {
895 try await self.setRemote(RTCSessionDescription(type: .answer, sdp: desc.sdp))
896 self.ignoreOffer = false
897 try await self.drain()
898 }
899 }
900 }
901 private func add(_ candidate: Candidate) async throws {
902 guard let pc, !closed, !candidate.candidate.isEmpty else { return }
903 let ice = RTCIceCandidate(
904 sdp: candidate.candidate, sdpMLineIndex: candidate.sdpMLineIndex ?? 0,
905 sdpMid: candidate.sdpMid)
906 try await PeerCalls.add(pc, ice)
907 }
908 private func drain() async throws {
909 let saved = candidates
910 candidates = []
911 for c in saved { try await add(c) }
912 }
913 /// A fresh `/ice` answer (§5.2, §5.3). The core fetches one on every
914 /// roster, and a roster answers every stream resume — every five
915 /// minutes at the stream's own TTL — so most answers are not renewals
916 /// at all. Up to 0.4.1 every one of them restarted ICE on every
917 /// connection, twelve times an hour, each one a new path for the
918 /// screen and camera to find their way back onto. Now only a renewal
919 /// that is due restarts (ICERenewal.restart); anything else is adopted
920 /// quietly, for whatever this connection gathers next.
921 func updateICE(_ ice: ICEConfig, now: Int64) {
922 guard ice.renewAt != lastICE?.renewAt || ice.iceServers.count != lastICE?.iceServers.count else { return }
923 lastICE = ice
924 let restart = ICERenewal.restart(gathered: gatheredICE, next: ice, now: now)
925 if restart { gatheredICE = ice }
926 enqueue { [weak self] in
927 guard let self, let pc = self.pc else { return }
928 let config = self.configuration(ice)
929 guard await PeerCalls.read(pc, { $0.setConfiguration(config) }) else { return }
930 if restart {
931 ScreenLog.info("ICE restart with \(ScreenWatchdog.short(self.id)): renewal due")
932 self.engine?.iceRestarts += 1
933 try await self.offer(restart: true)
934 }
935 }
936 }
937
938 // -------------------------------------------------------------
939 // The screen watchdog's hands, for this connection
940 // -------------------------------------------------------------
941
942 /// Whether this peer's screen is arriving here.
943 var receivingScreen: Bool { screenReceiver != nil && !screenMid.isEmpty }
944
945 /// The screen's encoding switched off and on again. WebRTC starts a
946 /// stream that comes back on with a keyframe, which is the one thing a
947 /// receiver stuck on a broken picture needs; there is no API on this
948 /// build of WebRTC to ask for one directly.
949 func forceScreenKeyframe() {
950 setScreenActive(false)
951 Task { @MainActor [weak self] in
952 try? await Task.sleep(nanoseconds: 150_000_000)
953 self?.setScreenActive(true)
954 }
955 }
956
957 func setScreenActive(_ active: Bool) {
958 guard let sender = screenTx?.sender else { return }
959 AudioWork.post {
960 let parameters = sender.parameters
961 guard let encoding = parameters.encodings.first, encoding.isActive != active else { return }
962 encoding.isActive = active
963 sender.parameters = parameters
964 }
965 }
966
967 /// A fresh encoder: the screen sent smaller for a while. A change of
968 /// size is what makes WebRTC set its encoder up again from nothing — for
969 /// H.264 on a Mac, a new VideoToolbox session, which is the part that
970 /// can go bad under a sleep or a GPU switch and hold the frames it was
971 /// given. The other way an encoder stops is a frame it cannot take at
972 /// all (the level's block count, see ScreenSendPolicy), which a smaller
973 /// size is also the way out of. A third off each side each time, down to
974 /// a quarter; full size again after thirty seconds, and if that stops it
975 /// again the watchdog comes back round, after its rest.
976 func recreateScreenEncoder() {
977 guard screenTx != nil else { return }
978 nudge = min(nudge * 1.5, 4)
979 nudges += 1
980 let mine = nudges
981 ScreenLog.info("screen to \(ScreenWatchdog.short(id)) now sent at 1/\(String(format: "%.2f", nudge)) of its size")
982 applySendPolicy()
983 setScreenActive(true)
984 Task { @MainActor [weak self] in
985 try? await Task.sleep(nanoseconds: 30_000_000_000)
986 guard let self, self.nudges == mine, self.nudge > 1 else { return }
987 self.nudge = 1
988 ScreenLog.info("screen to \(ScreenWatchdog.short(self.id)) back to full size")
989 self.applySendPolicy()
990 }
991 }
992 private var nudges = 0
993 private var nudge = 1.0
994
995 /// What stopping and sharing again did, on this connection alone: its
996 /// screen transceiver stopped and renegotiated, and a new one added as
997 /// soon as that settles (`settled` adds the screen to a connection that
998 /// has none while the app is sharing).
999 func reAddScreen() {
1000 guard screenTx != nil else { return }
1001 removeScreen()
1002 }
1003 /// The share has stopped: the next one starts at full size.
1004 func screenStopped() { nudge = 1 }
1005
1006 nonisolated func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {
1007 Task { @MainActor [weak self] in
1008 guard let self else { return }
1009 // Give the higher-id peer first chance, matching the browser's
1010 // polite yield. Perfect negotiation still handles actual glare.
1011 if self.polite { try? await Task.sleep(nanoseconds: 200_000_000) }
1012 self.enqueue { [weak self] in try await self?.offer() }
1013 }
1014 }
1015 nonisolated func peerConnection(
1016 _ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate
1017 ) {
1018 Task { @MainActor [weak self] in
1019 self?.send(
1020 "candidate",
1021 [
1022 "candidate": [
1023 "candidate": candidate.sdp, "sdpMid": candidate.sdpMid as Any? ?? NSNull(),
1024 "sdpMLineIndex": candidate.sdpMLineIndex,
1025 ]
1026 ])
1027 }
1028 }
1029 nonisolated func peerConnection(
1030 _ peerConnection: RTCPeerConnection, didAdd rtpReceiver: RTCRtpReceiver,
1031 streams: [RTCMediaStream]
1032 ) {
1033 // Keep decoding and RTP statistics available when a test disables speakers.
1034 if let audio = rtpReceiver.track as? RTCAudioTrack, !playAudio {
1035 audio.source.volume = 0
1036 }
1037 guard let track = rtpReceiver.track else { return }
1038 // Read here, on the signalling thread this callback arrives on, rather
1039 // than on the main actor: by the time the main actor gets to it the
1040 // next description can have moved the transceiver on.
1041 let receiver = rtpReceiver.receiverId
1042 let transceiver = peerConnection.transceivers.first { $0.receiver.receiverId == receiver }
1043 var info = ArrivingTrack(kind: track.kind, streamId: streams.first?.streamId ?? "")
1044 if let transceiver {
1045 info.mid = transceiver.mid
1046 // The negotiated direction when there is one. On the side that
1047 // answers an offer there is not yet, and that is exactly the side a
1048 // screen arrives at; the direction the transceiver was built with is
1049 // recvonly for an m-line the far side added, which is the point.
1050 var current = RTCRtpTransceiverDirection.inactive
1051 info.direction = sdpDirection(
1052 transceiver.currentDirection(&current) ? current : transceiver.direction)
1053 }
1054 Task { @MainActor [weak self] in
1055 guard let self, !self.closed else { return }
1056 self.arrived(track, receiver: receiver, info: info)
1057 }
1058 }
1059 /// §5.4's other ending: the sender stopped its screen's transceiver and
1060 /// the renegotiation took the receiver away. Only the receiver the
1061 /// screen is showing from counts. A screen replaced on its own mid ends
1062 /// its predecessor afterwards, and without the check that took the live
1063 /// replacement's tile with it (the browser's Codex S4-15).
1064 nonisolated func peerConnection(
1065 _ peerConnection: RTCPeerConnection, didRemove rtpReceiver: RTCRtpReceiver
1066 ) {
1067 let receiver = rtpReceiver.receiverId
1068 Task { @MainActor [weak self] in
1069 guard let self, !self.closed, self.screenReceiver == receiver else { return }
1070 self.dropScreen()
1071 }
1072 }
1073 nonisolated func peerConnection(
1074 _ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState
1075 ) {
1076 if newState == .failed { Task { @MainActor [weak self] in self?.engine?.call?.mediaFailed() } }
1077 }
1078 nonisolated func peerConnection(
1079 _ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState
1080 ) {
1081 guard stateChanged == .stable else { return }
1082 Task { @MainActor [weak self] in self?.settled() }
1083 }
1084 nonisolated func peerConnection(
1085 _ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream
1086 ) {}
1087 nonisolated func peerConnection(
1088 _ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream
1089 ) {}
1090 nonisolated func peerConnection(
1091 _ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState
1092 ) {}
1093 nonisolated func peerConnection(
1094 _ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]
1095 ) {}
1096 nonisolated func peerConnection(
1097 _ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel
1098 ) {}
1099}
1100