oficina / meet Public

Screen share and ICE renewal calls go through the audio queue too

Review of the previous commit: addScreen (signalingState, addTransceiver,
setCodecPreferences), removeScreen (stopInternal), renegotiate's
signalingState reads, updateICE's setConfiguration and the own-screen mid
were still made on the main thread, where they wait out an engine start as
everything else did. Now through PeerCalls; a share that ends while its
transceiver is being added is taken away again. The stall harness's iPhone
build takes AudioRoutes() without a defaults suite.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev d5a68836be27a6d66f947b4601b38156db13346f parent 24b83eb
2 files changed, +57 −23
  • native/apple/Sources/CallsKit/AudioStartProbe.swift +5 −1
  • native/apple/Sources/CallsKit/Media.swift +52 −22
diff --git a/native/apple/Sources/CallsKit/AudioStartProbe.swift b/native/apple/Sources/CallsKit/AudioStartProbe.swift
index d620522..dd97016 100644
--- a/native/apple/Sources/CallsKit/AudioStartProbe.swift
+++ b/native/apple/Sources/CallsKit/AudioStartProbe.swift
@@ -36,7 +36,11 @@ import LiveKitWebRTC
@MainActor public static func testAudio(closeAfter: Double? = nil) async -> Report {
let began = CaptureDenoiser.now()
var report = Report()
- let routes = AudioRoutes(defaults: UserDefaults(suiteName: "calls-stall-\(UUID().uuidString)")!)
+ #if os(macOS)
+ let routes = AudioRoutes(defaults: UserDefaults(suiteName: "calls-stall-\(UUID().uuidString)")!)
+ #else
+ let routes = AudioRoutes()
+ #endif
let capture = StandaloneCapture(routes: routes, input: nil, reduceNoise: true, assumePermitted: true)
let test = AudioSelfTest(capture: capture, player: EnginePlayer(), routes: routes)
var phases: [(Double, MicStartPhase)] = []
diff --git a/native/apple/Sources/CallsKit/Media.swift b/native/apple/Sources/CallsKit/Media.swift
index 5aff421..df3ea7d 100644
--- a/native/apple/Sources/CallsKit/Media.swift
+++ b/native/apple/Sources/CallsKit/Media.swift
@@ -244,6 +244,7 @@ import LiveKitWebRTC
]
for peer in peers.values {
guard let pc = peer.pc else { continue }
+ let ownScreen = await peer.ownScreenMid()
let report = await PeerCalls.statistics(pc)
for stat in report.statistics.values {
let kind = (stat.values["kind"] as? String) ?? (stat.values["mediaType"] as? String) ?? ""
@@ -275,7 +276,7 @@ import LiveKitWebRTC
}
if stat.type == "outbound-rtp" {
if kind == "video" {
- let field = peer.isOwnScreen(mid: mid) ? "screenBytesSent" : "videoBytesSent"
+ let field = !mid.isEmpty && mid == ownScreen ? "screenBytesSent" : "videoBytesSent"
values[field, default: 0] +=
(stat.values["bytesSent"] as? NSNumber)?.uint64Value ?? 0
}
@@ -336,7 +337,7 @@ import LiveKitWebRTC
func screenReports() async -> [String: ScreenSendReport] {
var reports: [String: ScreenSendReport] = [:]
for (id, peer) in peers {
- guard let pc = peer.pc, let mid = peer.ownScreenMid else { continue }
+ guard let pc = peer.pc, let mid = await peer.ownScreenMid() else { continue }
let report = await PeerCalls.statistics(pc)
let flat = report.statistics.mapValues { (type: $0.type, values: $0.values as [String: Any]) }
if var read = ScreenSendReport.read(flat, mid: mid) {
@@ -468,21 +469,38 @@ struct PeerAudioReading: Equatable, Sendable {
/// up each sending a screen in the place the other built for something
/// else. The browser's mutualshare gate found that; the deferred ones
/// are added when the connection settles.
+ ///
+ /// Added on the audio queue, as every call into the connection is; a
+ /// share that ended while it was being added is taken away again.
func addScreen() {
- guard let pc, !closed, screenTx == nil, let track = engine?.localScreen,
- pc.signalingState == .stable
+ guard let pc, !closed, screenTx == nil, !addingScreen, let track = engine?.localScreen, let engine
else { return }
- let setup = RTCRtpTransceiverInit()
- setup.direction = .sendOnly
- setup.streamIds = ["screen"]
- screenTx = pc.addTransceiver(with: track, init: setup)
- if let tx = screenTx, let engine {
- try? tx.setCodecPreferences(
- ScreenSendPolicy.preferringH264(engine.factory.rtpSenderCapabilities(forKind: "video").codecs),
- error: ())
+ addingScreen = true
+ let factory = engine.factory
+ Task { @MainActor [weak self] in
+ let added = await PeerCalls.read(pc) { pc -> RTCRtpTransceiver? in
+ guard pc.signalingState == .stable else { return nil }
+ let setup = RTCRtpTransceiverInit()
+ setup.direction = .sendOnly
+ setup.streamIds = ["screen"]
+ guard let tx = pc.addTransceiver(with: track, init: setup) else { return nil }
+ try? tx.setCodecPreferences(
+ ScreenSendPolicy.preferringH264(factory.rtpSenderCapabilities(forKind: "video").codecs), error: ())
+ return tx
+ }
+ guard let self else { return }
+ self.addingScreen = false
+ guard let added else { return }
+ self.screenTx = added
+ self.screenMidCache = nil
+ guard !self.closed, self.pc === pc, self.engine?.localScreen === track else {
+ self.removeScreen()
+ return
+ }
+ self.applySendPolicy()
}
- applySendPolicy()
}
+ private var addingScreen = false
/// The screen at its mode's parameters and the camera capped beside
/// it, or the camera back at its defaults when there is no screen.
@@ -517,7 +535,8 @@ struct PeerAudioReading: Equatable, Sendable {
func removeScreen() {
guard let tx = screenTx else { return }
screenTx = nil
- tx.stopInternal()
+ screenMidCache = nil
+ AudioWork.post { tx.stopInternal() }
applySendPolicy()
renegotiate()
}
@@ -527,16 +546,16 @@ struct PeerAudioReading: Equatable, Sendable {
/// owed, and made the next time it settles.
private func renegotiate() {
guard let pc, !closed else { return }
- guard pc.signalingState == .stable else {
- owesOffer = true
- return
- }
Task { @MainActor [weak self] in
+ guard await PeerCalls.read(pc, { $0.signalingState }) == .stable else {
+ self?.owesOffer = true
+ return
+ }
guard let self else { return }
if self.polite { try? await Task.sleep(nanoseconds: 200_000_000) }
self.enqueue { [weak self] in
guard let self, let pc = self.pc else { return }
- if pc.signalingState != .stable {
+ if await PeerCalls.read(pc, { $0.signalingState }) != .stable {
self.owesOffer = true
return
}
@@ -562,8 +581,18 @@ struct PeerAudioReading: Equatable, Sendable {
}
}
func isScreen(mid: String) -> Bool { !mid.isEmpty && mid == screenMid }
- func isOwnScreen(mid: String) -> Bool { !mid.isEmpty && mid == screenTx?.mid }
- var ownScreenMid: String? { screenTx?.mid.isEmpty == false ? screenTx?.mid : nil }
+ /// The mid our screen went out on, once negotiated: read on the audio
+ /// queue (a transceiver's mid is a hop to WebRTC's signalling thread),
+ /// and kept once known.
+ func ownScreenMid() async -> String? {
+ if let screenMidCache { return screenMidCache }
+ guard let pc, let tx = screenTx else { return nil }
+ let mid = await PeerCalls.read(pc) { _ in tx.mid }
+ guard !mid.isEmpty, screenTx === tx else { return nil }
+ screenMidCache = mid
+ return mid
+ }
+ private var screenMidCache: String?
func limitUplink(_ bps: Int?) {
guard let pc else { return }
AudioWork.post {
@@ -723,7 +752,8 @@ struct PeerAudioReading: Equatable, Sendable {
lastICE = ice
enqueue { [weak self] in
guard let self, let pc = self.pc else { return }
- if pc.setConfiguration(self.configuration(ice)) { try await self.offer(restart: true) }
+ let config = self.configuration(ice)
+ if await PeerCalls.read(pc, { $0.setConfiguration(config) }) { try await self.offer(restart: true) }
}
}
nonisolated func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {