serve-sim-sjchmiela 0.1.39 → 0.1.41
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/Sources/SimNative/sim-capture.swift +93 -7
- package/Sources/SimNative/sim-module.swift +15 -2
- package/Sources/SimStreamHelper/H264Encoder.swift +112 -11
- package/Sources/SimStreamHelper/StreamFormat.swift +16 -0
- package/Sources/SimStreamHelper/WebRTCPublisher.swift +25 -1
- package/dist/middleware.js +33 -33
- package/dist/native/serve-sim-native.node +0 -0
- package/dist/serve-sim.js +60 -60
- package/package.json +1 -1
- package/src/middleware.ts +41 -8
- package/src/native.ts +13 -1
- package/src/state.ts +4 -0
|
@@ -15,6 +15,13 @@ import CoreMedia
|
|
|
15
15
|
typealias SimFrameCallback = (Int32, Data, Int32, Int32, Int32) -> Void
|
|
16
16
|
typealias SimInputCallback = (Data) -> Void
|
|
17
17
|
|
|
18
|
+
struct CaptureEngineOptions {
|
|
19
|
+
let mjpegFps: Int
|
|
20
|
+
let mjpegQuality: Double
|
|
21
|
+
let h264Fps: Int
|
|
22
|
+
let h264Bitrate: Int
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
final class CaptureEngine {
|
|
19
26
|
static let codecMJPEG: Int32 = 0
|
|
20
27
|
static let codecAVCC: Int32 = 1
|
|
@@ -26,8 +33,8 @@ final class CaptureEngine {
|
|
|
26
33
|
private let webRTCPublisher = WebRTCPublisher()
|
|
27
34
|
|
|
28
35
|
private let frameCapture = FrameCapture()
|
|
29
|
-
private let videoEncoder
|
|
30
|
-
private let h264Encoder
|
|
36
|
+
private let videoEncoder: VideoEncoder
|
|
37
|
+
private let h264Encoder: H264Encoder
|
|
31
38
|
private let encodeQueue = DispatchQueue(label: "napi.encode", qos: .userInteractive)
|
|
32
39
|
private let h264Queue = DispatchQueue(label: "napi.encode.h264", qos: .userInteractive)
|
|
33
40
|
private static let h264EncodeTimeoutMs = 500
|
|
@@ -42,26 +49,55 @@ final class CaptureEngine {
|
|
|
42
49
|
private var forceKeyframe = false
|
|
43
50
|
private var avccActive = false
|
|
44
51
|
private var h264FrameToken: UInt64 = 0
|
|
52
|
+
private var h264ReservedCount: Int64 = 0
|
|
53
|
+
private var h264SubmittedCount: Int64 = 0
|
|
54
|
+
private var h264BackpressureSkips: Int64 = 0
|
|
55
|
+
private var mjpegThrottleSkips: Int64 = 0
|
|
56
|
+
private var h264ThrottleSkips: Int64 = 0
|
|
57
|
+
private var avccNativeEmitCount: Int64 = 0
|
|
58
|
+
private var lastMjpegReservedAtNs: UInt64 = 0
|
|
59
|
+
private var lastH264ReservedAtNs: UInt64 = 0
|
|
60
|
+
private let mjpegMinFrameIntervalNs: UInt64
|
|
61
|
+
private let h264MinFrameIntervalNs: UInt64
|
|
45
62
|
private var started = false
|
|
46
63
|
private var stopped = false
|
|
47
64
|
|
|
48
|
-
init(
|
|
65
|
+
init(
|
|
66
|
+
deviceUDID: String,
|
|
67
|
+
options: CaptureEngineOptions,
|
|
68
|
+
onFrame: @escaping SimFrameCallback,
|
|
69
|
+
onWebRTCInput: @escaping SimInputCallback
|
|
70
|
+
) {
|
|
49
71
|
self.deviceUDID = deviceUDID
|
|
50
72
|
self.onFrame = onFrame
|
|
73
|
+
let mjpegFps = max(1, options.mjpegFps)
|
|
74
|
+
let h264Fps = max(1, options.h264Fps)
|
|
75
|
+
let h264Bitrate = max(1, options.h264Bitrate)
|
|
76
|
+
videoEncoder = VideoEncoder(quality: CGFloat(min(max(options.mjpegQuality, 0.0), 1.0)))
|
|
77
|
+
h264Encoder = H264Encoder(fps: h264Fps, bitrate: h264Bitrate)
|
|
78
|
+
mjpegMinFrameIntervalNs = UInt64(1_000_000_000 / mjpegFps)
|
|
79
|
+
h264MinFrameIntervalNs = UInt64(1_000_000_000 / h264Fps)
|
|
51
80
|
webRTCPublisher.onInput = onWebRTCInput
|
|
52
81
|
|
|
53
82
|
h264Encoder.onEncoded = { [weak self] encoded in
|
|
54
83
|
guard let self else { return }
|
|
84
|
+
self.avccNativeEmitCount += 1
|
|
85
|
+
let emitCount = self.avccNativeEmitCount
|
|
55
86
|
if let description = encoded.description {
|
|
87
|
+
streamLog("[stream:avcc] native emit description bytes=\(description.count)")
|
|
56
88
|
self.emit(codec: Self.codecAVCC,
|
|
57
89
|
data: AVCCEnvelope.description(avcc: description),
|
|
58
90
|
flags: Self.flagDescription)
|
|
59
91
|
}
|
|
60
92
|
switch encoded.kind {
|
|
61
93
|
case .keyframe:
|
|
94
|
+
streamLog("[stream:avcc] native emit keyframe bytes=\(encoded.avcc.count)")
|
|
62
95
|
self.emit(codec: Self.codecAVCC, data: AVCCEnvelope.keyframe(avcc: encoded.avcc),
|
|
63
96
|
flags: Self.flagKeyframe)
|
|
64
97
|
case .delta:
|
|
98
|
+
if streamShouldLog(emitCount) {
|
|
99
|
+
streamLog("[stream:avcc] native emit delta #\(emitCount) bytes=\(encoded.avcc.count)")
|
|
100
|
+
}
|
|
65
101
|
self.emit(codec: Self.codecAVCC, data: AVCCEnvelope.delta(avcc: encoded.avcc), flags: 0)
|
|
66
102
|
}
|
|
67
103
|
}
|
|
@@ -100,11 +136,12 @@ final class CaptureEngine {
|
|
|
100
136
|
|
|
101
137
|
let h264Request = reserveH264EncodeIfNeeded()
|
|
102
138
|
let shouldSendWebRTC = webRTCPublisher.isActive
|
|
103
|
-
let shouldEncodeJpeg = encoderReady && !encoding
|
|
139
|
+
let shouldEncodeJpeg = encoderReady && !encoding && reserveMjpegEncodeIfNeeded()
|
|
104
140
|
if !shouldEncodeJpeg && h264Request == nil && !shouldSendWebRTC { return }
|
|
105
141
|
|
|
106
142
|
guard let stableFrame = copyPixelBuffer(pixelBuffer) else {
|
|
107
143
|
if let h264Request {
|
|
144
|
+
streamLog("[stream:avcc] failed to copy capture frame for H.264 token=\(h264Request.token)")
|
|
108
145
|
finishH264Encode(token: h264Request.token, restoreKeyframe: h264Request.forceKeyframe)
|
|
109
146
|
}
|
|
110
147
|
return
|
|
@@ -128,6 +165,14 @@ final class CaptureEngine {
|
|
|
128
165
|
if let h264Request {
|
|
129
166
|
h264Queue.async { [weak self] in
|
|
130
167
|
guard let self else { return }
|
|
168
|
+
self.h264SubmittedCount += 1
|
|
169
|
+
let submitted = self.h264SubmittedCount
|
|
170
|
+
if streamShouldLog(submitted) || h264Request.forceKeyframe {
|
|
171
|
+
streamLog(
|
|
172
|
+
"[stream:avcc] send frame to H264Encoder #\(submitted) token=\(h264Request.token) " +
|
|
173
|
+
"forceKeyframe=\(h264Request.forceKeyframe)"
|
|
174
|
+
)
|
|
175
|
+
}
|
|
131
176
|
self.h264Encoder.encode(stableFrame, forceKeyframe: h264Request.forceKeyframe) {
|
|
132
177
|
self.finishH264Encode(token: h264Request.token)
|
|
133
178
|
}
|
|
@@ -136,6 +181,19 @@ final class CaptureEngine {
|
|
|
136
181
|
}
|
|
137
182
|
}
|
|
138
183
|
|
|
184
|
+
private func reserveMjpegEncodeIfNeeded() -> Bool {
|
|
185
|
+
let now = DispatchTime.now().uptimeNanoseconds
|
|
186
|
+
if lastMjpegReservedAtNs != 0 && now - lastMjpegReservedAtNs < mjpegMinFrameIntervalNs {
|
|
187
|
+
mjpegThrottleSkips += 1
|
|
188
|
+
if streamShouldLog(mjpegThrottleSkips) {
|
|
189
|
+
streamLog("[stream:mjpeg] skip frame: fps throttle")
|
|
190
|
+
}
|
|
191
|
+
return false
|
|
192
|
+
}
|
|
193
|
+
lastMjpegReservedAtNs = now
|
|
194
|
+
return true
|
|
195
|
+
}
|
|
196
|
+
|
|
139
197
|
/// Copy the live Simulator IOSurface immediately on the capture queue. The
|
|
140
198
|
/// encoders run later and SimulatorKit recycles/mutates that IOSurface in
|
|
141
199
|
/// place, so passing the wrapper CVPixelBuffer across queues can encode a
|
|
@@ -176,12 +234,32 @@ final class CaptureEngine {
|
|
|
176
234
|
|
|
177
235
|
private func reserveH264EncodeIfNeeded() -> (forceKeyframe: Bool, token: UInt64)? {
|
|
178
236
|
h264Queue.sync {
|
|
179
|
-
guard avccActive
|
|
237
|
+
guard avccActive else { return nil }
|
|
238
|
+
let now = DispatchTime.now().uptimeNanoseconds
|
|
239
|
+
if !forceKeyframe && lastH264ReservedAtNs != 0 && now - lastH264ReservedAtNs < h264MinFrameIntervalNs {
|
|
240
|
+
h264ThrottleSkips += 1
|
|
241
|
+
if streamShouldLog(h264ThrottleSkips) {
|
|
242
|
+
streamLog("[stream:avcc] skip H.264 frame: fps throttle")
|
|
243
|
+
}
|
|
244
|
+
return nil
|
|
245
|
+
}
|
|
246
|
+
guard !h264Encoding else {
|
|
247
|
+
h264BackpressureSkips += 1
|
|
248
|
+
if streamShouldLog(h264BackpressureSkips) {
|
|
249
|
+
streamLog("[stream:avcc] skip H.264 frame: encode pending token=\(h264FrameToken)")
|
|
250
|
+
}
|
|
251
|
+
return nil
|
|
252
|
+
}
|
|
180
253
|
h264Encoding = true
|
|
181
254
|
h264FrameToken &+= 1
|
|
182
255
|
let token = h264FrameToken
|
|
183
256
|
let force = forceKeyframe
|
|
184
257
|
forceKeyframe = false
|
|
258
|
+
lastH264ReservedAtNs = now
|
|
259
|
+
h264ReservedCount += 1
|
|
260
|
+
if streamShouldLog(h264ReservedCount) || force {
|
|
261
|
+
streamLog("[stream:avcc] reserved H.264 frame #\(h264ReservedCount) token=\(token) forceKeyframe=\(force)")
|
|
262
|
+
}
|
|
185
263
|
return (forceKeyframe: force, token: token)
|
|
186
264
|
}
|
|
187
265
|
}
|
|
@@ -196,8 +274,10 @@ final class CaptureEngine {
|
|
|
196
274
|
|
|
197
275
|
private func scheduleH264EncodeTimeout(token: UInt64) {
|
|
198
276
|
h264Queue.asyncAfter(deadline: .now().advanced(by: .milliseconds(Self.h264EncodeTimeoutMs))) { [weak self] in
|
|
199
|
-
guard let self, self.h264FrameToken == token else { return }
|
|
277
|
+
guard let self, self.h264FrameToken == token, self.h264Encoding else { return }
|
|
200
278
|
self.h264Encoding = false
|
|
279
|
+
self.h264Encoder.handleEncodeTimeout()
|
|
280
|
+
streamLog("[stream:avcc] H.264 encode timed out token=\(token)")
|
|
201
281
|
}
|
|
202
282
|
}
|
|
203
283
|
|
|
@@ -207,12 +287,18 @@ final class CaptureEngine {
|
|
|
207
287
|
h264Queue.async { [weak self] in
|
|
208
288
|
guard let self else { return }
|
|
209
289
|
if active && !self.avccActive { self.forceKeyframe = true }
|
|
290
|
+
if active != self.avccActive {
|
|
291
|
+
streamLog("[stream:avcc] active=\(active) forceKeyframe=\(self.forceKeyframe)")
|
|
292
|
+
}
|
|
210
293
|
self.avccActive = active
|
|
211
294
|
}
|
|
212
295
|
}
|
|
213
296
|
|
|
214
297
|
func requestKeyframe() {
|
|
215
|
-
h264Queue.async { [weak self] in
|
|
298
|
+
h264Queue.async { [weak self] in
|
|
299
|
+
self?.forceKeyframe = true
|
|
300
|
+
streamLog("[stream:avcc] keyframe requested")
|
|
301
|
+
}
|
|
216
302
|
}
|
|
217
303
|
|
|
218
304
|
func handleWebRTCOffer(_ offerJson: String) throws -> String {
|
|
@@ -99,7 +99,15 @@ private func u32(_ v: Int) -> UInt32 {
|
|
|
99
99
|
private let queue: NodeAsyncQueue
|
|
100
100
|
private let inputQueue: NodeAsyncQueue
|
|
101
101
|
|
|
102
|
-
@NodeConstructor init(
|
|
102
|
+
@NodeConstructor init(
|
|
103
|
+
_ udid: String,
|
|
104
|
+
_ onFrame: NodeFunction,
|
|
105
|
+
_ onWebRTCInput: NodeFunction,
|
|
106
|
+
_ mjpegFps: Int,
|
|
107
|
+
_ mjpegQuality: Double,
|
|
108
|
+
_ h264Fps: Int,
|
|
109
|
+
_ h264Bitrate: Int
|
|
110
|
+
) throws {
|
|
103
111
|
// unref'd by NodeAsyncQueue's init, so the frame pipeline alone won't
|
|
104
112
|
// keep the event loop alive. Bounded queue + blocking AVCC preserves
|
|
105
113
|
// inter-frame ordering; MJPEG is nonblocking and drops under backpressure.
|
|
@@ -112,7 +120,12 @@ private func u32(_ v: Int) -> UInt32 {
|
|
|
112
120
|
|
|
113
121
|
// Capture the locals (not self) so the closure can be built before the
|
|
114
122
|
// engine property is initialized, and so it holds no strong ref to self.
|
|
115
|
-
engine = CaptureEngine(deviceUDID: udid,
|
|
123
|
+
engine = CaptureEngine(deviceUDID: udid, options: CaptureEngineOptions(
|
|
124
|
+
mjpegFps: mjpegFps,
|
|
125
|
+
mjpegQuality: mjpegQuality,
|
|
126
|
+
h264Fps: h264Fps,
|
|
127
|
+
h264Bitrate: h264Bitrate
|
|
128
|
+
), onFrame: { codec, data, w, h, flags in
|
|
116
129
|
// Runs on a native encode thread. AVCC is inter-frame H.264 — dropping
|
|
117
130
|
// a delta corrupts the decoder until the next IDR — so deliver it
|
|
118
131
|
// blocking; MJPEG is stateless and safe to drop. We copy the bytes
|
|
@@ -33,6 +33,10 @@ final class H264Encoder {
|
|
|
33
33
|
private let stateQueue = DispatchQueue(label: "H264Encoder.state")
|
|
34
34
|
private var emittedDescription = false
|
|
35
35
|
private var frameCount: Int64 = 0
|
|
36
|
+
private var encodedCount: Int64 = 0
|
|
37
|
+
private var lowLatencyEnabled = true
|
|
38
|
+
private var forceKeyframeAfterReset = false
|
|
39
|
+
private var retiredSessions: [VTCompressionSession] = []
|
|
36
40
|
|
|
37
41
|
init(fps: Int = 60, bitrate: Int = 6_000_000) {
|
|
38
42
|
self.fps = Int32(fps)
|
|
@@ -41,6 +45,7 @@ final class H264Encoder {
|
|
|
41
45
|
|
|
42
46
|
deinit {
|
|
43
47
|
if let session { VTCompressionSessionInvalidate(session) }
|
|
48
|
+
for session in retiredSessions { VTCompressionSessionInvalidate(session) }
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
/// Submit a frame. Returns immediately; `onEncoded` fires on VT's queue.
|
|
@@ -53,17 +58,33 @@ final class H264Encoder {
|
|
|
53
58
|
height = h
|
|
54
59
|
rebuildSession()
|
|
55
60
|
}
|
|
56
|
-
guard let session
|
|
61
|
+
guard let session else {
|
|
62
|
+
streamLog("[stream:h264] drop frame: VTCompressionSession unavailable size=\(w)x\(h)")
|
|
63
|
+
lock.unlock()
|
|
64
|
+
completion?()
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
guard let copy = copyBuffer(source) else {
|
|
68
|
+
streamLog("[stream:h264] drop frame: failed to copy pixel buffer size=\(w)x\(h)")
|
|
57
69
|
lock.unlock()
|
|
58
70
|
completion?()
|
|
59
71
|
return
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
frameCount += 1
|
|
75
|
+
let submittedFrame = frameCount
|
|
63
76
|
let pts = CMTime(value: frameCount, timescale: fps)
|
|
64
|
-
let
|
|
77
|
+
let effectiveForceKeyframe = forceKeyframe || forceKeyframeAfterReset
|
|
78
|
+
forceKeyframeAfterReset = false
|
|
79
|
+
let frameProps: NSDictionary? = effectiveForceKeyframe
|
|
65
80
|
? [kVTEncodeFrameOptionKey_ForceKeyFrame: kCFBooleanTrue!] as NSDictionary
|
|
66
81
|
: nil
|
|
82
|
+
if streamShouldLog(submittedFrame) || effectiveForceKeyframe {
|
|
83
|
+
streamLog(
|
|
84
|
+
"[stream:h264] submit frame #\(submittedFrame) size=\(w)x\(h) " +
|
|
85
|
+
"forceKeyframe=\(effectiveForceKeyframe)"
|
|
86
|
+
)
|
|
87
|
+
}
|
|
67
88
|
lock.unlock()
|
|
68
89
|
|
|
69
90
|
let status = VTCompressionSessionEncodeFrame(
|
|
@@ -75,14 +96,42 @@ final class H264Encoder {
|
|
|
75
96
|
infoFlagsOut: nil
|
|
76
97
|
) { [weak self] status, _, sampleBuffer in
|
|
77
98
|
defer { completion?() }
|
|
78
|
-
guard let self
|
|
79
|
-
|
|
99
|
+
guard let self else { return }
|
|
100
|
+
guard status == noErr else {
|
|
101
|
+
streamLog("[stream:h264] encode callback failed frame #\(submittedFrame) status=\(status)")
|
|
102
|
+
self.fallbackFromLowLatency(reason: "callback status=\(status)")
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
guard let sb = sampleBuffer else {
|
|
106
|
+
streamLog("[stream:h264] encode callback missing sample frame #\(submittedFrame)")
|
|
107
|
+
self.fallbackFromLowLatency(reason: "callback missing sample")
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
guard let encoded = self.extract(from: sb) else {
|
|
111
|
+
streamLog("[stream:h264] encode callback produced unextractable sample frame #\(submittedFrame)")
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
let encodedFrame = self.nextEncodedCount()
|
|
115
|
+
if streamShouldLog(encodedFrame) || encoded.description != nil || encoded.kind == .keyframe {
|
|
116
|
+
let kind = encoded.kind == .keyframe ? "keyframe" : "delta"
|
|
117
|
+
streamLog(
|
|
118
|
+
"[stream:h264] encoded #\(encodedFrame) kind=\(kind) bytes=\(encoded.avcc.count) " +
|
|
119
|
+
"descriptionBytes=\(encoded.description?.count ?? 0)"
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
self.onEncoded?(encoded)
|
|
80
123
|
}
|
|
81
124
|
if status != noErr {
|
|
125
|
+
streamLog("[stream:h264] VTCompressionSessionEncodeFrame failed frame #\(submittedFrame) status=\(status)")
|
|
126
|
+
fallbackFromLowLatency(reason: "submit status=\(status)")
|
|
82
127
|
completion?()
|
|
83
128
|
}
|
|
84
129
|
}
|
|
85
130
|
|
|
131
|
+
func handleEncodeTimeout() {
|
|
132
|
+
fallbackFromLowLatency(reason: "encode timeout")
|
|
133
|
+
}
|
|
134
|
+
|
|
86
135
|
func stop() {
|
|
87
136
|
lock.lock()
|
|
88
137
|
defer { lock.unlock() }
|
|
@@ -90,6 +139,8 @@ final class H264Encoder {
|
|
|
90
139
|
VTCompressionSessionInvalidate(session)
|
|
91
140
|
self.session = nil
|
|
92
141
|
}
|
|
142
|
+
for session in retiredSessions { VTCompressionSessionInvalidate(session) }
|
|
143
|
+
retiredSessions.removeAll()
|
|
93
144
|
pool = nil
|
|
94
145
|
}
|
|
95
146
|
|
|
@@ -150,17 +201,29 @@ final class H264Encoder {
|
|
|
150
201
|
compressionSessionOut: &sess
|
|
151
202
|
)
|
|
152
203
|
}
|
|
153
|
-
|
|
154
|
-
if
|
|
204
|
+
let preferredSpec: CFDictionary?
|
|
205
|
+
if lowLatencyEnabled {
|
|
206
|
+
preferredSpec = lowLatencySpec
|
|
207
|
+
} else {
|
|
208
|
+
preferredSpec = nil
|
|
209
|
+
}
|
|
210
|
+
var status = create(spec: preferredSpec)
|
|
211
|
+
if lowLatencyEnabled && (status != noErr || sess == nil) {
|
|
212
|
+
streamLog("[stream:h264] low-latency VT session create failed status=\(status); retrying default")
|
|
213
|
+
lowLatencyEnabled = false
|
|
214
|
+
forceKeyframeAfterReset = true
|
|
155
215
|
sess = nil
|
|
156
216
|
status = create(spec: nil)
|
|
157
217
|
}
|
|
158
|
-
guard status == noErr, let sess else {
|
|
218
|
+
guard status == noErr, let sess else {
|
|
219
|
+
streamLog("[stream:h264] VT session create failed status=\(status) size=\(width)x\(height)")
|
|
220
|
+
return
|
|
221
|
+
}
|
|
159
222
|
|
|
160
223
|
let props: [(CFString, Any)] = [
|
|
161
224
|
(kVTCompressionPropertyKey_RealTime, kCFBooleanTrue!),
|
|
162
225
|
(kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_High_AutoLevel),
|
|
163
|
-
(kVTCompressionPropertyKey_AllowFrameReordering,
|
|
226
|
+
(kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanTrue!),
|
|
164
227
|
(kVTCompressionPropertyKey_AverageBitRate, NSNumber(value: bitrate)),
|
|
165
228
|
(kVTCompressionPropertyKey_ExpectedFrameRate, NSNumber(value: fps)),
|
|
166
229
|
// 5s keyframe interval: IDRs are far larger than P-frames, so
|
|
@@ -169,12 +232,19 @@ final class H264Encoder {
|
|
|
169
232
|
(kVTCompressionPropertyKey_MaxKeyFrameInterval, NSNumber(value: fps * 5)),
|
|
170
233
|
]
|
|
171
234
|
for (key, value) in props {
|
|
172
|
-
VTSessionSetProperty(sess, key: key, value: value as CFTypeRef)
|
|
235
|
+
let propStatus = VTSessionSetProperty(sess, key: key, value: value as CFTypeRef)
|
|
236
|
+
if propStatus != noErr {
|
|
237
|
+
streamLog("[stream:h264] VTSessionSetProperty failed key=\(key) status=\(propStatus)")
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
let prepareStatus = VTCompressionSessionPrepareToEncodeFrames(sess)
|
|
241
|
+
if prepareStatus != noErr {
|
|
242
|
+
streamLog("[stream:h264] VTCompressionSessionPrepareToEncodeFrames status=\(prepareStatus)")
|
|
173
243
|
}
|
|
174
|
-
VTCompressionSessionPrepareToEncodeFrames(sess)
|
|
175
244
|
session = sess
|
|
176
245
|
stateQueue.sync {
|
|
177
246
|
emittedDescription = false
|
|
247
|
+
encodedCount = 0
|
|
178
248
|
}
|
|
179
249
|
|
|
180
250
|
// Pool feeding the deep-copy; BGRA matches the framebuffer surface.
|
|
@@ -185,10 +255,41 @@ final class H264Encoder {
|
|
|
185
255
|
kCVPixelBufferIOSurfacePropertiesKey as String: [:],
|
|
186
256
|
]
|
|
187
257
|
var newPool: CVPixelBufferPool?
|
|
188
|
-
CVPixelBufferPoolCreate(kCFAllocatorDefault, nil, attrs as CFDictionary, &newPool)
|
|
258
|
+
let poolStatus = CVPixelBufferPoolCreate(kCFAllocatorDefault, nil, attrs as CFDictionary, &newPool)
|
|
259
|
+
if poolStatus != kCVReturnSuccess || newPool == nil {
|
|
260
|
+
streamLog("[stream:h264] pixel buffer pool create failed status=\(poolStatus) size=\(width)x\(height)")
|
|
261
|
+
} else {
|
|
262
|
+
let mode = lowLatencyEnabled ? "low-latency" : "default"
|
|
263
|
+
streamLog("[stream:h264] VT session ready mode=\(mode) size=\(width)x\(height) fps=\(fps) bitrate=\(bitrate)")
|
|
264
|
+
}
|
|
189
265
|
pool = newPool
|
|
190
266
|
}
|
|
191
267
|
|
|
268
|
+
private func fallbackFromLowLatency(reason: String) {
|
|
269
|
+
lock.lock()
|
|
270
|
+
defer { lock.unlock() }
|
|
271
|
+
guard lowLatencyEnabled else { return }
|
|
272
|
+
streamLog("[stream:h264] low-latency encoder failed (\(reason)); default VT session will be used")
|
|
273
|
+
lowLatencyEnabled = false
|
|
274
|
+
forceKeyframeAfterReset = true
|
|
275
|
+
if let session {
|
|
276
|
+
retiredSessions.append(session)
|
|
277
|
+
self.session = nil
|
|
278
|
+
}
|
|
279
|
+
pool = nil
|
|
280
|
+
stateQueue.sync {
|
|
281
|
+
emittedDescription = false
|
|
282
|
+
encodedCount = 0
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private func nextEncodedCount() -> Int64 {
|
|
287
|
+
stateQueue.sync {
|
|
288
|
+
encodedCount += 1
|
|
289
|
+
return encodedCount
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
192
293
|
private func extract(from sample: CMSampleBuffer) -> Encoded? {
|
|
193
294
|
let isKeyframe = !notSync(sample)
|
|
194
295
|
guard let dataBuf = CMSampleBufferGetDataBuffer(sample) else { return nil }
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
|
+
/// Opt-in AVCC/H.264 diagnostics for staging stream failures. This path is hot
|
|
4
|
+
/// during playback, so keep it off unless explicitly requested.
|
|
5
|
+
let streamDebugEnabled =
|
|
6
|
+
ProcessInfo.processInfo.environment["SERVE_SIM_DEBUG_STREAM"] != nil ||
|
|
7
|
+
ProcessInfo.processInfo.environment["SERVE_SIM_DEBUG_AVCC"] != nil
|
|
8
|
+
|
|
9
|
+
@inline(__always)
|
|
10
|
+
func streamLog(_ message: @autoclosure () -> String) {
|
|
11
|
+
if streamDebugEnabled { print(message()) }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@inline(__always)
|
|
15
|
+
func streamShouldLog(_ count: Int64, first: Int64 = 5, every: Int64 = 120) -> Bool {
|
|
16
|
+
count <= first || count % every == 0
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
/// Wire format a viewer can request for the screen stream.
|
|
4
20
|
///
|
|
5
21
|
/// - `mjpeg`: stateless JPEG-per-frame inside a `multipart/x-mixed-replace`
|
|
@@ -12,6 +12,7 @@ struct WebRTCIceServerPayload: Codable {
|
|
|
12
12
|
struct WebRTCOfferPayload: Codable {
|
|
13
13
|
let type: String
|
|
14
14
|
let sdp: String
|
|
15
|
+
let codec: String?
|
|
15
16
|
let iceServers: [WebRTCIceServerPayload]?
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -103,7 +104,11 @@ final class WebRTCPublisher {
|
|
|
103
104
|
return
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
if let transceiver = peerConnection.addTransceiver(with: videoTrack) {
|
|
108
|
+
applyVideoCodecPreference(request.codec, to: transceiver)
|
|
109
|
+
} else {
|
|
110
|
+
_ = peerConnection.add(videoTrack, streamIds: ["stream0"])
|
|
111
|
+
}
|
|
107
112
|
let session = WebRTCSession(peerConnection: peerConnection, delegate: delegate)
|
|
108
113
|
self.session?.close()
|
|
109
114
|
self.session = session
|
|
@@ -154,6 +159,25 @@ final class WebRTCPublisher {
|
|
|
154
159
|
}
|
|
155
160
|
}
|
|
156
161
|
|
|
162
|
+
private func applyVideoCodecPreference(_ codec: String?, to transceiver: LKRTCRtpTransceiver) {
|
|
163
|
+
let preferredName = codec == "vp8" ? "VP8" : "H264"
|
|
164
|
+
let capabilities = factory.rtpSenderCapabilities(forKind: "video")
|
|
165
|
+
let preferredCodecs = capabilities.codecs.filter {
|
|
166
|
+
$0.name.caseInsensitiveCompare(preferredName) == .orderedSame ||
|
|
167
|
+
$0.mimeType.caseInsensitiveCompare("video/\(preferredName)") == .orderedSame
|
|
168
|
+
}
|
|
169
|
+
guard !preferredCodecs.isEmpty else {
|
|
170
|
+
print("[webrtc] No sender codec capability found for \(preferredName); using default order")
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
let remainingCodecs = capabilities.codecs.filter { capability in
|
|
174
|
+
!preferredCodecs.contains { $0 === capability }
|
|
175
|
+
}
|
|
176
|
+
let orderedCodecs = preferredCodecs + remainingCodecs
|
|
177
|
+
transceiver.codecPreferences = orderedCodecs
|
|
178
|
+
print("[webrtc] Preferred video codec: \(preferredName)")
|
|
179
|
+
}
|
|
180
|
+
|
|
157
181
|
private func makeError(_ message: String) -> Error {
|
|
158
182
|
NSError(domain: "serve-sim.webrtc", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
|
|
159
183
|
}
|