@stim-cli/server 1.13.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.
- package/README.md +1590 -42
- package/dist/protocol-MBMvJpvV.mjs +2715 -0
- package/dist/protocol.d.mts +2 -473
- package/dist/protocol.mjs +3 -2
- package/dist/protocol.schema.json +5121 -514
- package/dist/scrcpy/LICENSE +204 -0
- package/dist/scrcpy/NOTICE +22 -0
- package/dist/scrcpy/scrcpy-server +0 -0
- package/dist/stats-read.d.mts +1 -0
- package/dist/stats-read.mjs +46 -0
- package/dist/stim-frames/AndroidDeviceStream.swift +284 -0
- package/dist/stim-frames/DevToolsClient.swift +180 -0
- package/dist/stim-frames/DeviceFrameGeometry.swift +41 -0
- package/dist/stim-frames/DeviceFrameView.swift +167 -0
- package/dist/stim-frames/DuoCaptureSource.swift +132 -0
- package/dist/stim-frames/DuoDeviceProfile.swift +37 -0
- package/dist/stim-frames/DuoFrameRenderer.swift +111 -0
- package/dist/stim-frames/DuoHingeMonitor.swift +89 -0
- package/dist/stim-frames/DuoMesh.swift +100 -0
- package/dist/stim-frames/DuoModelView.swift +275 -0
- package/dist/stim-frames/DuoPoseClip.swift +140 -0
- package/dist/stim-frames/DuoScreenHitMesh.swift +200 -0
- package/dist/stim-frames/EmulatorFrameArtwork.swift +113 -0
- package/dist/stim-frames/EmulatorInput.swift +13 -2
- package/dist/stim-frames/EmulatorPosture.swift +2 -2
- package/dist/stim-frames/FrameArtwork.swift +37 -0
- package/dist/stim-frames/H264Decoder.swift +109 -0
- package/dist/stim-frames/InputMessages.swift +27 -6
- package/dist/stim-frames/KeyboardLayoutMap.swift +70 -0
- package/dist/stim-frames/MacosSource.swift +677 -0
- package/dist/stim-frames/OwnedAppWindowReader.swift +87 -0
- package/dist/stim-frames/OwnedAppWindows.swift +108 -0
- package/dist/stim-frames/PhoneSource.swift +315 -0
- package/dist/stim-frames/ProcessStream.swift +155 -0
- package/dist/stim-frames/ScrcpyProtocol.swift +167 -0
- package/dist/stim-frames/SimulatorDevelopmentOptions.swift +81 -0
- package/dist/stim-frames/SimulatorFrameArtwork.swift +118 -0
- package/dist/stim-frames/SimulatorHID.swift +70 -29
- package/dist/stim-frames/SimulatorHingeAngle.swift +56 -0
- package/dist/stim-frames/SimulatorPosture.swift +277 -0
- package/dist/stim-frames/SimulatorRotation.swift +28 -8
- package/dist/stim-frames/Siniulator-LICENSE.txt +21 -0
- package/dist/stim-frames/StoragePaths.swift +42 -0
- package/dist/stim-frames/VideoEncoder.swift +8 -6
- package/dist/stim-frames/WebPage.swift +301 -0
- package/dist/stim-frames/main.swift +651 -22
- package/dist/stim-host/Info.plist +28 -0
- package/dist/stim-host/stim-host.c +153 -0
- package/dist/stim-server-OiJI9xLU.mjs +13336 -0
- package/dist/stim-server.mjs +6 -3880
- package/package.json +3 -3
- package/dist/protocol-rC-5KT1O.mjs +0 -822
|
@@ -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
|
+
}
|