oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import LiveKitWebRTC
3
4/// What starts the microphone for the audio test outside a call: the
5/// test's microphone track, sent from one peer connection to another in
6/// this process, on the test's own factory.
7///
8/// WebRTC opens the microphone when an audio stream starts sending — not
9/// when a track is made — and only then does its audio transport run the
10/// processing module, and with it the capture hook, on each 10 ms of
11/// capture. Asking the audio device module to record on its own
12/// (`initAndStartRecording`) does start the module, but the capture then
13/// never reaches the processing module: tried, and the hook saw no
14/// buffers at all. A negotiated send stream is what a call has, so it is
15/// what the test has.
16///
17/// The two connections are negotiated (offer and answer, both in memory)
18/// and never connected: ICE gathers nothing (`iceTransportPolicy = .none`),
19/// so no socket is opened and nothing leaves the device. Negotiating is
20/// enough to start the stream sending into its transport, which is enough
21/// for WebRTC to open the microphone and process every buffer, as it does
22/// for a call waiting to connect. The receiving side hears nothing — no
23/// packet reaches it — and its track is silenced anyway.
24///
25/// Every call to WebRTC here is made on the audio queue (PeerCalls):
26/// applying the answer starts the engine's playout, 188–229 ms measured,
27/// and in 0.3.8 the main thread waited for it.
28final class SelfTestLoopback: @unchecked Sendable {
29 private let sender: RTCPeerConnection
30 private let receiver: RTCPeerConnection
31 /// Held until the connections are closed: a connection must never
32 /// outlive its factory.
33 private var factory: RTCPeerConnectionFactory?
34 private var closed = false
35
36 private init(sender: RTCPeerConnection, receiver: RTCPeerConnection, factory: RTCPeerConnectionFactory) {
37 self.sender = sender
38 self.receiver = receiver
39 self.factory = factory
40 }
41
42 static func make(factory: RTCPeerConnectionFactory, track: RTCAudioTrack) async throws -> SelfTestLoopback {
43 let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
44 let pair: SelfTestLoopback = try await AudioWork.run {
45 let config = RTCConfiguration()
46 config.sdpSemantics = .unifiedPlan
47 config.iceTransportPolicy = .none
48 guard let sender = factory.peerConnection(with: config, constraints: constraints, delegate: nil),
49 let receiver = factory.peerConnection(with: config, constraints: constraints, delegate: nil)
50 else { throw SelfTestLoopback.failed("the audio connection could not be made") }
51 let pair = SelfTestLoopback(sender: sender, receiver: receiver, factory: factory)
52 guard sender.add(track, streamIds: ["selftest"]) != nil else {
53 pair.close()
54 throw SelfTestLoopback.failed("the microphone could not be added")
55 }
56 return pair
57 }
58 do {
59 try await pair.negotiate(constraints)
60 } catch {
61 pair.close()
62 throw error
63 }
64 return pair
65 }
66
67 private func negotiate(_ constraints: RTCMediaConstraints) async throws {
68 let offer = try await PeerCalls.describe(sender, answer: false, constraints)
69 try await PeerCalls.set(sender, local: true, offer)
70 try await PeerCalls.set(receiver, local: false, offer)
71 await PeerCalls.read(receiver) { receiver in
72 for transceiver in receiver.transceivers {
73 (transceiver.receiver.track as? RTCAudioTrack)?.source.volume = 0
74 }
75 }
76 let answer = try await PeerCalls.describe(receiver, answer: true, constraints)
77 try await PeerCalls.set(receiver, local: true, answer)
78 try await PeerCalls.set(sender, local: false, answer)
79 }
80
81 /// Ends both, on the audio queue after anything already asked of them;
82 /// WebRTC closes the microphone with the last sending stream. Safe to
83 /// call twice.
84 func close() {
85 AudioWork.post { [self] in
86 guard !closed else { return }
87 closed = true
88 sender.close()
89 receiver.close()
90 factory = nil
91 }
92 }
93
94 private static func failed(_ why: String) -> NSError {
95 NSError(domain: "Calls", code: 6, userInfo: [NSLocalizedDescriptionKey: why])
96 }
97}
98