@stim-cli/server 1.10.0 → 1.11.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 +194 -31
- package/dist/{protocol-dn2_TUeH.mjs → protocol-CmlG7X7_.mjs} +272 -11
- package/dist/protocol.d.mts +190 -11
- package/dist/protocol.mjs +2 -2
- package/dist/protocol.schema.json +484 -5
- package/dist/stim-frames/EmulatorDiscovery.swift +49 -0
- package/dist/stim-frames/EmulatorInput.swift +141 -0
- package/dist/stim-frames/GrpcFraming.swift +85 -0
- package/dist/stim-frames/InputMessages.swift +73 -0
- package/dist/stim-frames/ScreenshotMessages.swift +129 -0
- package/dist/stim-frames/ScreenshotStream.swift +130 -0
- package/dist/stim-frames/SimulatorDisplay.swift +116 -0
- package/dist/stim-frames/SimulatorHID.swift +101 -0
- package/dist/stim-frames/VideoEncoder.swift +282 -0
- package/dist/stim-frames/main.swift +389 -0
- package/dist/stim-server.mjs +1394 -124
- package/package.json +4 -4
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import ObjectiveC
|
|
3
|
+
|
|
4
|
+
// SimulatorKit's HID client and Indigo message builders are private. These
|
|
5
|
+
// signatures match the assertion strings and ObjC type encodings that Xcode 27
|
|
6
|
+
// ships. Touch points are fractions of the screen when the size is 1x1, and
|
|
7
|
+
// 0x32 is the Indigo target of the main screen's digitizer.
|
|
8
|
+
private typealias AllocFn = @convention(c) (AnyClass, Selector) -> Unmanaged<AnyObject>
|
|
9
|
+
private typealias InitFn = @convention(c) (
|
|
10
|
+
Unmanaged<AnyObject>, Selector, AnyObject, UnsafeMutablePointer<Unmanaged<NSError>?>?
|
|
11
|
+
) -> Unmanaged<AnyObject>?
|
|
12
|
+
private typealias SendFn = @convention(c) (
|
|
13
|
+
AnyObject, Selector, UnsafeMutableRawPointer, Bool, DispatchQueue?, (@convention(block) (NSError?) -> Void)?
|
|
14
|
+
) -> Void
|
|
15
|
+
private typealias MouseMessageFn = @convention(c) (
|
|
16
|
+
UnsafePointer<CGPoint>, UnsafePointer<CGPoint>?, UInt32, UInt, CGSize, UInt32
|
|
17
|
+
) -> UnsafeMutableRawPointer?
|
|
18
|
+
private typealias KeyboardMessageFn = @convention(c) (UInt32, UInt32) -> UnsafeMutableRawPointer?
|
|
19
|
+
private typealias UsageForKeyCodeFn = @convention(c) (UInt32) -> UInt32
|
|
20
|
+
|
|
21
|
+
private enum SimulatorKit {
|
|
22
|
+
static let handle = dlopen(CoreSimulator.simulatorKitPath(CoreSimulator.developerDir), RTLD_NOW)
|
|
23
|
+
static let mouseMessage = symbol("IndigoHIDMessageForMouseNSEvent", MouseMessageFn.self)
|
|
24
|
+
static let keyboardMessage = symbol("IndigoHIDMessageForKeyboardArbitrary", KeyboardMessageFn.self)
|
|
25
|
+
static let usageForKeyCode = symbol("hidUsageForCGKeyCode", UsageForKeyCodeFn.self)
|
|
26
|
+
|
|
27
|
+
static func symbol<T>(_ name: String, _ type: T.Type) -> T? {
|
|
28
|
+
guard let handle, let pointer = dlsym(handle, name) else { return nil }
|
|
29
|
+
return unsafeBitCast(pointer, to: type)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
enum TouchPhase {
|
|
34
|
+
case down, move, up
|
|
35
|
+
|
|
36
|
+
var eventType: NSEvent.EventType {
|
|
37
|
+
switch self {
|
|
38
|
+
case .down: return .leftMouseDown
|
|
39
|
+
case .move: return .leftMouseDragged
|
|
40
|
+
case .up: return .leftMouseUp
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
final class SimulatorHID {
|
|
46
|
+
private static let mainScreenTarget: UInt32 = 0x32
|
|
47
|
+
private static let keyDown: UInt32 = 1
|
|
48
|
+
private static let keyUp: UInt32 = 2
|
|
49
|
+
private static let sendSelector = NSSelectorFromString("sendWithMessage:freeWhenDone:completionQueue:completion:")
|
|
50
|
+
|
|
51
|
+
private let client: AnyObject
|
|
52
|
+
private let send: SendFn
|
|
53
|
+
|
|
54
|
+
init?(udid: String) {
|
|
55
|
+
let allocSelector = NSSelectorFromString("alloc")
|
|
56
|
+
let initSelector = NSSelectorFromString("initWithDevice:error:")
|
|
57
|
+
guard SimulatorKit.handle != nil,
|
|
58
|
+
let device = CoreSimulator.device(udid: udid),
|
|
59
|
+
let cls = NSClassFromString("_TtC12SimulatorKit24SimDeviceLegacyHIDClient"),
|
|
60
|
+
let metaclass = object_getClass(cls),
|
|
61
|
+
class_respondsToSelector(metaclass, allocSelector),
|
|
62
|
+
class_respondsToSelector(cls, initSelector),
|
|
63
|
+
class_respondsToSelector(cls, Self.sendSelector)
|
|
64
|
+
else { return nil }
|
|
65
|
+
let alloc = unsafeBitCast(class_getMethodImplementation(metaclass, allocSelector), to: AllocFn.self)
|
|
66
|
+
let initialize = unsafeBitCast(class_getMethodImplementation(cls, initSelector), to: InitFn.self)
|
|
67
|
+
guard let client = initialize(alloc(cls, allocSelector), initSelector, device, nil)?.takeRetainedValue()
|
|
68
|
+
else { return nil }
|
|
69
|
+
self.client = client
|
|
70
|
+
send = unsafeBitCast(class_getMethodImplementation(cls, Self.sendSelector), to: SendFn.self)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The simulator's SimulatorHID addresses a display's digitizer by its screen
|
|
74
|
+
// ID with bit 30 set, and backboardd aborts on a target it has no service
|
|
75
|
+
// for. The main display keeps its legacy target.
|
|
76
|
+
static func digitizerTarget(screenID: UInt32) -> UInt32 {
|
|
77
|
+
screenID == 1 ? mainScreenTarget : 0x4000_0000 | screenID
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// `point` is a fraction of the screen, origin top-left.
|
|
81
|
+
func touch(_ phase: TouchPhase, at point: CGPoint, screenID: UInt32 = 1) {
|
|
82
|
+
let target = Self.digitizerTarget(screenID: screenID)
|
|
83
|
+
var point = point
|
|
84
|
+
// The builder returns nil for a drag that arrives within 16 ms of the previous message.
|
|
85
|
+
guard let message = SimulatorKit.mouseMessage?(
|
|
86
|
+
&point, nil, target, UInt(phase.eventType.rawValue), CGSize(width: 1, height: 1), 0)
|
|
87
|
+
else { return }
|
|
88
|
+
deliver(message)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func key(code: UInt16, down: Bool) {
|
|
92
|
+
guard let usage = SimulatorKit.usageForKeyCode?(UInt32(code)), usage != 0,
|
|
93
|
+
let message = SimulatorKit.keyboardMessage?(usage, down ? Self.keyDown : Self.keyUp)
|
|
94
|
+
else { return }
|
|
95
|
+
deliver(message)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private func deliver(_ message: UnsafeMutableRawPointer) {
|
|
99
|
+
send(client, Self.sendSelector, message, true, nil, nil)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import Accelerate
|
|
2
|
+
import CoreMedia
|
|
3
|
+
import CoreVideo
|
|
4
|
+
import Foundation
|
|
5
|
+
import VideoToolbox
|
|
6
|
+
|
|
7
|
+
/// One encoded H.264 access unit in Annex-B form. A keyframe starts with its SPS and PPS.
|
|
8
|
+
struct AccessUnit {
|
|
9
|
+
var data: Data
|
|
10
|
+
var keyframe: Bool
|
|
11
|
+
var capturedAt: Double
|
|
12
|
+
var width: Int
|
|
13
|
+
var height: Int
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// A low-latency hardware H.264 encoder: real time, no frame reordering, a keyframe at least every 2 s.
|
|
17
|
+
/// Each source frame is copied, rotated upright and scaled to fit `maxEdge` before encoding, so a source
|
|
18
|
+
/// surface the simulator keeps drawing into is never read after `encode` returns.
|
|
19
|
+
final class VideoEncoder {
|
|
20
|
+
static let minBitrate = 250_000
|
|
21
|
+
static let maxBitrate = 8_000_000
|
|
22
|
+
|
|
23
|
+
private let queue = DispatchQueue(label: "stim.video.encode")
|
|
24
|
+
private let output: (AccessUnit) -> Void
|
|
25
|
+
private var session: VTCompressionSession?
|
|
26
|
+
private var transfer: VTPixelTransferSession?
|
|
27
|
+
private var rotation: VTPixelRotationSession?
|
|
28
|
+
private var rotated: CVPixelBufferPool?
|
|
29
|
+
private var rotatedSize = (width: 0, height: 0)
|
|
30
|
+
private var size = (width: 0, height: 0)
|
|
31
|
+
private var forceKeyframe = true
|
|
32
|
+
private var lastTimestamp = CMTime.invalid
|
|
33
|
+
|
|
34
|
+
private var maxEdge: Int
|
|
35
|
+
private var fps: Int
|
|
36
|
+
private var bitrate: Int
|
|
37
|
+
private var bgra: CVPixelBufferPool?
|
|
38
|
+
private var bgraSize = (width: 0, height: 0)
|
|
39
|
+
|
|
40
|
+
init(maxEdge: Int, fps: Int, bitrate: Int, output: @escaping (AccessUnit) -> Void) {
|
|
41
|
+
self.maxEdge = maxEdge
|
|
42
|
+
self.fps = fps
|
|
43
|
+
self.bitrate = bitrate
|
|
44
|
+
self.output = output
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
deinit {
|
|
48
|
+
if let session { VTCompressionSessionInvalidate(session) }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// A disabled encoder drops its session and pools; the next frame after enabling starts with a keyframe.
|
|
52
|
+
func configure(enabled: Bool, maxEdge: Int, fps: Int, bitrate: Int) {
|
|
53
|
+
queue.async {
|
|
54
|
+
if !enabled {
|
|
55
|
+
self.invalidate()
|
|
56
|
+
self.transfer = nil
|
|
57
|
+
self.rotation = nil
|
|
58
|
+
self.rotated = nil
|
|
59
|
+
self.bgra = nil
|
|
60
|
+
}
|
|
61
|
+
self.maxEdge = maxEdge
|
|
62
|
+
if self.fps != fps, let session = self.session {
|
|
63
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: fps as CFNumber)
|
|
64
|
+
}
|
|
65
|
+
self.fps = fps
|
|
66
|
+
let clamped = min(max(bitrate, Self.minBitrate), Self.maxBitrate)
|
|
67
|
+
if self.bitrate != clamped, let session = self.session { Self.applyBitrate(session, clamped) }
|
|
68
|
+
self.bitrate = clamped
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func requestKeyframe() {
|
|
73
|
+
queue.async { self.forceKeyframe = true }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// `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) }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Encodes an upright RGBA8888 image, as the emulator's `streamScreenshot` sends it. CoreVideo has no
|
|
82
|
+
/// RGBA pixel format, so the bytes are swizzled into a BGRA buffer.
|
|
83
|
+
func encode(rgba: Data, width: Int, height: Int, capturedAt: Double) {
|
|
84
|
+
queue.sync {
|
|
85
|
+
if bgra == nil || bgraSize != (width, height) {
|
|
86
|
+
let attributes: [CFString: Any] = [
|
|
87
|
+
kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32BGRA,
|
|
88
|
+
kCVPixelBufferWidthKey: width,
|
|
89
|
+
kCVPixelBufferHeightKey: height,
|
|
90
|
+
kCVPixelBufferIOSurfacePropertiesKey: [:] as CFDictionary,
|
|
91
|
+
]
|
|
92
|
+
bgra = nil
|
|
93
|
+
CVPixelBufferPoolCreate(nil, nil, attributes as CFDictionary, &bgra)
|
|
94
|
+
bgraSize = (width, height)
|
|
95
|
+
}
|
|
96
|
+
var buffer: CVPixelBuffer?
|
|
97
|
+
guard rgba.count == width * height * 4, let bgra,
|
|
98
|
+
CVPixelBufferPoolCreatePixelBuffer(nil, bgra, &buffer) == kCVReturnSuccess, let buffer
|
|
99
|
+
else { return }
|
|
100
|
+
CVPixelBufferLockBaseAddress(buffer, [])
|
|
101
|
+
let swizzled = rgba.withUnsafeBytes { bytes -> Bool in
|
|
102
|
+
var source = vImage_Buffer(
|
|
103
|
+
data: UnsafeMutableRawPointer(mutating: bytes.baseAddress!), height: vImagePixelCount(height),
|
|
104
|
+
width: vImagePixelCount(width), rowBytes: width * 4)
|
|
105
|
+
var destination = vImage_Buffer(
|
|
106
|
+
data: CVPixelBufferGetBaseAddress(buffer), height: vImagePixelCount(height), width: vImagePixelCount(width),
|
|
107
|
+
rowBytes: CVPixelBufferGetBytesPerRow(buffer))
|
|
108
|
+
return vImagePermuteChannels_ARGB8888(&source, &destination, [2, 1, 0, 3], vImage_Flags(kvImageNoFlags))
|
|
109
|
+
== kvImageNoError
|
|
110
|
+
}
|
|
111
|
+
CVPixelBufferUnlockBaseAddress(buffer, [])
|
|
112
|
+
if swizzled { encodeOnQueue(buffer, quarterTurns: 0, capturedAt: capturedAt) }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func encodeOnQueue(_ source: CVPixelBuffer, quarterTurns: Int, capturedAt: Double) {
|
|
117
|
+
let sideways = quarterTurns % 2 != 0
|
|
118
|
+
let width = sideways ? CVPixelBufferGetHeight(source) : CVPixelBufferGetWidth(source)
|
|
119
|
+
let height = sideways ? CVPixelBufferGetWidth(source) : CVPixelBufferGetHeight(source)
|
|
120
|
+
let target = Self.fit(width: width, height: height, maxEdge: maxEdge)
|
|
121
|
+
guard let session = session(for: target), let pool = VTCompressionSessionGetPixelBufferPool(session),
|
|
122
|
+
let input = upright(source, quarterTurns: quarterTurns, width: width, height: height)
|
|
123
|
+
else { return }
|
|
124
|
+
var buffer: CVPixelBuffer?
|
|
125
|
+
guard CVPixelBufferPoolCreatePixelBuffer(nil, pool, &buffer) == kCVReturnSuccess, let buffer,
|
|
126
|
+
let transfer = transferSession(),
|
|
127
|
+
VTPixelTransferSessionTransferImage(transfer, from: input, to: buffer) == noErr
|
|
128
|
+
else { return }
|
|
129
|
+
var timestamp = CMTime(value: CMTimeValue(capturedAt * 1000), timescale: 1_000_000)
|
|
130
|
+
if lastTimestamp.isValid, timestamp <= lastTimestamp {
|
|
131
|
+
timestamp = CMTimeAdd(lastTimestamp, CMTime(value: 1, timescale: 1_000_000))
|
|
132
|
+
}
|
|
133
|
+
lastTimestamp = timestamp
|
|
134
|
+
let properties = forceKeyframe ? [kVTEncodeFrameOptionKey_ForceKeyFrame: true] as CFDictionary : nil
|
|
135
|
+
forceKeyframe = false
|
|
136
|
+
VTCompressionSessionEncodeFrame(
|
|
137
|
+
session, imageBuffer: buffer, presentationTimeStamp: timestamp, duration: .invalid,
|
|
138
|
+
frameProperties: properties, infoFlagsOut: nil
|
|
139
|
+
) { [output, weak self] status, _, sample in
|
|
140
|
+
guard status == noErr, let sample, let unit = Self.annexB(sample) else {
|
|
141
|
+
self?.requestKeyframe()
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
output(
|
|
145
|
+
AccessUnit(
|
|
146
|
+
data: unit.data, keyframe: unit.keyframe, capturedAt: capturedAt, width: target.width, height: target.height))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static func fit(width: Int, height: Int, maxEdge: Int) -> (width: Int, height: Int) {
|
|
151
|
+
let scale = min(1, Double(maxEdge) / Double(max(width, height)))
|
|
152
|
+
let even = { (value: Double) in max(2, Int(value.rounded(.down)) & ~1) }
|
|
153
|
+
return (even(Double(width) * scale), even(Double(height) * scale))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private func session(for target: (width: Int, height: Int)) -> VTCompressionSession? {
|
|
157
|
+
if let session, size == target { return session }
|
|
158
|
+
invalidate()
|
|
159
|
+
size = target
|
|
160
|
+
forceKeyframe = true
|
|
161
|
+
let specification: [CFString: Any] = [
|
|
162
|
+
kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder: true,
|
|
163
|
+
kVTVideoEncoderSpecification_EnableLowLatencyRateControl: true,
|
|
164
|
+
]
|
|
165
|
+
let attributes: [CFString: Any] = [
|
|
166
|
+
kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
|
167
|
+
kCVPixelBufferIOSurfacePropertiesKey: [:] as CFDictionary,
|
|
168
|
+
]
|
|
169
|
+
var created: VTCompressionSession?
|
|
170
|
+
guard
|
|
171
|
+
VTCompressionSessionCreate(
|
|
172
|
+
allocator: nil, width: Int32(target.width), height: Int32(target.height), codecType: kCMVideoCodecType_H264,
|
|
173
|
+
encoderSpecification: specification as CFDictionary, imageBufferAttributes: attributes as CFDictionary,
|
|
174
|
+
compressedDataAllocator: nil, outputCallback: nil, refcon: nil, compressionSessionOut: &created) == noErr,
|
|
175
|
+
let created
|
|
176
|
+
else { return nil }
|
|
177
|
+
let properties: [CFString: Any] = [
|
|
178
|
+
kVTCompressionPropertyKey_RealTime: true,
|
|
179
|
+
kVTCompressionPropertyKey_AllowFrameReordering: false,
|
|
180
|
+
kVTCompressionPropertyKey_ProfileLevel: kVTProfileLevel_H264_Main_AutoLevel,
|
|
181
|
+
kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration: 2,
|
|
182
|
+
kVTCompressionPropertyKey_ExpectedFrameRate: fps,
|
|
183
|
+
]
|
|
184
|
+
for (key, value) in properties { VTSessionSetProperty(created, key: key, value: value as CFTypeRef) }
|
|
185
|
+
Self.applyBitrate(created, bitrate)
|
|
186
|
+
VTCompressionSessionPrepareToEncodeFrames(created)
|
|
187
|
+
session = created
|
|
188
|
+
return created
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private func invalidate() {
|
|
192
|
+
guard let session else { return }
|
|
193
|
+
VTCompressionSessionCompleteFrames(session, untilPresentationTimeStamp: .invalid)
|
|
194
|
+
VTCompressionSessionInvalidate(session)
|
|
195
|
+
self.session = nil
|
|
196
|
+
size = (0, 0)
|
|
197
|
+
forceKeyframe = true
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private static func applyBitrate(_ session: VTCompressionSession, _ bitrate: Int) {
|
|
201
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AverageBitRate, value: bitrate as CFNumber)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private func transferSession() -> VTPixelTransferSession? {
|
|
205
|
+
if transfer == nil {
|
|
206
|
+
VTPixelTransferSessionCreate(allocator: nil, pixelTransferSessionOut: &transfer)
|
|
207
|
+
if let transfer {
|
|
208
|
+
VTSessionSetProperty(transfer, key: kVTPixelTransferPropertyKey_ScalingMode, value: kVTScalingMode_Normal)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return transfer
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private func upright(_ source: CVPixelBuffer, quarterTurns: Int, width: Int, height: Int) -> CVPixelBuffer? {
|
|
215
|
+
let turns = ((quarterTurns % 4) + 4) % 4
|
|
216
|
+
if turns == 0 { return source }
|
|
217
|
+
if rotation == nil { VTPixelRotationSessionCreate(nil, &rotation) }
|
|
218
|
+
guard let rotation else { return nil }
|
|
219
|
+
let angle = [kVTRotation_0, kVTRotation_CCW90, kVTRotation_180, kVTRotation_CW90][turns]
|
|
220
|
+
VTSessionSetProperty(rotation, key: kVTPixelRotationPropertyKey_Rotation, value: angle)
|
|
221
|
+
if rotated == nil || rotatedSize != (width, height) {
|
|
222
|
+
let attributes: [CFString: Any] = [
|
|
223
|
+
kCVPixelBufferPixelFormatTypeKey: CVPixelBufferGetPixelFormatType(source),
|
|
224
|
+
kCVPixelBufferWidthKey: width,
|
|
225
|
+
kCVPixelBufferHeightKey: height,
|
|
226
|
+
kCVPixelBufferIOSurfacePropertiesKey: [:] as CFDictionary,
|
|
227
|
+
]
|
|
228
|
+
rotated = nil
|
|
229
|
+
CVPixelBufferPoolCreate(nil, nil, attributes as CFDictionary, &rotated)
|
|
230
|
+
rotatedSize = (width, height)
|
|
231
|
+
}
|
|
232
|
+
var buffer: CVPixelBuffer?
|
|
233
|
+
guard let rotated, CVPixelBufferPoolCreatePixelBuffer(nil, rotated, &buffer) == kCVReturnSuccess, let buffer,
|
|
234
|
+
VTPixelRotationSessionRotateImage(rotation, source, buffer) == noErr
|
|
235
|
+
else { return nil }
|
|
236
|
+
return buffer
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private static let startCode: [UInt8] = [0, 0, 0, 1]
|
|
240
|
+
|
|
241
|
+
/// Converts the encoder's length-prefixed (AVCC) sample to Annex B, with the SPS and PPS before a keyframe.
|
|
242
|
+
static func annexB(_ sample: CMSampleBuffer) -> (data: Data, keyframe: Bool)? {
|
|
243
|
+
guard let block = CMSampleBufferGetDataBuffer(sample) else { return nil }
|
|
244
|
+
let attachments = CMSampleBufferGetSampleAttachmentsArray(sample, createIfNecessary: false) as? [[CFString: Any]]
|
|
245
|
+
let keyframe = !(attachments?.first?[kCMSampleAttachmentKey_NotSync] as? Bool ?? false)
|
|
246
|
+
var out = Data()
|
|
247
|
+
if keyframe, let format = CMSampleBufferGetFormatDescription(sample) {
|
|
248
|
+
var count = 0
|
|
249
|
+
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
|
|
250
|
+
format, parameterSetIndex: 0, parameterSetPointerOut: nil, parameterSetSizeOut: nil,
|
|
251
|
+
parameterSetCountOut: &count, nalUnitHeaderLengthOut: nil)
|
|
252
|
+
for index in 0..<count {
|
|
253
|
+
var pointer: UnsafePointer<UInt8>?
|
|
254
|
+
var length = 0
|
|
255
|
+
guard
|
|
256
|
+
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
|
|
257
|
+
format, parameterSetIndex: index, parameterSetPointerOut: &pointer, parameterSetSizeOut: &length,
|
|
258
|
+
parameterSetCountOut: nil, nalUnitHeaderLengthOut: nil) == noErr, let pointer
|
|
259
|
+
else { return nil }
|
|
260
|
+
out.append(contentsOf: startCode)
|
|
261
|
+
out.append(pointer, count: length)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
let total = CMBlockBufferGetDataLength(block)
|
|
265
|
+
guard total > 0 else { return nil }
|
|
266
|
+
var bytes = Data(count: total)
|
|
267
|
+
let copied = bytes.withUnsafeMutableBytes {
|
|
268
|
+
CMBlockBufferCopyDataBytes(block, atOffset: 0, dataLength: total, destination: $0.baseAddress!)
|
|
269
|
+
}
|
|
270
|
+
guard copied == noErr else { return nil }
|
|
271
|
+
var offset = 0
|
|
272
|
+
while offset + 4 <= bytes.count {
|
|
273
|
+
let length = bytes[offset..<(offset + 4)].reduce(0) { $0 << 8 | Int($1) }
|
|
274
|
+
offset += 4
|
|
275
|
+
guard length > 0, offset + length <= bytes.count else { return nil }
|
|
276
|
+
out.append(contentsOf: startCode)
|
|
277
|
+
out.append(bytes[offset..<(offset + length)])
|
|
278
|
+
offset += length
|
|
279
|
+
}
|
|
280
|
+
return (out, keyframe)
|
|
281
|
+
}
|
|
282
|
+
}
|