| 1 | import Foundation |
| 2 | import 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. |
| 28 | final 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 | |