oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1import Combine
2import Foundation
3import LiveKitWebRTC
4
5/// The main-thread stall harness's view into the audio start
6/// (`CallsInterop --audio-start-stall`). It runs the start the app runs —
7/// Test audio before a call, and a call's audio as a join builds it — on
8/// the real engine module, not manual rendering, while the harness
9/// watches the main thread. A test process has no microphone permission,
10/// so the module refuses right after it is asked; everything before that
11/// (building the factory, negotiating, the engine's start up to the
12/// microphone) runs as in the app. A Bluetooth headset's profile switch is
13/// stood in for by `engineStartDelay`, held on WebRTC's worker thread in
14/// the engine's "will enable" hook, which is where a real switch holds it.
15@_spi(Fixtures) public enum AudioStartProbe {
16 /// Seconds the engine's start with a microphone is held, as a Bluetooth
17 /// headset's switch to its hands-free profile holds it. Zero in the app.
18 nonisolated(unsafe) public static var engineStartDelay: TimeInterval = 0
19
20 public struct Report: Sendable {
21 /// What the test window said while it started, and when (seconds from
22 /// the start of the run).
23 public var phases: [(at: Double, phase: MicStartPhase)] = []
24 /// Seconds until `open` (or the join's audio start) returned.
25 public var startSeconds: Double = 0
26 /// Whether the module was asked for the microphone.
27 public var asked = false
28 /// Whether the module was still recording once it had all settled.
29 public var recordingAfter = false
30 public var problem: String?
31 public var detail = ""
32 }
33
34 /// Test audio before a call, on the device module: opened, and closed
35 /// once started — or, with `closeAfter`, closed partway through.
36 @MainActor public static func testAudio(closeAfter: Double? = nil) async -> Report {
37 let began = CaptureDenoiser.now()
38 var report = Report()
39 #if os(macOS)
40 let routes = AudioRoutes(defaults: UserDefaults(suiteName: "calls-stall-\(UUID().uuidString)")!)
41 #else
42 let routes = AudioRoutes()
43 #endif
44 let capture = StandaloneCapture(routes: routes, input: nil, reduceNoise: true, assumePermitted: true)
45 let test = AudioSelfTest(capture: capture, player: EnginePlayer(), routes: routes)
46 var phases: [(Double, MicStartPhase)] = []
47 let watch = test.$startPhase.sink { phases.append((CaptureDenoiser.now() - began, $0)) }
48 if let closeAfter {
49 Task { @MainActor in
50 try? await Task.sleep(for: .milliseconds(Int(closeAfter * 1000)))
51 test.close()
52 }
53 }
54 await test.open()
55 report.startSeconds = CaptureDenoiser.now() - began
56 report.problem = test.micProblem
57 test.close()
58 // The stop is queued behind the start; let it run.
59 await AudioWork.drain()
60 try? await Task.sleep(for: .milliseconds(200))
61 await AudioWork.drain()
62 watch.cancel()
63 report.phases = phases
64 report.asked = capture.audio?.recordingRequested ?? false
65 let module = capture.audio?.module
66 report.recordingAfter = await AudioWork.run { module?.isRecording ?? false }
67 report.detail = "suppressor \(test.suppressorName)"
68 return report
69 }
70
71 /// A call's audio as a join starts it: the media built as `Call.join`
72 /// builds it (MediaEngine.make), the camera and microphone tracks made
73 /// (`prepare`), the first sending and receiving streams negotiated on
74 /// the call's factory through PeerCalls, as MediaPeer negotiates them,
75 /// and then WebRTC's worker thread opening the microphone for the first
76 /// sending stream (AudioState::AddSendingStream), as it does once a
77 /// connection is up. While the worker is held there, the main thread
78 /// does what a call's does: reads the meters and the readout ten times
79 /// a second, and — half a second in — negotiates a second person's
80 /// connection.
81 @MainActor public static func callJoin(camera: VideoCapture) async -> Report {
82 let began = CaptureDenoiser.now()
83 var report = Report()
84 #if os(macOS)
85 let call = Call(playAudio: false, audioInput: nil)
86 #else
87 let call = Call(playAudio: false)
88 #endif
89 let media = await MediaEngine.make(call: call)
90 do {
91 try await media.prepare(capture: camera)
92 } catch {
93 report.problem = "prepare: \(error.localizedDescription)"
94 }
95 guard let track = media.localAudio else {
96 report.problem = "no microphone track"
97 return report
98 }
99 var loops: [SelfTestLoopback] = []
100 do {
101 loops.append(try await SelfTestLoopback.make(factory: media.factory, track: track))
102 } catch {
103 report.problem = "negotiation: \(error.localizedDescription)"
104 }
105 let module = media.audio.module
106 let opened = DispatchSemaphore(value: 0)
107 // WebRTC's own start, on its own thread: not the app's to call.
108 DispatchQueue.global(qos: .userInitiated).async {
109 if module.initRecording() == 0 { _ = module.startRecording() }
110 opened.signal()
111 }
112 let deadline = CaptureDenoiser.now() + AudioStartProbe.engineStartDelay + 1
113 var tick = 0
114 while CaptureDenoiser.now() < deadline {
115 try? await Task.sleep(for: .milliseconds(100))
116 tick += 1
117 // The call's main-thread reads each tick (sampleMic, the readout).
118 _ = media.audio.capturing
119 _ = media.audio.report()
120 _ = media.audio.denoiser.level.take()
121 if tick == 3 { _ = media.audio.diagnostics() }
122 if tick == 5 {
123 do {
124 loops.append(try await SelfTestLoopback.make(factory: media.factory, track: track))
125 } catch {
126 report.problem = "second negotiation: \(error.localizedDescription)"
127 }
128 }
129 }
130 await AudioWork.run { opened.wait() }
131 report.startSeconds = CaptureDenoiser.now() - began
132 report.asked = media.audio.recordingRequested
133 for loop in loops { loop.close() }
134 AudioWork.post { _ = module.stopRecording() }
135 media.close()
136 await AudioWork.drain()
137 report.recordingAfter = await AudioWork.run { module.isRecording }
138 report.detail = "\(loops.count) connections negotiated"
139 return report
140 }
141}
142