@stim-cli/server 1.14.0 → 1.16.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 +1773 -92
  2. package/dist/protocol-BHSl7bQ_.mjs +2772 -0
  3. package/dist/protocol.d.mts +2 -496
  4. package/dist/protocol.mjs +3 -2
  5. package/dist/protocol.schema.json +5388 -684
  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-CvqlgkBK.mjs +14887 -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,301 @@
1
+ import Foundation
2
+
3
+ /// One `Page.screencastFrame`: Chrome's JPEG, the page area it shows in CSS pixels, and when Chrome drew it,
4
+ /// in milliseconds since the epoch.
5
+ public struct ScreencastFrame {
6
+ public let jpeg: Data
7
+ public let cssWidth: Double
8
+ public let cssHeight: Double
9
+ public let capturedAt: Double
10
+ }
11
+
12
+ public enum WebTouchPhase: String {
13
+ case down, move, up
14
+ }
15
+
16
+ public enum WebMouseEvent: String {
17
+ case pressed = "mousePressed"
18
+ case moved = "mouseMoved"
19
+ case released = "mouseReleased"
20
+ }
21
+
22
+ /// A key `Input.dispatchKeyEvent` presses by name, with its DOM `key`, `code` and Windows virtual key code, which
23
+ /// Chrome needs to run the key's default action.
24
+ public enum WebKey: String, CaseIterable {
25
+ case enter, tab, backspace, escape, delete, left, right, up, down, home, end, pageUp, pageDown
26
+
27
+ var fields: (key: String, code: String, keyCode: Int, text: String?) {
28
+ switch self {
29
+ case .enter: ("Enter", "Enter", 13, "\r")
30
+ case .tab: ("Tab", "Tab", 9, nil)
31
+ case .backspace: ("Backspace", "Backspace", 8, nil)
32
+ case .escape: ("Escape", "Escape", 27, nil)
33
+ case .delete: ("Delete", "Delete", 46, nil)
34
+ case .left: ("ArrowLeft", "ArrowLeft", 37, nil)
35
+ case .right: ("ArrowRight", "ArrowRight", 39, nil)
36
+ case .up: ("ArrowUp", "ArrowUp", 38, nil)
37
+ case .down: ("ArrowDown", "ArrowDown", 40, nil)
38
+ case .home: ("Home", "Home", 36, nil)
39
+ case .end: ("End", "End", 35, nil)
40
+ case .pageUp: ("PageUp", "PageUp", 33, nil)
41
+ case .pageDown: ("PageDown", "PageDown", 34, nil)
42
+ }
43
+ }
44
+ }
45
+
46
+ /// `Input.dispatch*` modifier bits.
47
+ public struct WebModifiers: OptionSet, Sendable {
48
+ public let rawValue: Int
49
+ public init(rawValue: Int) { self.rawValue = rawValue }
50
+ public static let alt = WebModifiers(rawValue: 1)
51
+ public static let control = WebModifiers(rawValue: 2)
52
+ public static let meta = WebModifiers(rawValue: 4)
53
+ public static let shift = WebModifiers(rawValue: 8)
54
+ }
55
+
56
+ /// The owned page, attached through a verified `DevToolsClient`: its screencast and its input. Points are
57
+ /// fractions of the screencast frame, which shows the page's viewport; before the first frame, of the viewport
58
+ /// the page reported when it attached.
59
+ public final class WebPage: @unchecked Sendable {
60
+ public let targetId: String
61
+ private let client: DevToolsClient
62
+ private let sessionId: String
63
+ private let queue = DispatchQueue(label: "stim.web.page")
64
+ private var viewport: (width: Double, height: Double)?
65
+ private var frameHandler: ((ScreencastFrame) -> Void)?
66
+ private var endHandler: ((String) -> Void)?
67
+ private var ended: String?
68
+ private var touching = false
69
+ private var markedAt: DispatchTime?
70
+
71
+ private init(client: DevToolsClient, targetId: String, sessionId: String) {
72
+ self.client = client
73
+ self.targetId = targetId
74
+ self.sessionId = sessionId
75
+ }
76
+
77
+ /// Connects to the owned Chrome at `endpoint` and attaches to `targetId`, both from `stim status`'s `web` entry.
78
+ public static func open(
79
+ endpoint: URL, chromePid: Int32, targetId: String, completion: @escaping (Result<WebPage, DevToolsError>) -> Void
80
+ ) {
81
+ DevToolsClient.connect(endpoint: endpoint, chromePid: chromePid) { connected in
82
+ switch connected {
83
+ case .failure(let failure): completion(.failure(failure))
84
+ case .success(let client):
85
+ client.send("Target.attachToTarget", ["targetId": targetId, "flatten": true]) { attached in
86
+ guard case .success(let reply) = attached, let sessionId = reply["sessionId"] as? String else {
87
+ client.close()
88
+ if case .failure(let failure) = attached { return completion(.failure(failure)) }
89
+ return completion(.failure(DevToolsError("Chrome did not attach to the page \(targetId).")))
90
+ }
91
+ let page = WebPage(client: client, targetId: targetId, sessionId: sessionId)
92
+ client.onEvent { page.handle($0) }
93
+ client.onClose { page.end("The DevTools connection closed.") }
94
+ client.send("Target.setDiscoverTargets", ["discover": true])
95
+ client.send("Page.enable", sessionId: sessionId)
96
+ client.send(
97
+ "Runtime.evaluate", ["expression": "[innerWidth, innerHeight]", "returnByValue": true],
98
+ sessionId: sessionId
99
+ ) { measured in
100
+ if case .success(let reply) = measured,
101
+ let size = (reply["result"] as? [String: Any])?["value"] as? [Double], size.count == 2
102
+ {
103
+ page.queue.async { page.viewport = page.viewport ?? (size[0], size[1]) }
104
+ }
105
+ completion(.success(page))
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ /// Frames arrive on the page's private queue; Chrome sends one when the page changes, and once on start.
113
+ public func onFrame(_ handler: @escaping (ScreencastFrame) -> Void) {
114
+ queue.async { self.frameHandler = handler }
115
+ }
116
+
117
+ /// Called once, when the page closes, Chrome exits or the connection drops; a crashed page stays attached.
118
+ public func onEnd(_ handler: @escaping (String) -> Void) {
119
+ queue.async {
120
+ if let ended = self.ended { handler(ended) } else { self.endHandler = handler }
121
+ }
122
+ }
123
+
124
+ /// `maxEdge` bounds both sides of the JPEG, in pixels; `quality` is 0-100. A running screencast stops first:
125
+ /// Chrome sends no frame for a restart with new settings until the page changes.
126
+ public func startScreencast(maxEdge: Int, quality: Int) {
127
+ command("Page.stopScreencast")
128
+ command(
129
+ "Page.startScreencast",
130
+ ["format": "jpeg", "quality": quality, "maxWidth": maxEdge, "maxHeight": maxEdge, "everyNthFrame": 1])
131
+ }
132
+
133
+ public func stopScreencast() {
134
+ command("Page.stopScreencast")
135
+ }
136
+
137
+ public func close() {
138
+ client.close()
139
+ }
140
+
141
+ public func touch(_ phase: WebTouchPhase, x: Double, y: Double) {
142
+ queue.async {
143
+ guard let point = self.point(x, y) else { return }
144
+ self.markTakeover()
145
+ let type: String
146
+ switch phase {
147
+ case .down:
148
+ type = "touchStart"
149
+ self.touching = true
150
+ case .move:
151
+ guard self.touching else { return }
152
+ type = "touchMove"
153
+ case .up:
154
+ guard self.touching else { return }
155
+ self.touching = false
156
+ return self.command("Input.dispatchTouchEvent", ["type": "touchEnd", "touchPoints": [] as [Any]])
157
+ }
158
+ self.command("Input.dispatchTouchEvent", ["type": type, "touchPoints": [["x": point.x, "y": point.y]]])
159
+ }
160
+ }
161
+
162
+ public func mouse(
163
+ _ event: WebMouseEvent, x: Double, y: Double, pressed: Bool, clickCount: Int = 1, modifiers: WebModifiers = []
164
+ ) {
165
+ queue.async {
166
+ guard let point = self.point(x, y) else { return }
167
+ if event != .moved || pressed { self.markTakeover() }
168
+ var params: [String: Any] = [
169
+ "type": event.rawValue, "x": point.x, "y": point.y, "modifiers": modifiers.rawValue,
170
+ "button": event == .moved && !pressed ? "none" : "left", "buttons": pressed ? 1 : 0,
171
+ ]
172
+ if event != .moved { params["clickCount"] = clickCount }
173
+ self.command("Input.dispatchMouseEvent", params)
174
+ }
175
+ }
176
+
177
+ /// Scrolls at the point by `deltaX` and `deltaY` CSS pixels, positive down and right.
178
+ public func wheel(x: Double, y: Double, deltaX: Double, deltaY: Double, modifiers: WebModifiers = []) {
179
+ queue.async {
180
+ guard let point = self.point(x, y) else { return }
181
+ self.markTakeover()
182
+ self.command(
183
+ "Input.dispatchMouseEvent",
184
+ [
185
+ "type": "mouseWheel", "x": point.x, "y": point.y, "deltaX": deltaX, "deltaY": deltaY,
186
+ "modifiers": modifiers.rawValue,
187
+ ])
188
+ }
189
+ }
190
+
191
+ /// Types printable text as key presses; `\n` presses Enter, `\t` Tab and `\u{8}` Backspace.
192
+ public func type(_ text: String) {
193
+ queue.async {
194
+ self.markTakeover()
195
+ for character in text {
196
+ switch character {
197
+ case "\n", "\r": self.sendKey(.enter, [])
198
+ case "\t": self.sendKey(.tab, [])
199
+ case "\u{8}": self.sendKey(.backspace, [])
200
+ default:
201
+ let value = String(character)
202
+ self.command(
203
+ "Input.dispatchKeyEvent", ["type": "keyDown", "key": value, "text": value, "unmodifiedText": value])
204
+ self.command("Input.dispatchKeyEvent", ["type": "keyUp", "key": value])
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ public func press(_ key: WebKey, modifiers: WebModifiers = []) {
211
+ queue.async {
212
+ self.markTakeover()
213
+ self.sendKey(key, modifiers)
214
+ }
215
+ }
216
+
217
+ /// Tells the Stim browser supervisor's page listener that the input that follows is Take over, not an agent: it
218
+ /// drops input within 3 seconds of a `stim-takeover` event on the window. Sent at most once a second.
219
+ private func markTakeover() {
220
+ let now = DispatchTime.now()
221
+ if let markedAt, now.uptimeNanoseconds - markedAt.uptimeNanoseconds < 1_000_000_000 { return }
222
+ markedAt = now
223
+ command("Runtime.evaluate", ["expression": "window.dispatchEvent(new Event('stim-takeover'))"])
224
+ }
225
+
226
+ private func sendKey(_ key: WebKey, _ modifiers: WebModifiers) {
227
+ let fields = key.fields
228
+ var down: [String: Any] = [
229
+ "type": fields.text == nil ? "rawKeyDown" : "keyDown", "key": fields.key, "code": fields.code,
230
+ "windowsVirtualKeyCode": fields.keyCode, "modifiers": modifiers.rawValue,
231
+ ]
232
+ if let text = fields.text, modifiers.isEmpty { down["text"] = text }
233
+ command("Input.dispatchKeyEvent", down)
234
+ command(
235
+ "Input.dispatchKeyEvent",
236
+ [
237
+ "type": "keyUp", "key": fields.key, "code": fields.code, "windowsVirtualKeyCode": fields.keyCode,
238
+ "modifiers": modifiers.rawValue,
239
+ ])
240
+ }
241
+
242
+ /// Goes back one entry in the page's history, when it has one.
243
+ public func back() {
244
+ queue.async { self.goBack() }
245
+ }
246
+
247
+ private func goBack() {
248
+ client.send("Page.getNavigationHistory", sessionId: sessionId) { result in
249
+ guard case .success(let history) = result, let index = history["currentIndex"] as? Int, index > 0,
250
+ let entries = history["entries"] as? [[String: Any]], let id = entries[index - 1]["id"] as? Int
251
+ else { return }
252
+ self.command("Page.navigateToHistoryEntry", ["entryId": id])
253
+ }
254
+ }
255
+
256
+ public func reload() {
257
+ queue.async { self.command("Page.reload") }
258
+ }
259
+
260
+ private func command(_ method: String, _ params: [String: Any] = [:]) {
261
+ client.send(method, params, sessionId: sessionId)
262
+ }
263
+
264
+ private func point(_ x: Double, _ y: Double) -> (x: Double, y: Double)? {
265
+ guard let viewport else { return nil }
266
+ return (min(max(x, 0), 1) * viewport.width, min(max(y, 0), 1) * viewport.height)
267
+ }
268
+
269
+ private func handle(_ event: DevToolsClient.Event) {
270
+ switch event.method {
271
+ case "Page.screencastFrame" where event.sessionId == sessionId:
272
+ guard let ack = event.params["sessionId"] else { return }
273
+ command("Page.screencastFrameAck", ["sessionId": ack])
274
+ guard let base64 = event.params["data"] as? String, let jpeg = Data(base64Encoded: base64),
275
+ let metadata = event.params["metadata"] as? [String: Any],
276
+ let width = metadata["deviceWidth"] as? Double, let height = metadata["deviceHeight"] as? Double
277
+ else { return }
278
+ let timestamp = (metadata["timestamp"] as? Double).map { $0 * 1000 } ?? Date().timeIntervalSince1970 * 1000
279
+ queue.async {
280
+ self.viewport = (width, height)
281
+ self.frameHandler?(ScreencastFrame(jpeg: jpeg, cssWidth: width, cssHeight: height, capturedAt: timestamp))
282
+ }
283
+ case "Target.targetDestroyed" where event.params["targetId"] as? String == targetId:
284
+ end("The page was closed.")
285
+ case "Target.detachedFromTarget" where event.params["sessionId"] as? String == sessionId:
286
+ end("DevTools detached from the page.")
287
+ default:
288
+ break
289
+ }
290
+ }
291
+
292
+ private func end(_ reason: String) {
293
+ queue.async {
294
+ guard self.ended == nil else { return }
295
+ self.ended = reason
296
+ self.endHandler?(reason)
297
+ self.endHandler = nil
298
+ self.client.close()
299
+ }
300
+ }
301
+ }