oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1import XCTest
2
3@testable import CallsKit
4
5/// AudioHealth is the one part of the device work with real logic in it,
6/// and the one that has to be right: it is what a person reads when a
7/// call sounds wrong, and on macOS it is the only instrument there is.
8final class AudioHealthTests: XCTestCase {
9 func testNothingReceivedIsSaidPlainlyAndNeverDividedBy() {
10 let health = AudioHealth(concealed: 0, total: 0, inserted: 0, removed: 0, jitterBufferMs: 0)
11 XCTAssertTrue(health.silent)
12 // Not 0% clean: no audio is not clean audio, and a rate computed from
13 // an empty denominator would say the opposite of the truth.
14 XCTAssertEqual(health.concealmentRate, 0)
15 XCTAssertEqual(health.driftRate, 0)
16 XCTAssertEqual(health.summary, "No audio received yet.")
17 }
18
19 func testDriftIsNamedAheadOfLossWhenBothAreUp() {
20 // The growl case. Loss is common, transient and widely understood;
21 // sustained drift is rarer, is what makes a voice sound *wrong*
22 // rather than broken, and is the one nobody thinks to look for. With
23 // both raised, the rarer diagnosis is the useful one to lead with.
24 let health = AudioHealth(
25 concealed: 20_000, total: 100_000, inserted: 4_000, removed: 1_000, jitterBufferMs: 300)
26 XCTAssertEqual(health.driftRate, 0.05, accuracy: 0.0001)
27 XCTAssertEqual(health.concealmentRate, 0.20, accuracy: 0.0001)
28 XCTAssertTrue(health.summary.contains("resampled"), health.summary)
29 XCTAssertTrue(health.summary.contains("growling"), health.summary)
30 }
31
32 func testLossAloneReadsAsDropoutsAndNotAsDrift() {
33 let health = AudioHealth(
34 concealed: 10_000, total: 100_000, inserted: 0, removed: 0, jitterBufferMs: 40)
35 XCTAssertTrue(health.summary.contains("did not arrive"), health.summary)
36 XCTAssertTrue(health.summary.contains("dropouts"), health.summary)
37 XCTAssertFalse(health.summary.contains("growling"), health.summary)
38 }
39
40 func testAHealthyCallSaysSoWithItsNumbers() {
41 let health = AudioHealth(
42 concealed: 500, total: 100_000, inserted: 100, removed: 100, jitterBufferMs: 30)
43 XCTAssertFalse(health.silent)
44 XCTAssertTrue(health.summary.contains("cleanly"), health.summary)
45 // The numbers ride along even when the verdict is good: someone
46 // comparing two calls needs them, and rounding to zero is honest at
47 // this resolution.
48 XCTAssertTrue(health.summary.contains("0%"), health.summary)
49 }
50
51 func testDriftCountsBothDirections() {
52 // Inserting and removing are the same fault seen from two sides — the
53 // signal was rescaled either way. A reading that counted only one
54 // would miss a sender whose clock runs fast.
55 let fast = AudioHealth(concealed: 0, total: 1_000, inserted: 0, removed: 50, jitterBufferMs: 0)
56 let slow = AudioHealth(concealed: 0, total: 1_000, inserted: 50, removed: 0, jitterBufferMs: 0)
57 XCTAssertEqual(fast.driftRate, slow.driftRate)
58 XCTAssertTrue(fast.summary.contains("resampled"), fast.summary)
59 XCTAssertTrue(slow.summary.contains("resampled"), slow.summary)
60 }
61
62 func testItReadsTheStatsDictionaryTheEngineActuallyProduces() {
63 // The field names are libwebrtc's, spelled once in MediaEngine and
64 // once here. This is the test that fails if they stop agreeing.
65 let health = AudioHealth([
66 "concealedSamples": 7, "totalSamplesReceived": 70,
67 "insertedSamplesForDeceleration": 3, "removedSamplesForAcceleration": 4,
68 "jitterBufferDelayMs": 120,
69 ])
70 XCTAssertEqual(health.concealed, 7)
71 XCTAssertEqual(health.total, 70)
72 XCTAssertEqual(health.inserted, 3)
73 XCTAssertEqual(health.removed, 4)
74 XCTAssertEqual(health.jitterBufferMs, 120)
75 for field in MediaEngine.audioHealthFields {
76 XCTAssertTrue(
77 ["concealedSamples", "totalSamplesReceived", "insertedSamplesForDeceleration",
78 "removedSamplesForAcceleration"].contains(field),
79 "MediaEngine gathers \(field), which AudioHealth does not read")
80 }
81 }
82
83 func testAMissingFieldIsZeroAndNotACrash() {
84 // An older or newer libwebrtc that stops reporting one of these
85 // should cost the readout a number, not the call a panel.
86 let health = AudioHealth(["totalSamplesReceived": 1_000])
87 XCTAssertEqual(health.concealed, 0)
88 XCTAssertFalse(health.silent)
89 XCTAssertTrue(health.summary.contains("cleanly"), health.summary)
90 }
91}
92
93/// AudioRoutes on macOS reads and chooses devices on the call's own
94/// WebRTC audio device module. These run against the real machine and a
95/// real module (no recording is started), because the thing worth testing
96/// is that the module's lists come through named and choosable.
97final class AudioRoutesTests: XCTestCase {
98 @MainActor func testMacListsTheModuleDevicesAndCanChoose() throws {
99 #if os(macOS)
100 let suite = "calls-audio-module-\(UUID().uuidString)"
101 let store = UserDefaults(suiteName: suite)!
102 defer { store.removePersistentDomain(forName: suite) }
103 let routes = AudioRoutes(defaults: store)
104 XCTAssertTrue(routes.inputs.isEmpty, "no module yet, so nothing to list")
105 XCTAssertThrowsError(try routes.use(input: "anything"))
106
107 let audio = CallAudio(routes: routes, audioDevice: nil)
108 XCTAssertTrue(routes.selectable)
109 // The regression behind Paul's "not picking up the Scarlett at
110 // launch": the engine module lists only its "default" entry until a
111 // connection initialises it. The pickers come from CoreAudio, which
112 // is complete now.
113 XCTAssertEqual(
114 audio.factory.audioDeviceModule.inputDevices.map(\.deviceId), ["default"],
115 "LiveKit's module has not enumerated before the first connection")
116 XCTAssertEqual(
117 routes.inputs.count, CoreAudioDevices.inventory().inputs.count + 1,
118 "every CoreAudio input, and System default, before any connection")
119 // What this machine lists, for the record: every input device macOS
120 // has, named as macOS names it, the default among them.
121 for device in routes.inputs { print("audio input: \(device.name) [\(device.id)]") }
122 for device in routes.outputs { print("audio output: \(device.name) [\(device.id)]") }
123 // A machine with no input at all is possible, so this asserts the
124 // shape rather than the presence.
125 for device in routes.inputs + routes.outputs {
126 XCTAssertFalse(device.name.isEmpty, "a listed device must have a name")
127 XCTAssertTrue(device.selectable)
128 }
129 if let current = routes.currentInput, !routes.inputs.isEmpty {
130 XCTAssertTrue(routes.inputs.contains { $0.id == current }, "the current input is one listed")
131 }
132 XCTAssertThrowsError(try routes.use(input: "no-such-device")) { error in
133 XCTAssertEqual((error as NSError).code, 5)
134 }
135 if let first = routes.inputs.first {
136 XCTAssertNoThrow(try routes.use(input: first.id))
137 XCTAssertEqual(routes.currentInput, first.id)
138 }
139 // A device other than the default: the choice must hold. Choosing
140 // only records it; no engine is running, so nothing records.
141 if let other = routes.inputs.first(where: { !$0.name.hasPrefix("System default") }) {
142 XCTAssertNoThrow(try routes.use(input: other.id))
143 XCTAssertEqual(routes.currentInput, other.id)
144 routes.reset()
145 XCTAssertEqual(routes.currentInput, other.id, "leaving keeps the choice for next time")
146 }
147 #else
148 throw XCTSkip("macOS only")
149 #endif
150 }
151}
152
153/// The readout before the microphone has started. Paul's 0.3.1 screenshot,
154/// alone in a call, read "The microphone runs at 16 kHz … Echo
155/// cancellation: off. Noise suppression: off." Those are exactly what the
156/// module says before any microphone opens: WebRTC opens it when the first
157/// connection starts sending, and until then its processing sits at its
158/// construction default of 16 kHz with nothing applied.
159final class ProcessingReadoutTests: XCTestCase {
160 func testNotStartedSaysSoInsteadOfStaleValues() {
161 let report = ProcessingReport(
162 suppression: .unsupportedRate(16_000), echoCancellation: "off", noiseSuppression: "off",
163 gainControl: "off", refused: nil, started: false)
164 XCTAssertFalse(report.summary.contains("16 kHz"))
165 XCTAssertFalse(report.summary.contains("Echo cancellation: off"))
166 XCTAssertTrue(report.summary.hasPrefix("The microphone has not started yet."))
167 }
168
169 @MainActor func testAloneInACallTheMicrophoneHasNotStarted() throws {
170 #if os(macOS)
171 let suite = "calls-readout-\(UUID().uuidString)"
172 let store = UserDefaults(suiteName: suite)!
173 defer { store.removePersistentDomain(forName: suite) }
174 let audio = CallAudio(routes: AudioRoutes(defaults: store), audioDevice: nil)
175 let track = audio.makeTrack()
176 _ = track
177 let report = audio.report()
178 // The evidence: with a track made and nothing sending, the module is
179 // not recording, and the processing reports the 16 kHz default with
180 // echo cancellation off — the screenshot's numbers.
181 XCTAssertFalse(audio.capturing)
182 print("pre-start readout: suppression=\(audio.denoiser.state) ec=\(report.echoCancellation) ns=\(report.noiseSuppression) gain=\(report.gainControl)")
183 XCTAssertFalse(report.started)
184 XCTAssertTrue(report.summary.hasPrefix("The microphone has not started yet."))
185 #else
186 throw XCTSkip("macOS only")
187 #endif
188 }
189}
190