| 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) { |