oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1import LiveKitWebRTC
2import XCTest
3
4@testable import CallsKit
5
6/// The camera rules from Paul's ATEM Mini report against 0.3.1: the camera
7/// remembered, followed as it comes and goes, and watched for frames.
8@MainActor final class CameraRecoveryTests: XCTestCase {
9 private let atem = CameraDevice(id: "atem", name: "ATEM Mini")
10 private let faceTime = CameraDevice(id: "facetime", name: "FaceTime HD Camera", builtIn: true)
11 private let phone = CameraDevice(id: "phone", name: "Paul’s iPhone Camera")
12
13 private func defaults() -> UserDefaults {
14 let name = "calls-camera-tests-\(UUID().uuidString)"
15 let defaults = UserDefaults(suiteName: name)!
16 addTeardownBlock { defaults.removePersistentDomain(forName: name) }
17 return defaults
18 }
19
20 private func director(_ defaults: UserDefaults, formats: Int = 2) -> CameraDirector {
21 CameraDirector(defaults: defaults, formats: { _ in formats })
22 }
23
24 // MARK: Choosing
25
26 func testJoinUsesTheRememberedCamera() {
27 let store = defaults()
28 CameraPreference(id: "atem", name: "ATEM Mini").save(store)
29 let d = director(store)
30 XCTAssertEqual(d.start(devices: [faceTime, atem]), .open("atem", format: 0))
31 XCTAssertEqual(d.status, .starting("ATEM Mini"))
32 XCTAssertEqual(d.note, "")
33 }
34
35 func testJoinFallsBackToTheBuiltInCameraAndSaysSo() {
36 let store = defaults()
37 CameraPreference(id: "atem", name: "ATEM Mini").save(store)
38 let d = director(store)
39 // The built-in camera, not merely the first listed.
40 XCTAssertEqual(d.start(devices: [phone, faceTime]), .open("facetime", format: 0))
41 XCTAssertEqual(d.note, "ATEM Mini isn’t connected; using FaceTime HD Camera")
42 }
43
44 func testWithNothingRememberedTheBuiltInCameraIsFirstChoice() {
45 let d = director(defaults())
46 XCTAssertEqual(d.start(devices: [atem, faceTime]), .open("facetime", format: 0))
47 XCTAssertEqual(d.note, "")
48 XCTAssertEqual(CameraDirector.choose([atem, phone], preferred: nil)?.id, "atem")
49 XCTAssertNil(CameraDirector.choose([], preferred: CameraPreference(id: "atem", name: "ATEM Mini")))
50 }
51
52 func testNoCameraAtAllIsSaidAndStopped() {
53 let d = director(defaults())
54 XCTAssertEqual(d.start(devices: []), .stop)
55 XCTAssertEqual(d.status, .noCamera)
56 XCTAssertEqual(d.status.tileText, "No camera found")
57 // One arriving later is used.
58 XCTAssertEqual(d.devicesChanged([atem]), .open("atem", format: 0))
59 }
60
61 func testChoosingACameraRemembersIt() {
62 let store = defaults()
63 let d = director(store)
64 _ = d.start(devices: [faceTime, atem])
65 XCTAssertEqual(d.choose("atem"), .open("atem", format: 0))
66 XCTAssertEqual(CameraPreference.load(store), CameraPreference(id: "atem", name: "ATEM Mini"))
67 XCTAssertNil(d.choose("missing"), "a camera that is not connected cannot be chosen")
68 }
69
70 // MARK: Paul's 0.3.3 report: "This keeps forgetting my input"
71 //
72 // A MacBook Air in clamshell, an ATEM Mini ("Blackmagic Design") on USB.
73 // The remembered camera was the MacBook's own, which is listed with the
74 // lid closed but sends nothing; every call fell back to the Blackmagic
75 // and said "No signal from MacBook Air Camera; using Blackmagic Design",
76 // and choosing the Blackmagic in the panel did not stick.
77
78 private let macBook = CameraDevice(
79 id: "builtin", name: "MacBook Air Camera", builtIn: true, model: "FaceTime HD Camera")
80 private let lidClosed = CameraDevice(
81 id: "builtin", name: "MacBook Air Camera", builtIn: true, model: "FaceTime HD Camera", suspended: true)
82 private let blackmagic = CameraDevice(
83 id: "0x1100000_1edb_be55", name: "Blackmagic Design", model: "UVC Camera VendorID_7899 ProductID_48725")
84
85 /// The fallback, as 0.3.3 reached it: preferred camera silent, the
86 /// Blackmagic live.
87 private func onBlackmagicByFallback(_ store: UserDefaults) -> CameraDirector {
88 CameraPreference(macBook).save(store)
89 let d = director(store, formats: 1)
90 _ = d.start(devices: [macBook, blackmagic])
91 d.opened("builtin", at: 0, frames: 0)
92 _ = d.tick(frames: 0, at: 3)
93 d.opened("builtin", at: 3, frames: 0)
94 XCTAssertEqual(d.tick(frames: 0, at: 6), .open(blackmagic.id, format: 0))
95 d.opened(blackmagic.id, at: 6, frames: 0)
96 _ = d.tick(frames: 1, at: 6.5)
97 XCTAssertEqual(d.status, .live("Blackmagic Design"))
98 return d
99 }
100
101 func testChoosingTheCameraYouAreOnByFallbackRemembersItWithoutRestartingIt() {
102 let store = defaults()
103 let d = onBlackmagicByFallback(store)
104 XCTAssertNil(d.choose(blackmagic.id), "already live: nothing to reopen, no glitch")
105 XCTAssertEqual(CameraPreference.load(store), CameraPreference(blackmagic))
106 XCTAssertEqual(d.note, "")
107 XCTAssertEqual(d.status, .live("Blackmagic Design"))
108 XCTAssertNil(d.cameraToKeep, "nothing left to offer")
109 // And the next call starts on it.
110 let next = director(store)
111 XCTAssertEqual(next.start(devices: [macBook, blackmagic]), .open(blackmagic.id, format: 0))
112 }
113
114 func testFallbacksAndDefaultsNeverWriteThePreference() {
115 let store = defaults()
116 _ = onBlackmagicByFallback(store)
117 XCTAssertEqual(CameraPreference.load(store), CameraPreference(macBook), "a fallback is not a choice")
118 let fresh = defaults()
119 let d = director(fresh)
120 _ = d.start(devices: [blackmagic, macBook])
121 _ = d.devicesChanged([blackmagic])
122 _ = d.openFailed(blackmagic.id)
123 XCTAssertNil(CameraPreference.load(fresh), "a first-run default is not a choice either")
124 }
125
126 func testTheCameraInUseIsOfferedWhenItIsNotTheRememberedOne() {
127 let store = defaults()
128 let d = onBlackmagicByFallback(store)
129 XCTAssertEqual(d.cameraToKeep, blackmagic)
130 XCTAssertEqual(d.choose(blackmagic.id), nil)
131 XCTAssertEqual(CameraPreference.load(store)?.id, blackmagic.id)
132 // Nothing remembered and on the default: nothing to offer.
133 let plain = director(defaults())
134 _ = plain.start(devices: [macBook, blackmagic])
135 plain.opened("builtin", at: 0, frames: 0)
136 _ = plain.tick(frames: 1, at: 0.5)
137 XCTAssertNil(plain.cameraToKeep)
138 }
139
140 func testAReenumeratedCameraStillMatches() {
141 // Moved to another USB port: a new uniqueID, the same model.
142 let store = defaults()
143 CameraPreference(blackmagic).save(store)
144 let moved = CameraDevice(id: "0x1400000_1edb_be55", name: "Blackmagic Design", model: blackmagic.model)
145 XCTAssertEqual(director(store).start(devices: [macBook, moved]), .open(moved.id, format: 0))
146 // A choice saved by 0.3.3 has no model: the name finds it.
147 let old = defaults()
148 CameraPreference(id: blackmagic.id, name: "Blackmagic Design").save(old)
149 let d = director(old)
150 XCTAssertEqual(d.start(devices: [macBook, moved]), .open(moved.id, format: 0))
151 XCTAssertEqual(d.note, "", "found, so not \"isn’t connected\"")
152 XCTAssertNil(d.cameraToKeep, "it is the remembered camera")
153 // The id still wins over the model when both are there.
154 let twin = CameraDevice(id: "twin", name: "Blackmagic Design", model: blackmagic.model)
155 XCTAssertEqual(director(store).start(devices: [twin, blackmagic]), .open(blackmagic.id, format: 0))
156 }
157
158 func testLidClosedSkipsTheBuiltInCameraAtJoinAndSaysWhy() {
159 let store = defaults()
160 CameraPreference(macBook).save(store)
161 let d = director(store)
162 XCTAssertEqual(d.start(devices: [lidClosed, blackmagic]), .open(blackmagic.id, format: 0))
163 XCTAssertEqual(d.note, "MacBook Air Camera is off while the lid is closed; using Blackmagic Design")
164 XCTAssertEqual(CameraPreference.load(store), CameraPreference(macBook), "skipped, not forgotten")
165 d.opened(blackmagic.id, at: 0, frames: 0)
166 _ = d.tick(frames: 1, at: 0.5)
167 XCTAssertEqual(d.cameraToKeep, blackmagic)
168 // With nothing remembered, a closed lid is not the first choice either.
169 XCTAssertEqual(director(defaults()).start(devices: [lidClosed, blackmagic]), .open(blackmagic.id, format: 0))
170 // And with nothing else, it is tried anyway.
171 XCTAssertEqual(director(defaults()).start(devices: [lidClosed]), .open("builtin", format: 0))
172 }
173
174 func testLidClosingMidCallFallsBackAtOnceAndOpeningItReturns() {
175 let store = defaults()
176 CameraPreference(macBook).save(store)
177 let d = director(store)
178 _ = d.start(devices: [macBook, blackmagic])
179 d.opened("builtin", at: 0, frames: 0)
180 _ = d.tick(frames: 1, at: 0.5)
181 XCTAssertEqual(d.devicesChanged([lidClosed, blackmagic]), .open(blackmagic.id, format: 0))
182 XCTAssertEqual(d.note, "MacBook Air Camera is off while the lid is closed; using Blackmagic Design")
183 XCTAssertEqual(d.devicesChanged([macBook, blackmagic]), .open("builtin", format: 0))
184 XCTAssertEqual(d.note, "")
185 }
186
187 func testNoSignalFromALidClosedCameraSaysSo() {
188 // The lid closed after the list was read: the watchdog finds it.
189 let store = defaults()
190 CameraPreference(macBook).save(store)
191 let d = director(store, formats: 1)
192 _ = d.start(devices: [macBook, blackmagic])
193 d.opened("builtin", at: 0, frames: 0)
194 _ = d.tick(frames: 0, at: 3)
195 d.opened("builtin", at: 3, frames: 0)
196 _ = d.devicesChanged([lidClosed, blackmagic])
197 XCTAssertEqual(d.active, blackmagic.id)
198 XCTAssertEqual(d.note, "MacBook Air Camera is off while the lid is closed; using Blackmagic Design")
199 }
200
201 // MARK: Coming and going
202
203 func testActiveCameraDisconnectingFallsBackAtOnceAndSaysSo() {
204 let store = defaults()
205 CameraPreference(id: "atem", name: "ATEM Mini").save(store)
206 let d = director(store)
207 _ = d.start(devices: [faceTime, atem])
208 d.opened("atem", at: 0, frames: 0)
209 XCTAssertEqual(d.devicesChanged([faceTime]), .open("facetime", format: 0))
210 XCTAssertEqual(d.note, "ATEM Mini disconnected; using FaceTime HD Camera")
211 }
212
213 func testRememberedCameraReconnectingIsReturnedTo() {
214 let store = defaults()
215 CameraPreference(id: "atem", name: "ATEM Mini").save(store)
216 let d = director(store)
217 _ = d.start(devices: [faceTime, atem])
218 _ = d.devicesChanged([faceTime])
219 XCTAssertEqual(d.active, "facetime")
220 // Something unrelated changing does not move the call.
221 XCTAssertNil(d.devicesChanged([faceTime, phone]))
222 XCTAssertEqual(d.devicesChanged([faceTime, phone, atem]), .open("atem", format: 0))
223 XCTAssertEqual(d.note, "", "the fallback note goes when the camera is back")
224 }
225
226 func testRememberedCameraAppearingLaterIsSwitchedTo() {
227 let store = defaults()
228 CameraPreference(id: "atem", name: "ATEM Mini").save(store)
229 let d = director(store)
230 XCTAssertEqual(d.start(devices: [faceTime]), .open("facetime", format: 0))
231 XCTAssertEqual(d.devicesChanged([faceTime, atem]), .open("atem", format: 0))
232 }
233
234 func testLastCameraDisconnectingStopsAndSaysDisconnected() {
235 let d = director(defaults())
236 _ = d.start(devices: [atem])
237 XCTAssertEqual(d.devicesChanged([]), .stop)
238 XCTAssertEqual(d.status, .disconnected("ATEM Mini"))
239 XCTAssertEqual(d.status.tileText, "Camera disconnected")
240 XCTAssertEqual(d.devicesChanged([atem]), .open("atem", format: 0), "and back when it is")
241 }
242
243 func testOpenFailureTriesAnotherCameraThenGivesUpWithRetry() {
244 let d = director(defaults())
245 _ = d.start(devices: [atem, faceTime]) // facetime (built in)
246 XCTAssertEqual(d.openFailed("facetime"), .open("atem", format: 0))
247 XCTAssertEqual(d.note, "FaceTime HD Camera would not start; using ATEM Mini")
248 XCTAssertNil(d.openFailed("atem"))
249 XCTAssertEqual(d.status, .failed("ATEM Mini"))
250 XCTAssertEqual(d.retry(), .open("facetime", format: 0), "Try again forgives both")
251 }
252
253 // MARK: The watchdog
254
255 func testWatchdogLadder() {
256 var dog = CameraWatchdog()
257 dog.opened(at: 0, frames: 0)
258 XCTAssertNil(dog.observe(frames: 0, at: 2.9))
259 XCTAssertEqual(dog.observe(frames: 0, at: 3), .restart)
260 XCTAssertNil(dog.observe(frames: 0, at: 10), "nothing more until reopened")
261 dog.opened(at: 10, frames: 0)
262 XCTAssertEqual(dog.observe(frames: 0, at: 13), .nextFormat)
263 dog.opened(at: 13, frames: 0)
264 XCTAssertEqual(dog.observe(frames: 0, at: 16), .fallback)
265 }
266
267 func testWatchdogStallAfterFramesAndRecovery() {
268 var dog = CameraWatchdog()
269 dog.opened(at: 0, frames: 0)
270 XCTAssertNil(dog.observe(frames: 5, at: 1))
271 XCTAssertTrue(dog.flowing)
272 XCTAssertNil(dog.observe(frames: 9, at: 2))
273 XCTAssertNil(dog.observe(frames: 9, at: 4.9))
274 XCTAssertEqual(dog.observe(frames: 9, at: 5), .restart)
275 dog.opened(at: 5, frames: 9)
276 XCTAssertNil(dog.observe(frames: 12, at: 6))
277 XCTAssertEqual(dog.level, 0, "frames put it back at the bottom of the ladder")
278 }
279
280 func testNoFirstFrameRestartsThenNextFormatThenFallsBack() {
281 let store = defaults()
282 CameraPreference(id: "atem", name: "ATEM Mini").save(store)
283 let d = director(store)
284 _ = d.start(devices: [faceTime, atem])
285 d.opened("atem", at: 0, frames: 0)
286 XCTAssertEqual(d.tick(frames: 0, at: 3), .open("atem", format: 0), "restart, same format")
287 d.opened("atem", at: 4, frames: 0)
288 XCTAssertEqual(d.tick(frames: 0, at: 7), .open("atem", format: 1), "then the next format")
289 d.opened("atem", at: 8, frames: 0)
290 XCTAssertEqual(d.tick(frames: 0, at: 11), .open("facetime", format: 0), "then another camera")
291 XCTAssertEqual(d.note, "No signal from ATEM Mini; using FaceTime HD Camera")
292 d.opened("facetime", at: 12, frames: 0)
293 XCTAssertNil(d.tick(frames: 3, at: 12.5))
294 XCTAssertEqual(d.status, .live("FaceTime HD Camera"))
295 // The silent ATEM is not returned to just because something else
296 // changed; only its reconnecting (a new session) forgives it.
297 XCTAssertNil(d.devicesChanged([faceTime, atem, phone]))
298 _ = d.devicesChanged([faceTime, phone])
299 XCTAssertEqual(d.devicesChanged([faceTime, phone, atem]), .open("atem", format: 0))
300 }
301
302 func testOneFormatSkipsStraightToFallback() {
303 let d = director(defaults(), formats: 1)
304 _ = d.start(devices: [atem])
305 d.opened("atem", at: 0, frames: 0)
306 XCTAssertEqual(d.tick(frames: 0, at: 3), .open("atem", format: 0))
307 d.opened("atem", at: 3, frames: 0)
308 // No next format, no other camera: give up, say so, keep listening.
309 XCTAssertNil(d.tick(frames: 0, at: 6))
310 XCTAssertEqual(d.status, .noSignal("ATEM Mini"))
311 XCTAssertEqual(d.status.tileText, "No signal from ATEM Mini — check its input")
312 XCTAssertNil(d.tick(frames: 0, at: 60), "no restarting forever")
313 XCTAssertNil(d.tick(frames: 1, at: 61))
314 XCTAssertEqual(d.status, .live("ATEM Mini"), "its signal coming back is noticed")
315 }
316
317 func testHeldClockDoesNotClimbTheLadder() {
318 let d = director(defaults())
319 _ = d.start(devices: [atem])
320 d.opened("atem", at: 0, frames: 0)
321 for second in stride(from: 1.0, through: 30, by: 1) { d.hold(frames: 0, at: second) }
322 XCTAssertNil(d.tick(frames: 0, at: 31), "the background is not a stall")
323 XCTAssertEqual(d.tick(frames: 0, at: 33), .open("atem", format: 0))
324 }
325
326 func testRestartsAreRateLimited() {
327 let d = director(defaults())
328 _ = d.start(devices: [atem])
329 XCTAssertEqual(d.restart(at: 100), .open("atem", format: 0))
330 XCTAssertNil(d.restart(at: 102))
331 XCTAssertEqual(d.restart(at: 105), .open("atem", format: 0))
332 }
333
334 // MARK: Formats
335
336 func testFrameRateIsAlwaysOneTheFormatSupports() {
337 XCTAssertEqual(CameraFormatChoice.rate(for: [1...30]), 30)
338 XCTAssertEqual(CameraFormatChoice.rate(for: [29.97...29.97]), 29.97)
339 XCTAssertEqual(CameraFormatChoice.rate(for: [25...25]), 25)
340 XCTAssertEqual(CameraFormatChoice.rate(for: [50...50, 59.94...59.94]), 50)
341 XCTAssertEqual(CameraFormatChoice.rate(for: [23.976...23.976, 60...60]), 23.976)
342 XCTAssertNil(CameraFormatChoice.rate(for: []))
343 }
344
345 func testATEMFormatsRankByRateNearestThirty() {
346 let rates: [Double] = [23.976, 24, 25, 29.97, 30, 50, 59.94, 60]
347 let formats = rates.map {
348 CameraFormatChoice.Candidate(width: 1920, height: 1080, ranges: [$0...$0])
349 }
350 let ranked = CameraFormatChoice.rank(formats).map { rates[$0] }
351 XCTAssertEqual(ranked.prefix(3), [30, 29.97, 25])
352 XCTAssertEqual(ranked.count, rates.count)
353 }
354
355 func testSmallerFormatsComeFirstAndDuplicatesGo() {
356 let formats = [
357 CameraFormatChoice.Candidate(width: 1920, height: 1080, ranges: [1...30]),
358 CameraFormatChoice.Candidate(width: 640, height: 480, ranges: [1...30]),
359 CameraFormatChoice.Candidate(width: 640, height: 480, ranges: [1...30]),
360 CameraFormatChoice.Candidate(width: 1280, height: 720, ranges: [1...60]),
361 ]
362 XCTAssertEqual(CameraFormatChoice.rank(formats), [1, 3, 0])
363 }
364
365 // MARK: The panel
366
367 func testCameraRowStates() {
368 XCTAssertEqual(CameraRowState(cameras: 0, offers: false, status: nil), .hidden)
369 XCTAssertEqual(CameraRowState(cameras: 0, offers: true, status: nil), .noCamera)
370 XCTAssertTrue(CameraRowState(cameras: 0, offers: true, status: .noCamera).offersRetry)
371 XCTAssertEqual(
372 CameraRowState(cameras: 2, offers: true, status: .live("ATEM Mini")), .picker(line: nil, retry: false))
373 XCTAssertEqual(
374 CameraRowState(cameras: 1, offers: true, status: .starting("ATEM Mini")),
375 .picker(line: "Starting ATEM Mini…", retry: false))
376 let silent = CameraRowState(cameras: 1, offers: true, status: .noSignal("ATEM Mini"))
377 XCTAssertTrue(silent.offersRetry)
378 XCTAssertEqual(
379 silent,
380 .picker(
381 line: "ATEM Mini is connected but sending no picture. Check its input, or choose another camera.",
382 retry: true))
383 XCTAssertTrue(CameraRowState(cameras: 1, offers: true, status: .failed("ATEM Mini")).offersRetry)
384 }
385
386 func testSelfTileSaysWhatItIsWaitingFor() {
387 XCTAssertEqual(CameraStatus.starting("ATEM Mini").tileText, "Starting camera…")
388 XCTAssertEqual(CameraStatus.disconnected("ATEM Mini").tileText, "Camera disconnected")
389 XCTAssertNil(CameraStatus.live("ATEM Mini").tileText)
390 }
391
392 // MARK: The managed camera, end to end with a fake backend
393
394 private final class FakeBackend: CameraBackend {
395 var devices: [CameraDevice]
396 var opened: [String] = []
397 var refuse: Set<String> = []
398 var devicesChanged: (@MainActor () -> Void)?
399 var trouble: (@MainActor () -> Void)?
400 init(_ devices: [CameraDevice]) { self.devices = devices }
401 func requestAccess() async throws {}
402 func formatCount(_ id: String) -> Int { 1 }
403 func open(_ id: String, format: Int, delegate: RTCVideoCapturerDelegate) async throws {
404 if refuse.contains(id) { throw NSError(domain: "test", code: 1) }
405 opened.append(id)
406 }
407 func close() async {}
408 }
409
410 func testManagedCameraFollowsDisconnectAndReturn() async throws {
411 let store = defaults()
412 CameraPreference(id: "atem", name: "ATEM Mini").save(store)
413 let backend = FakeBackend([faceTime, atem])
414 let camera = ManagedCamera(backend: backend, defaults: store)
415 var changes = 0
416 camera.observe { changes += 1 }
417 RTCInitializeSSL()
418 let factory = RTCPeerConnectionFactory()
419 try await camera.start(source: factory.videoSource())
420 XCTAssertEqual(camera.currentCamera, "atem")
421 XCTAssertEqual(camera.cameraStatus, .starting("ATEM Mini"))
422
423 backend.devices = [faceTime]
424 backend.devicesChanged?()
425 try await waitFor { camera.currentCamera == "facetime" }
426 XCTAssertEqual(camera.cameraNote, "ATEM Mini disconnected; using FaceTime HD Camera")
427
428 backend.devices = [faceTime, atem]
429 backend.devicesChanged?()
430 try await waitFor { camera.currentCamera == "atem" }
431 XCTAssertEqual(camera.cameraNote, "")
432 XCTAssertEqual(backend.opened, ["atem", "facetime", "atem"])
433 XCTAssertGreaterThan(changes, 0)
434 camera.stop()
435 XCTAssertNil(camera.cameraStatus)
436 }
437
438 func testManagedCameraRefusedOpenFallsBack() async throws {
439 let backend = FakeBackend([faceTime, atem])
440 backend.refuse = ["facetime"]
441 let camera = ManagedCamera(backend: backend, defaults: defaults())
442 RTCInitializeSSL()
443 try await camera.start(source: RTCPeerConnectionFactory().videoSource())
444 XCTAssertEqual(camera.currentCamera, "atem")
445 XCTAssertEqual(camera.cameraNote, "FaceTime HD Camera would not start; using ATEM Mini")
446 do {
447 try await camera.use(camera: "nothing")
448 XCTFail("an unknown camera is refused")
449 } catch {}
450 camera.stop()
451 }
452
453 func testManagedCameraOnTheLidClosedFallbackRemembersThePick() async throws {
454 let store = defaults()
455 CameraPreference(macBook).save(store)
456 let backend = FakeBackend([lidClosed, blackmagic])
457 let camera = ManagedCamera(backend: backend, defaults: store)
458 RTCInitializeSSL()
459 try await camera.start(source: RTCPeerConnectionFactory().videoSource())
460 XCTAssertEqual(backend.opened, [blackmagic.id], "the closed lid's camera is never opened")
461 XCTAssertEqual(camera.cameraNote, "MacBook Air Camera is off while the lid is closed; using Blackmagic Design")
462 // Picking the camera it is already on is remembered.
463 try await camera.use(camera: blackmagic.id)
464 XCTAssertEqual(CameraPreference.load(store), CameraPreference(blackmagic))
465 XCTAssertEqual(camera.cameraNote, "")
466 camera.stop()
467 }
468
469 private func waitFor(_ condition: @escaping @MainActor () -> Bool) async throws {
470 for _ in 0..<200 {
471 if condition() { return }
472 try await Task.sleep(for: .milliseconds(10))
473 }
474 XCTFail("condition never held")
475 }
476}
477