oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1import CoreVideo
2import LiveKitWebRTC
3import XCTest
4
5@testable import CallsBroadcastKit
6@testable import CallsKit
7
8/// The iPhone's screen travels from the broadcast extension to the app
9/// over a Unix socket, and nothing about that needs an iPhone: the same
10/// sender and inbox run here, over a real socket, with real pixel
11/// buffers. What cannot run here is ReplayKit itself, the App Group
12/// container and the extension's memory ceiling — those need a device.
13final class BroadcastTests: XCTestCase {
14 private var path = ""
15
16 override func setUp() {
17 // Short, because a socket path has to fit in 104 bytes.
18 path = "/tmp/calls-\(UUID().uuidString.prefix(8)).sock"
19 }
20 override func tearDown() { unlink(path) }
21
22 func testTheHeaderRoundTripsAndRefusesWhatItCannotUse() {
23 let header = BroadcastFrameHeader(
24 width: 640, height: 360, orientation: 8, timestampNs: 123_456_789, videoRange: true)
25 var bytes = [UInt8](repeating: 0, count: BroadcastFrameHeader.size)
26 bytes.withUnsafeMutableBytes { header.write(to: $0.baseAddress!) }
27 XCTAssertEqual(bytes.withUnsafeBytes { BroadcastFrameHeader.read($0.baseAddress!) }, header)
28 // Odd sizes, absurd sizes, a length that disagrees with them, and a
29 // stranger's magic are all refused: the length is what gets allocated.
30 for (offset, value) in [(8, UInt32(641)), (12, UInt32(100_000)), (24, UInt32(12)), (0, UInt32(7))] {
31 var bad = bytes
32 bad.withUnsafeMutableBytes { $0.storeBytes(of: value.littleEndian, toByteOffset: offset, as: UInt32.self) }
33 XCTAssertNil(bad.withUnsafeBytes { BroadcastFrameHeader.read($0.baseAddress!) }, "offset \(offset)")
34 }
35 }
36
37 func testAFrameCrossesTheSocketSmallerAndIntact() throws {
38 let inbox = BroadcastInbox(path: path)
39 let connected = expectation(description: "connected")
40 let arrived = expectation(description: "frame")
41 var received: (CVPixelBuffer, UInt16)?
42 inbox.onConnection = { connection in
43 connection.onFrame = { pixels, orientation in
44 guard received == nil else { return }
45 received = (pixels, orientation)
46 arrived.fulfill()
47 }
48 connection.resume()
49 connected.fulfill()
50 }
51 try inbox.start()
52 defer { inbox.stop() }
53
54 let sender = BroadcastSender(path: path, longest: 640, framesPerSecond: 100)
55 XCTAssertTrue(sender.connect(timeout: 2))
56 defer { sender.close() }
57 wait(for: [connected], timeout: 2)
58 // A 1280x720 frame, luma 100 and chroma (60, 200) throughout, as
59 // ReplayKit's full-range bi-planar buffers are laid out.
60 sender.send(try nv12(width: 1280, height: 720), orientation: 6, timestampNs: 1)
61 wait(for: [arrived], timeout: 2)
62
63 let (pixels, orientation) = try XCTUnwrap(received)
64 XCTAssertEqual(orientation, 6)
65 XCTAssertEqual(CVPixelBufferGetWidth(pixels), 640)
66 XCTAssertEqual(CVPixelBufferGetHeight(pixels), 360)
67 XCTAssertEqual(
68 CVPixelBufferGetPixelFormatType(pixels), kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
69 CVPixelBufferLockBaseAddress(pixels, .readOnly)
70 defer { CVPixelBufferUnlockBaseAddress(pixels, .readOnly) }
71 let luma = CVPixelBufferGetBaseAddressOfPlane(pixels, 0)!.assumingMemoryBound(to: UInt8.self)
72 let chroma = CVPixelBufferGetBaseAddressOfPlane(pixels, 1)!.assumingMemoryBound(to: UInt8.self)
73 let lumaStride = CVPixelBufferGetBytesPerRowOfPlane(pixels, 0)
74 let chromaStride = CVPixelBufferGetBytesPerRowOfPlane(pixels, 1)
75 XCTAssertEqual(luma[0], 100)
76 XCTAssertEqual(luma[359 * lumaStride + 639], 100)
77 XCTAssertEqual(chroma[0], 60)
78 XCTAssertEqual(chroma[1], 200)
79 XCTAssertEqual(chroma[179 * chromaStride + 638], 60)
80 XCTAssertEqual(chroma[179 * chromaStride + 639], 200)
81 }
82
83 func testTheExtensionGoingAwayEndsTheConnection() throws {
84 let inbox = BroadcastInbox(path: path)
85 let closed = expectation(description: "closed")
86 inbox.onConnection = { connection in
87 connection.onClose = { closed.fulfill() }
88 connection.resume()
89 }
90 try inbox.start()
91 defer { inbox.stop() }
92 let sender = BroadcastSender(path: path)
93 XCTAssertTrue(sender.connect(timeout: 2))
94 sender.close()
95 wait(for: [closed], timeout: 2)
96 }
97
98 func testStoppingInTheAppReachesTheExtension() throws {
99 // The app closing its end is the whole of "stop sharing": the
100 // extension learns it from its next write, and ends the broadcast.
101 let inbox = BroadcastInbox(path: path)
102 var held: BroadcastConnection?
103 let connected = expectation(description: "connected")
104 inbox.onConnection = { connection in
105 held = connection
106 connection.resume()
107 connected.fulfill()
108 }
109 try inbox.start()
110 defer { inbox.stop() }
111 let sender = BroadcastSender(path: path, longest: 64, framesPerSecond: 1000)
112 let gone = expectation(description: "extension told")
113 sender.onClosed = { gone.fulfill() }
114 XCTAssertTrue(sender.connect(timeout: 2))
115 wait(for: [connected], timeout: 2)
116 held?.close()
117 let frame = try nv12(width: 64, height: 64)
118 let until = Date().addingTimeInterval(2)
119 while Date() < until {
120 sender.send(frame, orientation: 1, timestampNs: 0)
121 if XCTWaiter.wait(for: [gone], timeout: 0.05) == .completed { return }
122 }
123 XCTFail("the extension never learned the app had stopped")
124 }
125
126 func testOnlyOneBroadcastAtATime() throws {
127 let inbox = BroadcastInbox(path: path)
128 var connections = 0
129 let first = expectation(description: "first")
130 inbox.onConnection = { connection in
131 connections += 1
132 connection.resume()
133 first.fulfill()
134 }
135 try inbox.start()
136 defer { inbox.stop() }
137 let one = BroadcastSender(path: path)
138 XCTAssertTrue(one.connect(timeout: 2))
139 defer { one.close() }
140 wait(for: [first], timeout: 2)
141 let two = BroadcastSender(path: path)
142 let refused = expectation(description: "second refused")
143 two.onClosed = { refused.fulfill() }
144 XCTAssertTrue(two.connect(timeout: 2))
145 defer { two.close() }
146 let frame = try nv12(width: 64, height: 64)
147 let until = Date().addingTimeInterval(2)
148 while Date() < until {
149 two.send(frame, orientation: 1, timestampNs: 0)
150 if XCTWaiter.wait(for: [refused], timeout: 0.05) == .completed { break }
151 }
152 XCTAssertEqual(connections, 1)
153 }
154
155 func testReplayKitOrientationBecomesAnUprightRotation() {
156 XCTAssertEqual(broadcastRotation(1), ._0)
157 XCTAssertEqual(broadcastRotation(3), ._180)
158 XCTAssertEqual(broadcastRotation(8), ._90)
159 XCTAssertEqual(broadcastRotation(6), ._270)
160 XCTAssertEqual(broadcastRotation(0), ._0)
161 }
162
163 private func nv12(width: Int, height: Int) throws -> CVPixelBuffer {
164 var made: CVPixelBuffer?
165 CVPixelBufferCreate(
166 nil, width, height, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
167 [kCVPixelBufferIOSurfacePropertiesKey: [:]] as CFDictionary, &made)
168 let pixels = try XCTUnwrap(made)
169 CVPixelBufferLockBaseAddress(pixels, [])
170 defer { CVPixelBufferUnlockBaseAddress(pixels, []) }
171 let luma = CVPixelBufferGetBaseAddressOfPlane(pixels, 0)!
172 memset(luma, 100, CVPixelBufferGetBytesPerRowOfPlane(pixels, 0) * height)
173 let chroma = CVPixelBufferGetBaseAddressOfPlane(pixels, 1)!.assumingMemoryBound(to: UInt8.self)
174 let stride = CVPixelBufferGetBytesPerRowOfPlane(pixels, 1)
175 for row in 0..<(height / 2) {
176 for pair in 0..<(width / 2) {
177 chroma[row * stride + pair * 2] = 60
178 chroma[row * stride + pair * 2 + 1] = 200
179 }
180 }
181 return pixels
182 }
183}
184