@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.
- package/README.md +1773 -92
- package/dist/protocol-BHSl7bQ_.mjs +2772 -0
- package/dist/protocol.d.mts +2 -496
- package/dist/protocol.mjs +3 -2
- package/dist/protocol.schema.json +5388 -684
- 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-CvqlgkBK.mjs +14887 -0
- package/dist/stim-server.mjs +6 -4421
- package/package.json +3 -3
- package/dist/protocol-IroIg8Td.mjs +0 -865
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// The hinge path, spring and vendor reports below are adapted from Siniulator
|
|
2
|
+
// (github.com/kmagiera/Siniulator, Sources/Siniulator/DuoMotion.swift and
|
|
3
|
+
// Sources/Siniulator/Input.swift), under this licence:
|
|
4
|
+
//
|
|
5
|
+
// MIT License
|
|
6
|
+
//
|
|
7
|
+
// Copyright (c) 2026 Siniulator contributors
|
|
8
|
+
//
|
|
9
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
// in the Software without restriction, including without limitation the rights
|
|
12
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
// furnished to do so, subject to the following conditions:
|
|
15
|
+
//
|
|
16
|
+
// The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
// copies or substantial portions of the Software.
|
|
18
|
+
//
|
|
19
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
// SOFTWARE.
|
|
26
|
+
|
|
27
|
+
import Foundation
|
|
28
|
+
import IOKit
|
|
29
|
+
|
|
30
|
+
/// The iPhone Duo postures Xcode's Device Hub offers, as hinge angles in degrees.
|
|
31
|
+
public enum DuoPosture: CaseIterable, Sendable {
|
|
32
|
+
case closed, halfOpen, open
|
|
33
|
+
|
|
34
|
+
public var hingeAngle: Double {
|
|
35
|
+
switch self {
|
|
36
|
+
case .closed: return 0
|
|
37
|
+
case .halfOpen: return 120
|
|
38
|
+
case .open: return 180
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public var label: String {
|
|
43
|
+
switch self {
|
|
44
|
+
case .closed: return "Folded"
|
|
45
|
+
case .halfOpen: return "Half open"
|
|
46
|
+
case .open: return "Unfolded"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public var systemImage: String {
|
|
51
|
+
switch self {
|
|
52
|
+
case .closed: return "rectangle.portrait"
|
|
53
|
+
case .halfOpen: return "book"
|
|
54
|
+
case .open: return "rectangle"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Whether the posture lights the cover panel rather than the inner one.
|
|
59
|
+
public var isFolded: Bool { self == .closed }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// The path the hinge takes between two angles. iOS 27.1 swaps panels only
|
|
63
|
+
/// while the angle moves continuously through the turn between the cover and
|
|
64
|
+
/// inner rest angles; a jump across it can leave both panels black.
|
|
65
|
+
enum DuoHinge {
|
|
66
|
+
static let coverEnd = 0.20
|
|
67
|
+
static let innerStart = 0.50
|
|
68
|
+
static let coverRestAngle = 40.0
|
|
69
|
+
static let innerRestAngle = 110.0
|
|
70
|
+
|
|
71
|
+
static func estimatedAngle(remembered: Double?, folded: Bool?) -> Double {
|
|
72
|
+
if folded == true {
|
|
73
|
+
let openPreset = DuoPosture.allCases.contains { !$0.isFolded && $0.hingeAngle == remembered }
|
|
74
|
+
return openPreset ? 0 : remembered ?? 0
|
|
75
|
+
}
|
|
76
|
+
if folded == false, remembered == 0 { return 180 }
|
|
77
|
+
return remembered ?? 180
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static func sweep(from: Double, to: Double, interval: Double = 1.0 / 60) -> [Double] {
|
|
81
|
+
var spring = Spring(phase(for: from), maximumSpeed: 1)
|
|
82
|
+
spring.target = phase(for: to)
|
|
83
|
+
var angles: [Double] = []
|
|
84
|
+
while !spring.isSettled {
|
|
85
|
+
spring.advance(seconds: interval)
|
|
86
|
+
angles.append(angle(at: spring.value))
|
|
87
|
+
}
|
|
88
|
+
return angles
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static func phase(for angle: Double) -> Double {
|
|
92
|
+
let angle = min(180, max(0, angle))
|
|
93
|
+
if angle == 0 { return 0 }
|
|
94
|
+
if angle == 180 { return 1 }
|
|
95
|
+
var low = 0.0
|
|
96
|
+
var high = 1.0
|
|
97
|
+
for _ in 0..<40 {
|
|
98
|
+
let middle = (low + high) / 2
|
|
99
|
+
if Self.angle(at: middle) < angle { low = middle } else { high = middle }
|
|
100
|
+
}
|
|
101
|
+
return (low + high) / 2
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static func angle(at phase: Double) -> Double {
|
|
105
|
+
let phase = min(1, max(0, phase))
|
|
106
|
+
let firstSlope = coverRestAngle / coverEnd
|
|
107
|
+
let turnSlope = (innerRestAngle - coverRestAngle) / (innerStart - coverEnd)
|
|
108
|
+
let lastSlope = (180 - innerRestAngle) / (1 - innerStart)
|
|
109
|
+
let firstTangent = 1.5 / (0.8 / firstSlope + 0.7 / turnSlope)
|
|
110
|
+
let secondTangent = 2.4 / (1.3 / turnSlope + 1.1 / lastSlope)
|
|
111
|
+
if phase <= coverEnd {
|
|
112
|
+
return segment(
|
|
113
|
+
phase / coverEnd, start: 0, end: coverRestAngle, slope0: firstSlope * coverEnd,
|
|
114
|
+
slope1: firstTangent * coverEnd)
|
|
115
|
+
}
|
|
116
|
+
if phase < innerStart {
|
|
117
|
+
let span = innerStart - coverEnd
|
|
118
|
+
return segment(
|
|
119
|
+
(phase - coverEnd) / span, start: coverRestAngle, end: innerRestAngle, slope0: firstTangent * span,
|
|
120
|
+
slope1: secondTangent * span)
|
|
121
|
+
}
|
|
122
|
+
let span = 1 - innerStart
|
|
123
|
+
return segment(
|
|
124
|
+
(phase - innerStart) / span, start: innerRestAngle, end: 180, slope0: secondTangent * span,
|
|
125
|
+
slope1: lastSlope * span)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private static func segment(_ t: Double, start: Double, end: Double, slope0: Double, slope1: Double) -> Double {
|
|
129
|
+
let t2 = t * t
|
|
130
|
+
let t3 = t2 * t
|
|
131
|
+
return (2 * t3 - 3 * t2 + 1) * start + (t3 - 2 * t2 + t) * slope0 + (-2 * t3 + 3 * t2) * end + (t3 - t2) * slope1
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
struct Spring {
|
|
135
|
+
var value: Double
|
|
136
|
+
var target: Double
|
|
137
|
+
private(set) var velocity = 0.0
|
|
138
|
+
let maximumSpeed: Double
|
|
139
|
+
|
|
140
|
+
init(_ value: Double, maximumSpeed: Double) {
|
|
141
|
+
self.value = value
|
|
142
|
+
target = value
|
|
143
|
+
self.maximumSpeed = maximumSpeed
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
var isSettled: Bool { abs(target - value) < 0.00001 && abs(velocity) < 0.0001 }
|
|
147
|
+
|
|
148
|
+
mutating func advance(seconds: Double) {
|
|
149
|
+
let dt = min(1.0 / 30, max(0, seconds))
|
|
150
|
+
guard dt > 0 else { return }
|
|
151
|
+
let frequency = 12.0
|
|
152
|
+
let error = value - target
|
|
153
|
+
let tangent = velocity + frequency * error
|
|
154
|
+
let decay = exp(-frequency * dt)
|
|
155
|
+
let proposed = target + (error + tangent * dt) * decay
|
|
156
|
+
let step = proposed - value
|
|
157
|
+
if abs(step) > maximumSpeed * dt {
|
|
158
|
+
velocity = step.sign == .minus ? -maximumSpeed : maximumSpeed
|
|
159
|
+
value += velocity * dt
|
|
160
|
+
} else {
|
|
161
|
+
value = proposed
|
|
162
|
+
velocity = (velocity - frequency * tangent * dt) * decay
|
|
163
|
+
}
|
|
164
|
+
if isSettled {
|
|
165
|
+
value = target
|
|
166
|
+
velocity = 0
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// The vendor-defined HID report that sets the hinge angle through the
|
|
172
|
+
/// simulator's Virtualization provider, in IOKit's binary serialization.
|
|
173
|
+
static func report(angle: Double) -> Data? {
|
|
174
|
+
let event: NSDictionary = [
|
|
175
|
+
"provider": "com.apple.Virtualization.VirtualMachines", "source": "hinge-slider-control", "type": "range",
|
|
176
|
+
"value": angle,
|
|
177
|
+
]
|
|
178
|
+
return IOCFSerialize(event, CFOptionFlags(kIOCFSerializeToBinary)) as Data?
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// Moves a booted iPhone Duo simulator's hinge through dtuhidd's vendor-defined
|
|
183
|
+
/// HID service, which CoreSimulator 1174 and later run in the simulator.
|
|
184
|
+
public enum SimulatorPosture {
|
|
185
|
+
private static let lock = NSLock()
|
|
186
|
+
private static var connections: [String: CoreDeviceHID] = [:]
|
|
187
|
+
private static var angles: [String: Double] = [:]
|
|
188
|
+
|
|
189
|
+
/// Whether the simulator takes hinge input. Blocks on a CoreSimulator lookup;
|
|
190
|
+
/// call it off the main thread.
|
|
191
|
+
public static func isAvailable(udid: String) -> Bool { connection(udid: udid) != nil }
|
|
192
|
+
|
|
193
|
+
/// The posture Stim Desktop last moved this simulator to, if any.
|
|
194
|
+
public static func lastPosture(udid: String) -> DuoPosture? {
|
|
195
|
+
guard let angle = lastAngle(udid: udid) else { return nil }
|
|
196
|
+
return DuoPosture.allCases.first { $0.hingeAngle == angle }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// The hinge angle Stim Desktop last moved this simulator to, in degrees.
|
|
200
|
+
public static func lastAngle(udid: String) -> Double? {
|
|
201
|
+
lock.lock()
|
|
202
|
+
defer { lock.unlock() }
|
|
203
|
+
return angles[udid]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// Estimates the hinge angle from the observed active panel and the last
|
|
207
|
+
/// requested angle. An observed panel change overrides a conflicting cache.
|
|
208
|
+
public static func estimatedAngle(udid: String, folded: Bool?) -> Double {
|
|
209
|
+
DuoHinge.estimatedAngle(remembered: lastAngle(udid: udid), folded: folded)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// Sweeps the hinge from `angle`, where it is now, to `posture`, about one
|
|
213
|
+
/// second for a full fold. Returns an error message when the input service
|
|
214
|
+
/// is missing or closes.
|
|
215
|
+
public static func move(udid: String, from angle: Double, to posture: DuoPosture) async -> String? {
|
|
216
|
+
await move(udid: udid, from: angle, to: posture.hingeAngle)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Sweeps the simulated hinge to an angle in 0...180 degrees. Returns an
|
|
220
|
+
/// error message when the input service is missing or closes.
|
|
221
|
+
public static func move(udid: String, from angle: Double, to target: Double) async -> String? {
|
|
222
|
+
guard let hid = await Task.detached(operation: { connection(udid: udid) }).value else {
|
|
223
|
+
return "This simulator has no hinge input service."
|
|
224
|
+
}
|
|
225
|
+
while !hid.isReady, hid.isConnected { try? await Task.sleep(for: .milliseconds(50)) }
|
|
226
|
+
guard hid.isConnected else { return "The simulator's input connection closed. Try again." }
|
|
227
|
+
for angle in DuoHinge.sweep(from: angle, to: target) {
|
|
228
|
+
guard let report = DuoHinge.report(angle: angle) else { return "IOKit could not serialize the hinge report." }
|
|
229
|
+
hid.vendorDefined(report)
|
|
230
|
+
try? await Task.sleep(for: .milliseconds(16))
|
|
231
|
+
}
|
|
232
|
+
guard hid.isConnected else { return "The simulator's input connection closed. Try again." }
|
|
233
|
+
remember(target, udid: udid)
|
|
234
|
+
return nil
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// The Virtualization provider overwrites ordinary Duo rotation events; its enum follows
|
|
238
|
+
// https://github.com/kmagiera/Siniulator/blob/e181521a32595c2e537e1ee6c359aae254dbfed3/Sources/Siniulator/Input.swift#L115-L146
|
|
239
|
+
static func orient(udid: String, orientation: UInt32) -> Bool {
|
|
240
|
+
guard let hid = connection(udid: udid) else { return false }
|
|
241
|
+
while !hid.isReady, hid.isConnected { Thread.sleep(forTimeInterval: 0.05) }
|
|
242
|
+
guard hid.isConnected else { return false }
|
|
243
|
+
let values = ["portrait", "pud", "landscape-left", "landscape-right"]
|
|
244
|
+
let event: NSDictionary = [
|
|
245
|
+
"provider": "com.apple.Virtualization.VirtualMachines", "source": "orientation-picker-control", "type": "enum",
|
|
246
|
+
"value": values[Int(orientation) - 1],
|
|
247
|
+
]
|
|
248
|
+
guard let report = IOCFSerialize(event, CFOptionFlags(kIOCFSerializeToBinary)) as Data? else { return false }
|
|
249
|
+
hid.vendorDefined(report)
|
|
250
|
+
return hid.isConnected
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private static func remember(_ angle: Double, udid: String) {
|
|
254
|
+
lock.lock()
|
|
255
|
+
angles[udid] = angle
|
|
256
|
+
lock.unlock()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private static func connection(udid: String) -> CoreDeviceHID? {
|
|
260
|
+
lock.lock()
|
|
261
|
+
if let hid = connections[udid] {
|
|
262
|
+
if hid.isConnected {
|
|
263
|
+
lock.unlock()
|
|
264
|
+
return hid
|
|
265
|
+
}
|
|
266
|
+
angles[udid] = nil
|
|
267
|
+
}
|
|
268
|
+
lock.unlock()
|
|
269
|
+
guard let device = CoreSimulator.device(udid: udid),
|
|
270
|
+
let hid = CoreDeviceHID(device: device, feature: CoreDeviceHID.vendorDefined)
|
|
271
|
+
else { return nil }
|
|
272
|
+
lock.lock()
|
|
273
|
+
connections[udid] = hid
|
|
274
|
+
lock.unlock()
|
|
275
|
+
return hid
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -3,16 +3,33 @@ import Foundation
|
|
|
3
3
|
|
|
4
4
|
/// Turns a booted simulator a quarter turn, as Simulator.app's Rotate Left and
|
|
5
5
|
/// Rotate Right do. Returns false when the simulator cannot be reached.
|
|
6
|
+
/// A CoreSimulator lookup and input activation can block; call this off the main thread.
|
|
6
7
|
public enum SimulatorRotation {
|
|
8
|
+
private static let lock = NSLock()
|
|
7
9
|
private static var sent: [String: UInt32] = [:]
|
|
8
10
|
|
|
9
11
|
public static func rotate(udid: String, clockwise: Bool) -> Bool {
|
|
10
12
|
guard let device = CoreSimulator.device(udid: udid) else { return false }
|
|
11
|
-
let
|
|
12
|
-
|
|
13
|
+
let displays = CoreSimulator.displays(udid: udid).filter { $0.screenProperties?.screenType == 0 }
|
|
14
|
+
let duo = displays.count > 1
|
|
15
|
+
let display = duo ? displays.first { $0.framebufferSurface.map { !isBlack($0) } ?? false } : displays.first
|
|
16
|
+
lock.lock()
|
|
17
|
+
let remembered = sent[udid]
|
|
18
|
+
lock.unlock()
|
|
19
|
+
let current =
|
|
20
|
+
remembered
|
|
21
|
+
?? deviceOrientation(
|
|
22
|
+
interface: display?.screenProperties?.uiOrientation ?? 1,
|
|
23
|
+
innerPanel: duo && display?.screenProperties?.screenID == displays.last?.screenProperties?.screenID)
|
|
13
24
|
let next = quarterTurn(from: current, clockwise: clockwise)
|
|
14
|
-
|
|
25
|
+
if duo {
|
|
26
|
+
guard SimulatorPosture.orient(udid: udid, orientation: next) else { return false }
|
|
27
|
+
} else {
|
|
28
|
+
guard send(orientation: next, to: device) else { return false }
|
|
29
|
+
}
|
|
30
|
+
lock.lock()
|
|
15
31
|
sent[udid] = next
|
|
32
|
+
lock.unlock()
|
|
16
33
|
return true
|
|
17
34
|
}
|
|
18
35
|
|
|
@@ -47,13 +64,16 @@ public enum SimulatorRotation {
|
|
|
47
64
|
}
|
|
48
65
|
|
|
49
66
|
/// The UIDeviceOrientation that shows a UIInterfaceOrientation upright.
|
|
50
|
-
|
|
67
|
+
/// The Duo inner panel is natively a quarter turn from its cover panel.
|
|
68
|
+
func deviceOrientation(interface: UInt32, innerPanel: Bool = false) -> UInt32 {
|
|
69
|
+
let orientation: UInt32
|
|
51
70
|
switch interface {
|
|
52
|
-
case 3:
|
|
53
|
-
case 4:
|
|
54
|
-
case 2:
|
|
55
|
-
default:
|
|
71
|
+
case 3: orientation = 4
|
|
72
|
+
case 4: orientation = 3
|
|
73
|
+
case 2: orientation = 2
|
|
74
|
+
default: orientation = 1
|
|
56
75
|
}
|
|
76
|
+
return innerPanel ? quarterTurn(from: orientation, clockwise: false) : orientation
|
|
57
77
|
}
|
|
58
78
|
|
|
59
79
|
/// The UIDeviceOrientation a quarter turn from `orientation`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Siniulator contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Where the space Stim Desktop measures lives. Stim's own directories are sized by `stim gc --json`;
|
|
4
|
+
/// these are the paths the app sizes itself with `du`, none of them under `$STIM_HOME`.
|
|
5
|
+
public struct StoragePaths: Equatable, Sendable {
|
|
6
|
+
public var home: String
|
|
7
|
+
public var simulatorDevices: String
|
|
8
|
+
public var avds: String
|
|
9
|
+
public var derivedData: String
|
|
10
|
+
public var gradleCaches: String
|
|
11
|
+
public var libraryCaches: String
|
|
12
|
+
public var systemImages: String
|
|
13
|
+
|
|
14
|
+
/// The AVD directory follows the Android tools: `ANDROID_AVD_HOME`, then `ANDROID_USER_HOME/avd`, then `~/.android/avd`.
|
|
15
|
+
public init(home: String, environment: [String: String] = [:]) {
|
|
16
|
+
self.home = home
|
|
17
|
+
simulatorDevices = "\(home)/Library/Developer/CoreSimulator/Devices"
|
|
18
|
+
avds =
|
|
19
|
+
environment["ANDROID_AVD_HOME"] ?? environment["ANDROID_USER_HOME"].map { "\($0)/avd" } ?? "\(home)/.android/avd"
|
|
20
|
+
derivedData = "\(home)/Library/Developer/Xcode/DerivedData"
|
|
21
|
+
gradleCaches = "\(home)/.gradle/caches"
|
|
22
|
+
libraryCaches = "\(home)/Library/Caches"
|
|
23
|
+
let sdk =
|
|
24
|
+
[environment["ANDROID_HOME"], environment["ANDROID_SDK_ROOT"]].compactMap { $0 }.first { !$0.isEmpty }
|
|
25
|
+
?? "\(home)/Library/Android/sdk"
|
|
26
|
+
systemImages = "\(sdk.hasSuffix("/") ? String(sdk.dropLast()) : sdk)/system-images"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public func simulator(_ udid: String) -> String { "\(simulatorDevices)/\(udid)" }
|
|
30
|
+
public func avd(_ name: String) -> String { "\(avds)/\(name).avd" }
|
|
31
|
+
|
|
32
|
+
/// Directories `du` sizes entry by entry, with the depth that reaches each AVD and each system image.
|
|
33
|
+
public var deviceSets: [(path: String, depth: Int)] { [(avds, 1), (systemImages, 3)] }
|
|
34
|
+
|
|
35
|
+
public var unmanaged: [(title: String, path: String)] {
|
|
36
|
+
[
|
|
37
|
+
("Xcode DerivedData", derivedData),
|
|
38
|
+
("Gradle caches", gradleCaches),
|
|
39
|
+
("~/Library/Caches", libraryCaches),
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -11,6 +11,7 @@ struct AccessUnit {
|
|
|
11
11
|
var capturedAt: Double
|
|
12
12
|
var width: Int
|
|
13
13
|
var height: Int
|
|
14
|
+
var artworkTurns: Int?
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/// A low-latency hardware H.264 encoder: real time, no frame reordering, a keyframe at least every 2 s.
|
|
@@ -74,13 +75,13 @@ final class VideoEncoder {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
/// `quarterTurns` counter-clockwise quarter turns make the source upright.
|
|
77
|
-
func encode(_ source: CVPixelBuffer, quarterTurns: Int, capturedAt: Double) {
|
|
78
|
-
queue.sync { self.encodeOnQueue(source, quarterTurns: quarterTurns, capturedAt: capturedAt) }
|
|
78
|
+
func encode(_ source: CVPixelBuffer, quarterTurns: Int, capturedAt: Double, artworkTurns: Int? = nil) {
|
|
79
|
+
queue.sync { self.encodeOnQueue(source, quarterTurns: quarterTurns, capturedAt: capturedAt, artworkTurns: artworkTurns) }
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
/// Encodes an upright RGBA8888 image, as the emulator's `streamScreenshot` sends it. CoreVideo has no
|
|
82
83
|
/// RGBA pixel format, so the bytes are swizzled into a BGRA buffer.
|
|
83
|
-
func encode(rgba: Data, width: Int, height: Int, capturedAt: Double) {
|
|
84
|
+
func encode(rgba: Data, width: Int, height: Int, capturedAt: Double, artworkTurns: Int? = nil) {
|
|
84
85
|
queue.sync {
|
|
85
86
|
if bgra == nil || bgraSize != (width, height) {
|
|
86
87
|
let attributes: [CFString: Any] = [
|
|
@@ -109,11 +110,11 @@ final class VideoEncoder {
|
|
|
109
110
|
== kvImageNoError
|
|
110
111
|
}
|
|
111
112
|
CVPixelBufferUnlockBaseAddress(buffer, [])
|
|
112
|
-
if swizzled { encodeOnQueue(buffer, quarterTurns: 0, capturedAt: capturedAt) }
|
|
113
|
+
if swizzled { encodeOnQueue(buffer, quarterTurns: 0, capturedAt: capturedAt, artworkTurns: artworkTurns) }
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
private func encodeOnQueue(_ source: CVPixelBuffer, quarterTurns: Int, capturedAt: Double) {
|
|
117
|
+
private func encodeOnQueue(_ source: CVPixelBuffer, quarterTurns: Int, capturedAt: Double, artworkTurns: Int?) {
|
|
117
118
|
let sideways = quarterTurns % 2 != 0
|
|
118
119
|
let width = sideways ? CVPixelBufferGetHeight(source) : CVPixelBufferGetWidth(source)
|
|
119
120
|
let height = sideways ? CVPixelBufferGetWidth(source) : CVPixelBufferGetHeight(source)
|
|
@@ -143,7 +144,8 @@ final class VideoEncoder {
|
|
|
143
144
|
}
|
|
144
145
|
output(
|
|
145
146
|
AccessUnit(
|
|
146
|
-
data: unit.data, keyframe: unit.keyframe, capturedAt: capturedAt, width: target.width, height: target.height
|
|
147
|
+
data: unit.data, keyframe: unit.keyframe, capturedAt: capturedAt, width: target.width, height: target.height,
|
|
148
|
+
artworkTurns: artworkTurns))
|
|
147
149
|
}
|
|
148
150
|
}
|
|
149
151
|
|