@stim-cli/server 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +1584 -89
  2. package/dist/protocol-MBMvJpvV.mjs +2715 -0
  3. package/dist/protocol.d.mts +2 -496
  4. package/dist/protocol.mjs +3 -2
  5. package/dist/protocol.schema.json +5074 -549
  6. package/dist/scrcpy/LICENSE +204 -0
  7. package/dist/scrcpy/NOTICE +22 -0
  8. package/dist/scrcpy/scrcpy-server +0 -0
  9. package/dist/stats-read.d.mts +1 -0
  10. package/dist/stats-read.mjs +46 -0
  11. package/dist/stim-frames/AndroidDeviceStream.swift +284 -0
  12. package/dist/stim-frames/DevToolsClient.swift +180 -0
  13. package/dist/stim-frames/DeviceFrameGeometry.swift +41 -0
  14. package/dist/stim-frames/DeviceFrameView.swift +167 -0
  15. package/dist/stim-frames/DuoCaptureSource.swift +132 -0
  16. package/dist/stim-frames/DuoDeviceProfile.swift +37 -0
  17. package/dist/stim-frames/DuoFrameRenderer.swift +111 -0
  18. package/dist/stim-frames/DuoHingeMonitor.swift +89 -0
  19. package/dist/stim-frames/DuoMesh.swift +100 -0
  20. package/dist/stim-frames/DuoModelView.swift +275 -0
  21. package/dist/stim-frames/DuoPoseClip.swift +140 -0
  22. package/dist/stim-frames/DuoScreenHitMesh.swift +200 -0
  23. package/dist/stim-frames/EmulatorFrameArtwork.swift +113 -0
  24. package/dist/stim-frames/EmulatorInput.swift +13 -2
  25. package/dist/stim-frames/EmulatorPosture.swift +2 -2
  26. package/dist/stim-frames/FrameArtwork.swift +37 -0
  27. package/dist/stim-frames/H264Decoder.swift +109 -0
  28. package/dist/stim-frames/InputMessages.swift +27 -6
  29. package/dist/stim-frames/KeyboardLayoutMap.swift +70 -0
  30. package/dist/stim-frames/MacosSource.swift +677 -0
  31. package/dist/stim-frames/OwnedAppWindowReader.swift +87 -0
  32. package/dist/stim-frames/OwnedAppWindows.swift +108 -0
  33. package/dist/stim-frames/PhoneSource.swift +315 -0
  34. package/dist/stim-frames/ProcessStream.swift +155 -0
  35. package/dist/stim-frames/ScrcpyProtocol.swift +167 -0
  36. package/dist/stim-frames/SimulatorDevelopmentOptions.swift +81 -0
  37. package/dist/stim-frames/SimulatorFrameArtwork.swift +118 -0
  38. package/dist/stim-frames/SimulatorHID.swift +70 -29
  39. package/dist/stim-frames/SimulatorHingeAngle.swift +56 -0
  40. package/dist/stim-frames/SimulatorPosture.swift +277 -0
  41. package/dist/stim-frames/SimulatorRotation.swift +28 -8
  42. package/dist/stim-frames/Siniulator-LICENSE.txt +21 -0
  43. package/dist/stim-frames/StoragePaths.swift +42 -0
  44. package/dist/stim-frames/VideoEncoder.swift +8 -6
  45. package/dist/stim-frames/WebPage.swift +301 -0
  46. package/dist/stim-frames/main.swift +651 -22
  47. package/dist/stim-host/Info.plist +28 -0
  48. package/dist/stim-host/stim-host.c +153 -0
  49. package/dist/stim-server-OiJI9xLU.mjs +13336 -0
  50. package/dist/stim-server.mjs +6 -4421
  51. package/package.json +3 -3
  52. package/dist/protocol-IroIg8Td.mjs +0 -865
@@ -0,0 +1,200 @@
1
+ // Copyright (c) 2026 Siniulator contributors. Adapted under the MIT license in Support/Siniulator-LICENSE.txt.
2
+ import SceneKit
3
+ import simd
4
+
5
+ @MainActor final class DuoScreenHitMesh {
6
+ private let mesh: DuoMesh
7
+ private struct Triangle {
8
+ let positions: SIMD3<Int>
9
+ let uv: [SIMD2<Float>]
10
+ }
11
+ private let triangles: [Triangle]
12
+ private var posed: [SIMD3<Float>] = []
13
+ private var lastTransforms: [simd_float4x4] = []
14
+
15
+ init?(node: SCNNode) {
16
+ guard let geometry = node.geometry, let mesh = DuoMesh(node: node),
17
+ let triangles = Self.triangles(geometry)
18
+ else { return nil }
19
+ self.mesh = mesh
20
+ self.triangles = triangles
21
+ }
22
+
23
+ private func updatePose() {
24
+ let transforms = mesh.transforms
25
+ if transforms != lastTransforms {
26
+ posed = mesh.vertices.indices.map { index in
27
+ let point = mesh.position(at: index, using: transforms)
28
+ return SIMD3(point.x, point.y, point.z)
29
+ }
30
+ lastTransforms = transforms
31
+ }
32
+ }
33
+
34
+ func textureCoordinate(from near: SCNVector3, to far: SCNVector3) -> CGPoint? {
35
+ updatePose()
36
+ let origin = SIMD3<Float>(Float(near.x), Float(near.y), Float(near.z))
37
+ let direction = SIMD3<Float>(Float(far.x), Float(far.y), Float(far.z)) - origin
38
+ var closest: Float = 1
39
+ var result: SIMD2<Float>?
40
+ for triangle in triangles {
41
+ let a = posed[triangle.positions.x]
42
+ let ab = posed[triangle.positions.y] - a
43
+ let ac = posed[triangle.positions.z] - a
44
+ let p = simd_cross(direction, ac)
45
+ let determinant = simd_dot(ab, p)
46
+ guard abs(determinant) > 1e-8 else { continue }
47
+ let offset = origin - a
48
+ let u = simd_dot(offset, p) / determinant
49
+ guard u >= -1e-5, u <= 1.00001 else { continue }
50
+ let q = simd_cross(offset, ab)
51
+ let v = simd_dot(direction, q) / determinant
52
+ guard v >= -1e-5, u + v <= 1.00001 else { continue }
53
+ let distance = simd_dot(ac, q) / determinant
54
+ guard distance >= 0, distance <= closest else { continue }
55
+ closest = distance
56
+ result = triangle.uv[0] * (1 - u - v) + triangle.uv[1] * u + triangle.uv[2] * v
57
+ }
58
+ return result.map { CGPoint(x: CGFloat($0.x), y: CGFloat($0.y)) }
59
+ }
60
+
61
+ func nearestTextureCoordinate(
62
+ to point: CGPoint, project: (SCNVector3) -> SCNVector3,
63
+ unproject: (SCNVector3) -> SCNVector3,
64
+ maximumDistance: CGFloat = .infinity
65
+ ) -> CGPoint? {
66
+ updatePose()
67
+ let projected = posed.map { position -> SIMD3<Float> in
68
+ let p = project(SCNVector3(position.x, position.y, position.z))
69
+ return SIMD3(Float(p.x), Float(p.y), Float(p.z))
70
+ }
71
+ let target = SIMD2<Float>(Float(point.x), Float(point.y))
72
+ var distance = Float.infinity
73
+ var best: SIMD2<Float>?
74
+ for triangle in triangles {
75
+ for edge in 0..<3 {
76
+ let next = (edge + 1) % 3
77
+ let a = projected[triangle.positions[edge]]
78
+ let b = projected[triangle.positions[next]]
79
+ let t = Self.segmentFraction(target, SIMD2(a.x, a.y), SIMD2(b.x, b.y))
80
+ let candidate = a + (b - a) * t
81
+ let squared = simd_length_squared(SIMD2(candidate.x, candidate.y) - target)
82
+ if squared < distance {
83
+ let p = unproject(SCNVector3(candidate.x, candidate.y, candidate.z))
84
+ let start = posed[triangle.positions[edge]]
85
+ let end = posed[triangle.positions[next]]
86
+ let length = simd_length_squared(end - start)
87
+ let fraction =
88
+ length > 1e-12
89
+ ? min(1, max(0, simd_dot(SIMD3(Float(p.x), Float(p.y), Float(p.z)) - start, end - start) / length)) : 0
90
+ best = triangle.uv[edge] * (1 - fraction) + triangle.uv[next] * fraction
91
+ distance = squared
92
+ }
93
+ }
94
+ }
95
+ guard distance <= Float(maximumDistance * maximumDistance) else { return nil }
96
+ return best.map { CGPoint(x: CGFloat($0.x), y: CGFloat($0.y)) }
97
+ }
98
+
99
+ func position(at uv: CGPoint) -> SCNVector3? {
100
+ updatePose()
101
+ let target = SIMD2<Float>(Float(uv.x), Float(uv.y))
102
+ var distance = Float.infinity
103
+ var best: SIMD3<Float>?
104
+ func world(_ p: SIMD3<Float>) -> SCNVector3 { SCNVector3(p.x, p.y, p.z) }
105
+ for triangle in triangles {
106
+ let a = triangle.uv[0]
107
+ let ab = triangle.uv[1] - a
108
+ let ac = triangle.uv[2] - a
109
+ let offset = target - a
110
+ let determinant = ab.x * ac.y - ab.y * ac.x
111
+ if abs(determinant) > 1e-10 {
112
+ let u = (offset.x * ac.y - offset.y * ac.x) / determinant
113
+ let v = (ab.x * offset.y - ab.y * offset.x) / determinant
114
+ if u >= -1e-5, v >= -1e-5, u + v <= 1.00001 {
115
+ return world(
116
+ posed[triangle.positions.x] * (1 - u - v)
117
+ + posed[triangle.positions.y] * u + posed[triangle.positions.z] * v)
118
+ }
119
+ }
120
+ for edge in 0..<3 {
121
+ let next = (edge + 1) % 3
122
+ let a = triangle.uv[edge]
123
+ let b = triangle.uv[next]
124
+ let t = Self.segmentFraction(target, a, b)
125
+ let squared = simd_length_squared(a + (b - a) * t - target)
126
+ if squared < distance {
127
+ distance = squared
128
+ best = posed[triangle.positions[edge]] * (1 - t) + posed[triangle.positions[next]] * t
129
+ }
130
+ }
131
+ }
132
+ return best.map(world)
133
+ }
134
+
135
+ private static func segmentFraction(_ point: SIMD2<Float>, _ a: SIMD2<Float>, _ b: SIMD2<Float>) -> Float {
136
+ let length = simd_length_squared(b - a)
137
+ return length > 1e-12 ? min(1, max(0, simd_dot(point - a, b - a) / length)) : 0
138
+ }
139
+
140
+ private static func triangles(_ geometry: SCNGeometry) -> [Triangle]? {
141
+ guard let positionSource = geometry.sources.firstIndex(where: { $0.semantic == .vertex }),
142
+ let uvSource = geometry.sources.firstIndex(where: { $0.semantic == .texcoord }),
143
+ let uvValues = DuoMesh.components(geometry.sources[uvSource]),
144
+ geometry.sources[uvSource].componentsPerVector == 2
145
+ else { return nil }
146
+ let uv = stride(from: 0, to: uvValues.count, by: 2).map {
147
+ SIMD2<Float>(Float(uvValues[$0]), Float(uvValues[$0 + 1]))
148
+ }
149
+ let channels =
150
+ geometry.geometrySourceChannels?.map(\.intValue)
151
+ ?? Array(repeating: 0, count: geometry.sources.count)
152
+ guard channels.count == geometry.sources.count else { return nil }
153
+ var result: [Triangle] = []
154
+ for element in geometry.elements {
155
+ guard element.primitiveType == .triangles || element.primitiveType == .polygon,
156
+ element.indicesChannelCount > 0,
157
+ channels.allSatisfy({ $0 >= 0 && $0 < element.indicesChannelCount }),
158
+ [1, 2, 4].contains(element.bytesPerIndex)
159
+ else { return nil }
160
+ let indices: [Int] = element.data.withUnsafeBytes { bytes in
161
+ stride(from: 0, to: bytes.count - element.bytesPerIndex + 1, by: element.bytesPerIndex).map { offset in
162
+ switch element.bytesPerIndex {
163
+ case 1: Int(bytes.loadUnaligned(fromByteOffset: offset, as: UInt8.self))
164
+ case 2: Int(bytes.loadUnaligned(fromByteOffset: offset, as: UInt16.self))
165
+ default: Int(bytes.loadUnaligned(fromByteOffset: offset, as: UInt32.self))
166
+ }
167
+ }
168
+ }
169
+ let header = element.primitiveType == .polygon ? element.primitiveCount : 0
170
+ guard indices.count >= header else { return nil }
171
+ let counts = header > 0 ? Array(indices.prefix(header)) : Array(repeating: 3, count: element.primitiveCount)
172
+ let count = counts.reduce(0, +)
173
+ guard counts.allSatisfy({ $0 >= 3 }),
174
+ indices.count == header + count * element.indicesChannelCount
175
+ else { return nil }
176
+ func index(_ corner: Int, _ source: Int) -> Int {
177
+ indices[
178
+ header
179
+ + (element.hasInterleavedIndicesChannels
180
+ ? corner * element.indicesChannelCount + channels[source]
181
+ : channels[source] * count + corner)]
182
+ }
183
+ var start = 0
184
+ for corners in counts {
185
+ for corner in 1..<(corners - 1) {
186
+ let corners = [start, start + corner, start + corner + 1]
187
+ let positions = corners.map { index($0, positionSource) }
188
+ let texcoords = corners.map { index($0, uvSource) }
189
+ guard positions.allSatisfy({ $0 < geometry.sources[positionSource].vectorCount }),
190
+ texcoords.allSatisfy({ $0 < uv.count })
191
+ else { return nil }
192
+ result.append(Triangle(positions: SIMD3(positions), uv: texcoords.map { uv[$0] }))
193
+ }
194
+ start += corners
195
+ }
196
+ }
197
+ return result.isEmpty ? nil : result
198
+ }
199
+
200
+ }
@@ -0,0 +1,113 @@
1
+ import AppKit
2
+
3
+ #if canImport(StimKit)
4
+ import StimKit
5
+ #endif
6
+
7
+ struct EmulatorSkinLayout {
8
+ var geometry: DeviceFrameGeometry
9
+ var image: String
10
+ var mask: String?
11
+ var cornerRadius: CGFloat
12
+
13
+ init?(_ contents: String) {
14
+ let text = contents.split(whereSeparator: \.isNewline).map {
15
+ $0.split(separator: "#", maxSplits: 1).first.map(String.init) ?? ""
16
+ }.joined(separator: "\n")
17
+ guard let expression = try? NSRegularExpression(pattern: "[{}]|\"[^\"]*\"|[^\\s{}]+") else { return nil }
18
+ let tokens = expression.matches(in: text, range: NSRange(text.startIndex..., in: text)).compactMap { match in
19
+ Range(match.range, in: text).map { String(text[$0]).trimmingCharacters(in: CharacterSet(charactersIn: "\"")) }
20
+ }
21
+ var index = 0
22
+ guard let root = SkinNode.parse(tokens, index: &index), index == tokens.count,
23
+ let parts = root.children["parts"], let layouts = root.children["layouts"], layouts.children.count == 1,
24
+ let portrait = layouts.children["portrait"], let display = parts.children["device"]?.children["display"],
25
+ let placement = portrait.children.values.first(where: { $0.values["name"] == "device" }),
26
+ let body = parts.children["portrait"], let background = body.children["background"]?.values["image"],
27
+ let width = portrait.number("width"), let height = portrait.number("height"),
28
+ let screenWidth = display.number("width"), let screenHeight = display.number("height"),
29
+ display.number("x") == 0, display.number("y") == 0,
30
+ let x = placement.number("x"), let y = placement.number("y"),
31
+ width > 0, height > 0, screenWidth > 0, screenHeight > 0, x >= 0, y >= 0,
32
+ x + screenWidth <= width, y + screenHeight <= height
33
+ else { return nil }
34
+ geometry = DeviceFrameGeometry(
35
+ size: CGSize(width: width, height: height), aperture: CGRect(x: x, y: y, width: screenWidth, height: screenHeight))
36
+ image = background
37
+ mask = body.children["foreground"]?.values["mask"]
38
+ cornerRadius = display.number("corner_radius") ?? 0
39
+ }
40
+ }
41
+
42
+ private struct SkinNode {
43
+ var values: [String: String] = [:]
44
+ var children: [String: SkinNode] = [:]
45
+
46
+ func number(_ key: String) -> CGFloat? { values[key].flatMap(Double.init).map { CGFloat($0) } }
47
+
48
+ static func parse(_ tokens: [String], index: inout Int) -> Self? {
49
+ var node = Self()
50
+ while index < tokens.count, tokens[index] != "}" {
51
+ let key = tokens[index]
52
+ index += 1
53
+ guard index < tokens.count else { return nil }
54
+ if tokens[index] == "{" {
55
+ index += 1
56
+ guard let child = parse(tokens, index: &index), index < tokens.count, tokens[index] == "}" else { return nil }
57
+ node.children[key] = child
58
+ } else {
59
+ node.values[key] = tokens[index]
60
+ }
61
+ index += 1
62
+ }
63
+ return node
64
+ }
65
+ }
66
+
67
+ @MainActor
68
+ enum EmulatorFrameArtwork {
69
+ static func load(avdName: String) -> DeviceFrameArtwork? {
70
+ let environment = ProcessInfo.processInfo.environment
71
+ let home = FileManager.default.homeDirectoryForCurrentUser
72
+ let avdRoot = URL(fileURLWithPath: StoragePaths(home: home.path, environment: environment).avds)
73
+ let ini = values(avdRoot.appendingPathComponent(avdName).appendingPathExtension("ini"))
74
+ guard let path = ini["path"] else { return nil }
75
+ let config = values(URL(fileURLWithPath: path).appendingPathComponent("config.ini"))
76
+ guard config["hw.sensor.hinge"] != "yes", config["hw.lcd.circular"] != "true" else { return nil }
77
+ let skin: URL
78
+ if let path = config["skin.path"], path.hasPrefix("/") {
79
+ skin = URL(fileURLWithPath: path)
80
+ } else if let name = config["hw.device.name"], !name.contains("/") {
81
+ skin = URL(fileURLWithPath: "/Applications/Android Studio.app/Contents/plugins/android/resources/device-art-resources")
82
+ .appendingPathComponent(name)
83
+ } else {
84
+ return nil
85
+ }
86
+ guard let contents = try? String(contentsOf: skin.appendingPathComponent("layout"), encoding: .utf8),
87
+ let layout = EmulatorSkinLayout(contents),
88
+ config["hw.lcd.width"].flatMap(Double.init) == Double(layout.geometry.aperture.width),
89
+ config["hw.lcd.height"].flatMap(Double.init) == Double(layout.geometry.aperture.height),
90
+ let background = NSImage(contentsOf: skin.appendingPathComponent(layout.image))
91
+ else { return nil }
92
+ let foreground = layout.mask.flatMap { NSImage(contentsOf: skin.appendingPathComponent($0)) }
93
+ guard layout.mask == nil || foreground != nil else { return nil }
94
+ return DeviceFrameArtwork(
95
+ geometry: layout.geometry, cornerRadius: layout.cornerRadius,
96
+ background: { rect in draw(background, in: rect) },
97
+ foreground: { _ in if let foreground { draw(foreground, in: layout.geometry.aperture) } })
98
+ }
99
+
100
+ static func values(_ url: URL) -> [String: String] {
101
+ guard let text = try? String(contentsOf: url, encoding: .utf8) else { return [:] }
102
+ var values: [String: String] = [:]
103
+ for line in text.split(whereSeparator: \.isNewline) {
104
+ let pair = line.split(separator: "=", maxSplits: 1)
105
+ if pair.count == 2 { values[String(pair[0])] = String(pair[1]) }
106
+ }
107
+ return values
108
+ }
109
+
110
+ private static func draw(_ image: NSImage, in rect: CGRect) {
111
+ image.draw(in: rect, from: .zero, operation: .sourceOver, fraction: 1, respectFlipped: true, hints: nil)
112
+ }
113
+ }
@@ -12,11 +12,14 @@ final class EmulatorInput {
12
12
  private static let window: UInt32 = 1 << 30
13
13
 
14
14
  private struct Call {
15
+ var id = UUID()
15
16
  var method: String
16
17
  var message: Data
17
18
  var completion: ((Data?) -> Void)?
18
19
  }
19
20
 
21
+ var onDisconnect: (() -> Void)?
22
+
20
23
  private let endpoint: EmulatorEndpoint
21
24
  private let queue = DispatchQueue(label: "stim.emulator-input")
22
25
  private var connection: NWConnection?
@@ -31,9 +34,15 @@ final class EmulatorInput {
31
34
  self.endpoint = endpoint
32
35
  }
33
36
 
34
- func call(_ method: String, _ message: Data, completion: ((Data?) -> Void)? = nil) {
37
+ func call(_ method: String, _ message: Data, timeout: TimeInterval? = nil, completion: ((Data?) -> Void)? = nil) {
35
38
  queue.async {
36
- self.pending.append(Call(method: method, message: message, completion: completion))
39
+ let call = Call(method: method, message: message, completion: completion)
40
+ self.pending.append(call)
41
+ if let timeout {
42
+ self.queue.asyncAfter(deadline: .now() + timeout) {
43
+ if self.current?.call.id == call.id || self.pending.contains(where: { $0.id == call.id }) { self.reset() }
44
+ }
45
+ }
37
46
  self.connect()
38
47
  self.sendNext()
39
48
  }
@@ -128,6 +137,7 @@ final class EmulatorInput {
128
137
  }
129
138
 
130
139
  private func reset() {
140
+ let disconnected = connection != nil && !closing
131
141
  connection?.cancel()
132
142
  connection = nil
133
143
  ready = false
@@ -137,5 +147,6 @@ final class EmulatorInput {
137
147
  current = nil
138
148
  pending = []
139
149
  for call in dropped { call.completion?(nil) }
150
+ if disconnected { onDisconnect?() }
140
151
  }
141
152
  }
@@ -65,9 +65,9 @@ enum PostureMessages {
65
65
  }
66
66
 
67
67
  extension EmulatorInput {
68
- func call(_ method: String, _ message: Data) async -> Data? {
68
+ func call(_ method: String, _ message: Data, timeout: TimeInterval? = nil) async -> Data? {
69
69
  await withCheckedContinuation { continuation in
70
- call(method, message) { continuation.resume(returning: $0) }
70
+ call(method, message, timeout: timeout) { continuation.resume(returning: $0) }
71
71
  }
72
72
  }
73
73
  }
@@ -0,0 +1,37 @@
1
+ import AppKit
2
+
3
+ final class FrameArtworkPublisher {
4
+ private let load: @MainActor () -> DeviceFrameArtwork?
5
+ private var artwork: DeviceFrameArtwork?
6
+ private var loaded = false
7
+
8
+ init(load: @escaping @MainActor () -> DeviceFrameArtwork?) { self.load = load }
9
+
10
+ func send(quarterTurns: Int) {
11
+ Task { @MainActor in
12
+ if !loaded {
13
+ loaded = true
14
+ artwork = load()
15
+ }
16
+ guard let artwork, let images = artwork.pngLayers(quarterTurns: quarterTurns),
17
+ images.background.count + images.foreground.count <= 10 * 1024 * 1024
18
+ else {
19
+ Output.notice(["deviceFrame": NSNull()])
20
+ return
21
+ }
22
+ let geometry = artwork.geometry.rotated(quarterTurns: quarterTurns)
23
+ Output.notice([
24
+ "deviceFrame": [
25
+ "width": geometry.size.width, "height": geometry.size.height,
26
+ "aperture": [
27
+ "x": geometry.aperture.minX, "y": geometry.aperture.minY,
28
+ "width": geometry.aperture.width, "height": geometry.aperture.height,
29
+ ],
30
+ "cornerRadius": artwork.cornerRadius, "quarterTurns": quarterTurns,
31
+ "background": images.background.base64EncodedString(),
32
+ "foreground": images.foreground.base64EncodedString(),
33
+ ]
34
+ ])
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,109 @@
1
+ import CoreMedia
2
+ import CoreVideo
3
+ import Foundation
4
+ import VideoToolbox
5
+
6
+ /// Decodes an Annex-B H.264 stream into BGRA pixel buffers, one per access unit. A config packet (SPS and
7
+ /// PPS) with new parameter sets replaces the session, and one that repeats them keeps it. A session VideoToolbox
8
+ /// reports invalid (`kVTInvalidSessionErr`, for example after sleep) is dropped, so the next config packet makes a
9
+ /// new one. Packets before the first config are dropped.
10
+ public final class H264Decoder {
11
+ private var format: CMVideoFormatDescription?
12
+ private var session: VTDecompressionSession?
13
+ private var parameterSets: [Data] = []
14
+ private let output: (CVPixelBuffer) -> Void
15
+
16
+ public init(output: @escaping (CVPixelBuffer) -> Void) {
17
+ self.output = output
18
+ }
19
+
20
+ deinit {
21
+ invalidate()
22
+ }
23
+
24
+ public func invalidate() {
25
+ if let session {
26
+ VTDecompressionSessionWaitForAsynchronousFrames(session)
27
+ VTDecompressionSessionInvalidate(session)
28
+ }
29
+ session = nil
30
+ format = nil
31
+ parameterSets = []
32
+ }
33
+
34
+ /// Returns false when the parameter sets could not make a decoder.
35
+ @discardableResult
36
+ public func configure(_ annexB: Data) -> Bool {
37
+ let units = AnnexB.units(annexB)
38
+ guard let sps = units.first(where: { ($0.first ?? 0) & 0x1f == 7 }),
39
+ let pps = units.first(where: { ($0.first ?? 0) & 0x1f == 8 })
40
+ else {
41
+ invalidate()
42
+ return false
43
+ }
44
+ if session != nil, parameterSets == [sps, pps] { return true }
45
+ invalidate()
46
+ var description: CMVideoFormatDescription?
47
+ let status = sps.withUnsafeBytes { spsBytes in
48
+ pps.withUnsafeBytes { ppsBytes in
49
+ let pointers = [
50
+ spsBytes.bindMemory(to: UInt8.self).baseAddress!, ppsBytes.bindMemory(to: UInt8.self).baseAddress!,
51
+ ]
52
+ return CMVideoFormatDescriptionCreateFromH264ParameterSets(
53
+ allocator: nil, parameterSetCount: 2, parameterSetPointers: pointers, parameterSetSizes: [sps.count, pps.count],
54
+ nalUnitHeaderLength: 4, formatDescriptionOut: &description)
55
+ }
56
+ }
57
+ guard status == noErr, let description else { return false }
58
+ let attributes: [CFString: Any] = [
59
+ kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32BGRA,
60
+ kCVPixelBufferIOSurfacePropertiesKey: [:] as CFDictionary,
61
+ ]
62
+ var created: VTDecompressionSession?
63
+ guard
64
+ VTDecompressionSessionCreate(
65
+ allocator: nil, formatDescription: description, decoderSpecification: nil,
66
+ imageBufferAttributes: attributes as CFDictionary, outputCallback: nil, decompressionSessionOut: &created)
67
+ == noErr, let created
68
+ else { return false }
69
+ format = description
70
+ session = created
71
+ parameterSets = [sps, pps]
72
+ return true
73
+ }
74
+
75
+ /// Returns false once there is no session, so the caller waits for or asks for a config packet.
76
+ @discardableResult
77
+ public func decode(_ annexB: Data) -> Bool {
78
+ guard let session, let format else { return false }
79
+ let units = AnnexB.units(annexB).filter { !$0.isEmpty && ![7, 8].contains($0[0] & 0x1f) }
80
+ guard !units.isEmpty else { return true }
81
+ let sample = AnnexB.lengthPrefixed(units)
82
+ var block: CMBlockBuffer?
83
+ guard
84
+ CMBlockBufferCreateWithMemoryBlock(
85
+ allocator: nil, memoryBlock: nil, blockLength: sample.count, blockAllocator: nil, customBlockSource: nil,
86
+ offsetToData: 0, dataLength: sample.count, flags: kCMBlockBufferAssureMemoryNowFlag, blockBufferOut: &block)
87
+ == noErr, let block,
88
+ sample.withUnsafeBytes({
89
+ CMBlockBufferReplaceDataBytes(
90
+ with: $0.baseAddress!, blockBuffer: block, offsetIntoDestination: 0, dataLength: sample.count)
91
+ }) == noErr
92
+ else { return true }
93
+ var buffer: CMSampleBuffer?
94
+ var size = sample.count
95
+ guard
96
+ CMSampleBufferCreateReady(
97
+ allocator: nil, dataBuffer: block, formatDescription: format, sampleCount: 1, sampleTimingEntryCount: 0,
98
+ sampleTimingArray: nil, sampleSizeEntryCount: 1, sampleSizeArray: &size, sampleBufferOut: &buffer) == noErr,
99
+ let buffer
100
+ else { return true }
101
+ let output = self.output
102
+ let status = VTDecompressionSessionDecodeFrame(session, sampleBuffer: buffer, flags: [], infoFlagsOut: nil) {
103
+ status, _, image, _, _ in
104
+ if status == noErr, let image { output(image) }
105
+ }
106
+ if status == kVTInvalidSessionErr { invalidate() }
107
+ return self.session != nil
108
+ }
109
+ }
@@ -3,6 +3,21 @@ import Foundation
3
3
  // Field numbers and enum values follow MouseEvent, KeyboardEvent and
4
4
  // EmulatorStatus in the emulator's emulator_controller.proto (sdk/emulator/lib).
5
5
  enum InputMessages {
6
+ // ClipData.text is field 1, UTF-8, in emulator_controller.proto.
7
+ static func clipboard(_ text: String) -> Data {
8
+ var out = Data()
9
+ appendBytes(field: 1, Data(text.utf8), to: &out)
10
+ return out
11
+ }
12
+
13
+ static func clipboardText(_ bytes: Data) -> String? {
14
+ var reader = ProtoReader(bytes)
15
+ while let (field, value) = reader.next() {
16
+ if field == 1, case .bytes(let text) = value { return String(data: text, encoding: .utf8) }
17
+ }
18
+ return ""
19
+ }
20
+
6
21
  static let macKeyCodeType: UInt64 = 4
7
22
  static let keyup: UInt64 = 1
8
23
  static let keypress: UInt64 = 2
@@ -17,13 +32,19 @@ enum InputMessages {
17
32
  /// is false (pressure 0). Unlike a MouseEvent, Android sees a touchscreen
18
33
  /// finger, not a mouse or stylus.
19
34
  static func touch(x: Int, y: Int, pressed: Bool) -> Data {
20
- var touch = Data()
21
- for (field, value) in [(1, UInt64(max(x, 0))), (2, UInt64(max(y, 0))), (4, pressed ? 1 : 0)] where value != 0 {
22
- ScreenshotMessages.appendVarint(UInt64(field << 3), to: &touch)
23
- ScreenshotMessages.appendVarint(value, to: &touch)
24
- }
35
+ touches([(x, y)], pressed: pressed)
36
+ }
37
+
38
+ /// Contacts keep their list index as their tracking identifier until pressure-zero release.
39
+ static func touches(_ points: [(x: Int, y: Int)], pressed: Bool) -> Data {
25
40
  var out = Data()
26
- appendBytes(field: 1, touch, to: &out)
41
+ for (identifier, point) in points.enumerated() {
42
+ let touch = message([
43
+ (1, UInt64(max(point.x, 0))), (2, UInt64(max(point.y, 0))),
44
+ (3, UInt64(identifier)), (4, pressed ? 1 : 0),
45
+ ])
46
+ appendBytes(field: 1, touch, to: &out)
47
+ }
27
48
  return out
28
49
  }
29
50
 
@@ -0,0 +1,70 @@
1
+ import Carbon
2
+ import Foundation
3
+
4
+ public struct KeyboardLayoutMap: Sendable {
5
+ private var layers: [Bool: [Bool: [Character: UInt16]]] = [:]
6
+
7
+ public init(translate: (UInt16, Bool, Bool) -> String?) {
8
+ for command in [false, true] {
9
+ for control in [false, true] {
10
+ var codes: [Character: UInt16] = [:]
11
+ for keyCode: UInt16 in 0...50 {
12
+ guard let text = translate(keyCode, command, false), text.unicodeScalars.count == 1,
13
+ let scalar = text.unicodeScalars.first, let character = text.first,
14
+ "abcdefghijklmnopqrstuvwxyz0123456789".contains(character), codes[character] == nil
15
+ else { continue }
16
+ if control {
17
+ guard let controlled = translate(keyCode, command, true), controlled.unicodeScalars.count == 1,
18
+ controlled == text
19
+ || (scalar.value >= 0x61 && controlled.unicodeScalars.first?.value == scalar.value - 0x60)
20
+ else { continue }
21
+ }
22
+ codes[character] = keyCode
23
+ }
24
+ layers[command, default: [:]][control] = codes
25
+ }
26
+ }
27
+ }
28
+
29
+ public init(layoutData: CFData) {
30
+ let layout = UnsafeRawPointer(CFDataGetBytePtr(layoutData)!).assumingMemoryBound(to: UCKeyboardLayout.self)
31
+ let keyboardType = UInt32(LMGetKbdType())
32
+ self.init { keyCode, command, control in
33
+ var deadKeyState: UInt32 = 0
34
+ var length = 0
35
+ var output = [UniChar](repeating: 0, count: 255)
36
+ let modifiers = (command ? UInt32(cmdKey >> 8) : 0) | (control ? UInt32(controlKey >> 8) : 0)
37
+ let status = UCKeyTranslate(
38
+ layout, keyCode, UInt16(kUCKeyActionDown), modifiers,
39
+ keyboardType, 0, &deadKeyState, output.count, &length, &output)
40
+ guard status == noErr, deadKeyState == 0, length > 0 else { return nil }
41
+ return String(utf16CodeUnits: output, count: length)
42
+ }
43
+ }
44
+
45
+ public func keyCode(for character: Character, command: Bool, control: Bool) -> UInt16? {
46
+ layers[command]?[control]?[character]
47
+ }
48
+ }
49
+
50
+ public final class KeyboardLayoutCache {
51
+ private var maps: [String: KeyboardLayoutMap] = [:]
52
+
53
+ public init() {}
54
+
55
+ public func current() -> (id: String, map: KeyboardLayoutMap)? {
56
+ // Apple TextInputSources requires main-thread access.
57
+ precondition(Thread.isMainThread)
58
+ guard let source = TISCopyCurrentKeyboardLayoutInputSource()?.takeRetainedValue(),
59
+ let idProperty = TISGetInputSourceProperty(source, kTISPropertyInputSourceID),
60
+ let dataProperty = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData)
61
+ else { return nil }
62
+ let id = Unmanaged<CFString>.fromOpaque(idProperty).takeUnretainedValue() as String
63
+ let data = Unmanaged<CFData>.fromOpaque(dataProperty).takeUnretainedValue()
64
+ guard CFDataGetLength(data) > 0 else { return nil }
65
+ if let map = maps[id] { return (id, map) }
66
+ let map = KeyboardLayoutMap(layoutData: data)
67
+ maps[id] = map
68
+ return (id, map)
69
+ }
70
+ }