| 1 | import CallsKit |
| 2 | import LiveKitWebRTC |
| 3 | import XCTest |
| 4 | |
| 5 | final class CallsPhoneTests: XCTestCase { |
| 6 | @MainActor func testNativeVideoAndRenewal() async throws { |
| 7 | guard let text = ProcessInfo.processInfo.environment["CALLS_TEST_MEDIA"], |
| 8 | let data = text.data(using: .utf8) |
| 9 | else { |
| 10 | throw XCTSkip("Run the Go iPhone media harness for temporary sessions and a meeting.") |
| 11 | } |
| 12 | struct Fixture: Decodable { |
| 13 | let origin: String |
| 14 | let room: String |
| 15 | let cookiesA: String |
| 16 | let cookiesB: String |
| 17 | } |
| 18 | let input = try JSONDecoder().decode(Fixture.self, from: data) |
| 19 | // No hardware capture or playback in this simulator video test. |
| 20 | let a = Call(audioDevice: SilentAudioDevice()) |
| 21 | let b = Call(audioDevice: SilentAudioDevice()) |
| 22 | defer { |
| 23 | a.close() |
| 24 | b.close() |
| 25 | } |
| 26 | // Synthetic cameras behind the app's own camera management, as the Mac |
| 27 | // gate has them; each call its own defaults, never the simulator's. |
| 28 | func synthetic() -> ManagedCamera { |
| 29 | ManagedCamera( |
| 30 | backend: SyntheticCameras(), |
| 31 | defaults: UserDefaults(suiteName: "calls-phone-test-\(UUID().uuidString)")!) |
| 32 | } |
| 33 | try await a.join( |
| 34 | origin: input.origin, room: input.room, cookies: input.cookiesA, capture: synthetic()) |
| 35 | a.mute() |
| 36 | try await b.join( |
| 37 | origin: input.origin, room: input.room, cookies: input.cookiesB, capture: synthetic()) |
| 38 | b.mute() |
| 39 | var decodedA: UInt64 = 0 |
| 40 | var decodedB: UInt64 = 0 |
| 41 | for _ in 0..<80 { |
| 42 | decodedA = await a.sampleStats()["framesDecoded", default: 0] |
| 43 | decodedB = await b.sampleStats()["framesDecoded", default: 0] |
| 44 | if decodedA > 15 && decodedB > 15 { break } |
| 45 | try await Task.sleep(for: .milliseconds(500)) |
| 46 | } |
| 47 | XCTAssertGreaterThan(decodedA, 15, "First iPhone peer must decode the other peer's video") |
| 48 | XCTAssertGreaterThan(decodedB, 15, "Second iPhone peer must decode the other peer's video") |
| 49 | XCTAssertEqual(a.state?.phase, "admitted") |
| 50 | XCTAssertEqual(b.state?.phase, "admitted") |
| 51 | let peerA = a.state?.peer |
| 52 | let peerB = b.state?.peer |
| 53 | try await Task.sleep(for: .seconds(8)) |
| 54 | let renewedA = await a.sampleStats() |
| 55 | let renewedB = await b.sampleStats() |
| 56 | XCTAssertGreaterThan(renewedA["framesDecoded", default: 0], decodedA) |
| 57 | XCTAssertGreaterThan(renewedB["framesDecoded", default: 0], decodedB) |
| 58 | XCTAssertEqual(a.state?.peer, peerA) |
| 59 | XCTAssertEqual(b.state?.peer, peerB) |
| 60 | await b.leave() |
| 61 | for _ in 0..<20 { |
| 62 | if a.remoteTracks.isEmpty { break } |
| 63 | try await Task.sleep(for: .milliseconds(250)) |
| 64 | } |
| 65 | XCTAssertTrue(a.remoteTracks.isEmpty, "Departure must remove the remote track") |
| 66 | await a.leave() |
| 67 | } |
| 68 | } |
| 69 | |