@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,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 current = sent[udid] ?? deviceOrientation(interface: CoreSimulator.displays(udid: udid).first?
12
- .screenProperties?.uiOrientation ?? 1)
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
- guard send(orientation: next, to: device) else { return false }
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
- func deviceOrientation(interface: UInt32) -> UInt32 {
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: return 4
53
- case 4: return 3
54
- case 2: return 2
55
- default: return 1
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