serve-sim-sjchmiela 0.1.52 → 0.1.53

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.
@@ -39,6 +39,8 @@ final class CaptureEngine {
39
39
  private let h264Encoder: H264Encoder
40
40
  private let encodeQueue = DispatchQueue(label: "napi.encode", qos: .userInteractive)
41
41
  private let h264Queue = DispatchQueue(label: "napi.encode.h264", qos: .userInteractive)
42
+ private let framePreparationQueue = DispatchQueue(label: "napi.frame.prepare", qos: .userInteractive)
43
+ private let framePreparationLock = NSLock()
42
44
  private static let h264EncodeTimeoutMs = 500
43
45
 
44
46
  // Mirrors main.swift's globals; mutated from the capture queue, read from the
@@ -51,6 +53,7 @@ final class CaptureEngine {
51
53
  private var encoding = false // MJPEG backpressure
52
54
  private var h264Encoding = false // H.264 backpressure
53
55
  private var forceKeyframe = false
56
+ private var mjpegActive = false
54
57
  private var avccActive = false
55
58
  private var h264FrameToken: UInt64 = 0
56
59
  private var h264ReservedCount: Int64 = 0
@@ -60,9 +63,11 @@ final class CaptureEngine {
60
63
  private var h264ThrottleSkips: Int64 = 0
61
64
  private var webRTCReservedCount: Int64 = 0
62
65
  private var webRTCThrottleSkips: Int64 = 0
66
+ private var webRTCDirectCount: Int64 = 0
63
67
  private var webRTCCopyFailures: Int64 = 0
64
68
  private var avccNativeEmitCount: Int64 = 0
65
69
  private var pixelBufferCopyCount: Int64 = 0
70
+ private var scaledFramePreparationPending = false
66
71
  private let statsLock = NSLock()
67
72
  private let statsStartNs = DispatchTime.now().uptimeNanoseconds
68
73
  private var statsCaptureFrames: Int64 = 0
@@ -71,10 +76,17 @@ final class CaptureEngine {
71
76
  private var statsScaleTotalMs = 0.0
72
77
  private var statsScaleLastMs = 0.0
73
78
  private var statsScaleMaxMs = 0.0
79
+ private var statsScaleBackpressureSkips: Int64 = 0
74
80
  private var statsScaleInputWidth = 0
75
81
  private var statsScaleInputHeight = 0
76
82
  private var statsScaleOutputWidth = 0
77
83
  private var statsScaleOutputHeight = 0
84
+ private var statsCopyCount: Int64 = 0
85
+ private var statsCopyTotalMs = 0.0
86
+ private var statsCopyLastMs = 0.0
87
+ private var statsCopyMaxMs = 0.0
88
+ private var statsCopyWidth = 0
89
+ private var statsCopyHeight = 0
78
90
  private var statsMjpegReserved: Int64 = 0
79
91
  private var statsMjpegThrottleSkips: Int64 = 0
80
92
  private var statsH264Reserved: Int64 = 0
@@ -82,6 +94,7 @@ final class CaptureEngine {
82
94
  private var statsH264BackpressureSkips: Int64 = 0
83
95
  private var statsWebRTCReserved: Int64 = 0
84
96
  private var statsWebRTCThrottleSkips: Int64 = 0
97
+ private var statsWebRTCDirect: Int64 = 0
85
98
  private var statsWebRTCCopyFailures: Int64 = 0
86
99
  private var lastMjpegReservedAtNs: UInt64 = 0
87
100
  private var lastH264ReservedAtNs: UInt64 = 0
@@ -168,12 +181,19 @@ final class CaptureEngine {
168
181
  recordCapturedFrame()
169
182
  let encodeSize = encodedSize(width: w, height: h)
170
183
 
171
- if !encoderReady || w != screenWidth || h != screenHeight || encodeSize.width != encodeWidth || encodeSize.height != encodeHeight {
184
+ let dimensionsChanged = w != screenWidth || h != screenHeight ||
185
+ encodeSize.width != encodeWidth || encodeSize.height != encodeHeight
186
+ if dimensionsChanged {
172
187
  screenWidth = w
173
188
  screenHeight = h
174
189
  encodeWidth = encodeSize.width
175
190
  encodeHeight = encodeSize.height
176
- videoEncoder.stop()
191
+ if encoderReady {
192
+ videoEncoder.stop()
193
+ encoderReady = false
194
+ }
195
+ }
196
+ if mjpegActive && !encoderReady {
177
197
  videoEncoder.setup(width: Int32(encodeSize.width), height: Int32(encodeSize.height), fps: 60) { [weak self] jpeg in
178
198
  self?.emit(codec: Self.codecMJPEG, data: jpeg, flags: 0)
179
199
  }
@@ -182,24 +202,79 @@ final class CaptureEngine {
182
202
 
183
203
  let h264Request = reserveH264EncodeIfNeeded()
184
204
  let shouldSendWebRTC = reserveWebRTCFrameIfNeeded()
185
- let shouldEncodeJpeg = encoderReady && !encoding && reserveMjpegEncodeIfNeeded()
205
+ let shouldEncodeJpeg = mjpegActive && encoderReady && !encoding && reserveMjpegEncodeIfNeeded()
186
206
  if !shouldEncodeJpeg && h264Request == nil && !shouldSendWebRTC { return }
187
207
 
188
- guard let stableFrame = copyPixelBuffer(pixelBuffer, targetWidth: encodeSize.width, targetHeight: encodeSize.height) else {
189
- if let h264Request {
190
- streamLog("[stream:avcc] failed to copy capture frame for H.264 token=\(h264Request.token)")
191
- finishH264Encode(token: h264Request.token, restoreKeyframe: h264Request.forceKeyframe)
208
+ if shouldSendWebRTC && h264Request == nil && !shouldEncodeJpeg && encodeSize.width == w && encodeSize.height == h {
209
+ webRTCDirectCount += 1
210
+ recordWebRTCDirect()
211
+ if streamShouldLog(webRTCDirectCount) {
212
+ streamLog("[webrtc] send direct frame #\(webRTCDirectCount) \(w)x\(h)")
213
+ }
214
+ webRTCPublisher.sendFrameDirect(pixelBuffer, timestamp: timestamp)
215
+ return
216
+ }
217
+
218
+ if encodeSize.width != w || encodeSize.height != h {
219
+ guard reserveScaledFramePreparation() else {
220
+ recordScaleBackpressureSkip()
221
+ if shouldEncodeJpeg { lastMjpegReservedAtNs = 0 }
222
+ if let h264Request {
223
+ finishH264Encode(token: h264Request.token, restoreKeyframe: h264Request.forceKeyframe)
224
+ }
225
+ return
226
+ }
227
+ if shouldEncodeJpeg { encoding = true }
228
+ guard let stableSourceFrame = copyPixelBuffer(pixelBuffer) else {
229
+ finishScaledFramePreparation()
230
+ if shouldEncodeJpeg { encoding = false }
231
+ handleStableFrameFailure(h264Request: h264Request, shouldSendWebRTC: shouldSendWebRTC)
232
+ return
192
233
  }
193
- if shouldSendWebRTC {
194
- webRTCCopyFailures += 1
195
- recordWebRTCCopyFailure()
196
- if streamShouldLog(webRTCCopyFailures) {
197
- streamLog("[webrtc] failed to copy capture frame for WebRTC")
234
+ framePreparationQueue.async { [weak self] in
235
+ guard let self else { return }
236
+ defer { self.finishScaledFramePreparation() }
237
+ guard let stableFrame = self.copyPixelBuffer(
238
+ stableSourceFrame,
239
+ targetWidth: encodeSize.width,
240
+ targetHeight: encodeSize.height
241
+ ) else {
242
+ if shouldEncodeJpeg { self.encoding = false }
243
+ self.handleStableFrameFailure(h264Request: h264Request, shouldSendWebRTC: shouldSendWebRTC)
244
+ return
198
245
  }
246
+ self.deliverStableFrame(
247
+ stableFrame,
248
+ timestamp: timestamp,
249
+ shouldSendWebRTC: shouldSendWebRTC,
250
+ shouldEncodeJpeg: shouldEncodeJpeg,
251
+ h264Request: h264Request
252
+ )
199
253
  }
200
254
  return
201
255
  }
202
256
 
257
+ guard let stableFrame = copyPixelBuffer(pixelBuffer, targetWidth: encodeSize.width, targetHeight: encodeSize.height) else {
258
+ handleStableFrameFailure(h264Request: h264Request, shouldSendWebRTC: shouldSendWebRTC)
259
+ return
260
+ }
261
+ if shouldEncodeJpeg { encoding = true }
262
+ deliverStableFrame(
263
+ stableFrame,
264
+ timestamp: timestamp,
265
+ shouldSendWebRTC: shouldSendWebRTC,
266
+ shouldEncodeJpeg: shouldEncodeJpeg,
267
+ h264Request: h264Request
268
+ )
269
+ }
270
+
271
+ private func deliverStableFrame(
272
+ _ stableFrame: CVPixelBuffer,
273
+ timestamp: CMTime,
274
+ shouldSendWebRTC: Bool,
275
+ shouldEncodeJpeg: Bool,
276
+ h264Request: (forceKeyframe: Bool, token: UInt64)?
277
+ ) {
203
278
  if shouldSendWebRTC {
204
279
  webRTCPublisher.sendFrame(stableFrame, timestamp: timestamp)
205
280
  }
@@ -234,6 +309,23 @@ final class CaptureEngine {
234
309
  }
235
310
  }
236
311
 
312
+ private func handleStableFrameFailure(
313
+ h264Request: (forceKeyframe: Bool, token: UInt64)?,
314
+ shouldSendWebRTC: Bool
315
+ ) {
316
+ if let h264Request {
317
+ streamLog("[stream:avcc] failed to prepare capture frame for H.264 token=\(h264Request.token)")
318
+ finishH264Encode(token: h264Request.token, restoreKeyframe: h264Request.forceKeyframe)
319
+ }
320
+ if shouldSendWebRTC {
321
+ webRTCCopyFailures += 1
322
+ recordWebRTCCopyFailure()
323
+ if streamShouldLog(webRTCCopyFailures) {
324
+ streamLog("[webrtc] failed to prepare capture frame for WebRTC")
325
+ }
326
+ }
327
+ }
328
+
237
329
  private func reserveMjpegEncodeIfNeeded() -> Bool {
238
330
  let now = DispatchTime.now().uptimeNanoseconds
239
331
  if lastMjpegReservedAtNs != 0 && now - lastMjpegReservedAtNs < mjpegMinFrameIntervalNs {
@@ -281,9 +373,22 @@ final class CaptureEngine {
281
373
  let dstStride = CVPixelBufferGetBytesPerRow(dst)
282
374
  let rows = CVPixelBufferGetHeight(source)
283
375
  let copyBytes = min(srcStride, dstStride)
376
+ let startNs = DispatchTime.now().uptimeNanoseconds
284
377
  for row in 0..<rows {
285
378
  memcpy(dstAddr + row * dstStride, srcAddr + row * srcStride, copyBytes)
286
379
  }
380
+ let durationMs = Double(DispatchTime.now().uptimeNanoseconds - startNs) / 1_000_000.0
381
+ recordCopiedFrame(
382
+ width: width,
383
+ height: height,
384
+ durationMs: durationMs
385
+ )
386
+ if streamShouldLog(statsCopyCount) {
387
+ streamLog(
388
+ "[stream] copied frame #\(statsCopyCount) \(width)x\(height) " +
389
+ "ms=\(String(format: "%.2f", durationMs))"
390
+ )
391
+ }
287
392
  return dst
288
393
  }
289
394
 
@@ -426,6 +531,7 @@ final class CaptureEngine {
426
531
  let uptimeSec = max(0.001, Double(nowNs - statsStartNs) / 1_000_000_000.0)
427
532
  let captureStats: [String: Any]
428
533
  let scaleStats: [String: Any]
534
+ let copyStats: [String: Any]
429
535
  let mjpegStats: [String: Any]
430
536
  let h264Stats: [String: Any]
431
537
  let webRTCStats: [String: Any]
@@ -449,11 +555,22 @@ final class CaptureEngine {
449
555
  "msLast": statsScaleLastMs,
450
556
  "msAvg": statsScaleCount > 0 ? statsScaleTotalMs / Double(statsScaleCount) : 0.0,
451
557
  "msMax": statsScaleMaxMs,
558
+ "backpressureSkips": statsScaleBackpressureSkips,
559
+ ]
560
+ copyStats = [
561
+ "frames": statsCopyCount,
562
+ "avgFps": Double(statsCopyCount) / uptimeSec,
563
+ "width": statsCopyWidth,
564
+ "height": statsCopyHeight,
565
+ "msLast": statsCopyLastMs,
566
+ "msAvg": statsCopyCount > 0 ? statsCopyTotalMs / Double(statsCopyCount) : 0.0,
567
+ "msMax": statsCopyMaxMs,
452
568
  ]
453
569
  mjpegStats = [
454
570
  "reserved": statsMjpegReserved,
455
571
  "reservedAvgFps": Double(statsMjpegReserved) / uptimeSec,
456
572
  "throttleSkips": statsMjpegThrottleSkips,
573
+ "active": mjpegActive,
457
574
  ]
458
575
  h264Stats = [
459
576
  "reserved": statsH264Reserved,
@@ -466,6 +583,7 @@ final class CaptureEngine {
466
583
  "reserved": statsWebRTCReserved,
467
584
  "reservedAvgFps": Double(statsWebRTCReserved) / uptimeSec,
468
585
  "throttleSkips": statsWebRTCThrottleSkips,
586
+ "direct": statsWebRTCDirect,
469
587
  "copyFailures": statsWebRTCCopyFailures,
470
588
  "minFrameIntervalNs": webRTCMinFrameIntervalNs,
471
589
  "maxFps": webRTCMaxFps,
@@ -478,6 +596,7 @@ final class CaptureEngine {
478
596
  "maxDimension": maxDimension,
479
597
  "capture": captureStats,
480
598
  "scale": scaleStats,
599
+ "copy": copyStats,
481
600
  "mjpeg": mjpegStats,
482
601
  "h264": h264Stats,
483
602
  "webrtc": webRTCStats,
@@ -497,6 +616,20 @@ final class CaptureEngine {
497
616
  statsLock.unlock()
498
617
  }
499
618
 
619
+ private func reserveScaledFramePreparation() -> Bool {
620
+ framePreparationLock.lock()
621
+ defer { framePreparationLock.unlock() }
622
+ if scaledFramePreparationPending { return false }
623
+ scaledFramePreparationPending = true
624
+ return true
625
+ }
626
+
627
+ private func finishScaledFramePreparation() {
628
+ framePreparationLock.lock()
629
+ scaledFramePreparationPending = false
630
+ framePreparationLock.unlock()
631
+ }
632
+
500
633
  private func recordCapturedFrame() {
501
634
  let nowNs = DispatchTime.now().uptimeNanoseconds
502
635
  withStatsLock {
@@ -524,6 +657,21 @@ final class CaptureEngine {
524
657
  }
525
658
  }
526
659
 
660
+ private func recordScaleBackpressureSkip() {
661
+ withStatsLock { statsScaleBackpressureSkips += 1 }
662
+ }
663
+
664
+ private func recordCopiedFrame(width: Int, height: Int, durationMs: Double) {
665
+ withStatsLock {
666
+ statsCopyCount += 1
667
+ statsCopyTotalMs += durationMs
668
+ statsCopyLastMs = durationMs
669
+ statsCopyMaxMs = max(statsCopyMaxMs, durationMs)
670
+ statsCopyWidth = width
671
+ statsCopyHeight = height
672
+ }
673
+ }
674
+
527
675
  private func recordMjpegReserved() {
528
676
  withStatsLock { statsMjpegReserved += 1 }
529
677
  }
@@ -552,6 +700,10 @@ final class CaptureEngine {
552
700
  withStatsLock { statsWebRTCThrottleSkips += 1 }
553
701
  }
554
702
 
703
+ private func recordWebRTCDirect() {
704
+ withStatsLock { statsWebRTCDirect += 1 }
705
+ }
706
+
555
707
  private func recordWebRTCCopyFailure() {
556
708
  withStatsLock { statsWebRTCCopyFailures += 1 }
557
709
  }
@@ -586,6 +738,19 @@ final class CaptureEngine {
586
738
  }
587
739
  }
588
740
 
741
+ func setMjpegActive(_ active: Bool) {
742
+ encodeQueue.async { [weak self] in
743
+ guard let self else { return }
744
+ if active != self.mjpegActive {
745
+ streamLog("[stream:mjpeg] active=\(active)")
746
+ }
747
+ self.mjpegActive = active
748
+ if active {
749
+ self.lastMjpegReservedAtNs = 0
750
+ }
751
+ }
752
+ }
753
+
589
754
  func requestKeyframe() {
590
755
  h264Queue.async { [weak self] in
591
756
  self?.forceKeyframe = true
@@ -156,6 +156,10 @@ private func u32(_ v: Int) -> UInt32 {
156
156
  engine.setAvccActive(active)
157
157
  }
158
158
 
159
+ @NodeMethod func setMjpegActive(_ active: Bool) {
160
+ engine.setMjpegActive(active)
161
+ }
162
+
159
163
  @NodeMethod func requestKeyframe() {
160
164
  engine.requestKeyframe()
161
165
  }
@@ -34,6 +34,8 @@ final class WebRTCPublisher {
34
34
  private var lastOutputWidth = 0
35
35
  private var lastOutputHeight = 0
36
36
  private var sentFrameCount: Int64 = 0
37
+ private var queuedInputFrameCount: Int64 = 0
38
+ private var directInputFrameCount: Int64 = 0
37
39
  private var lastFrameTimestampNs: Int64 = 0
38
40
  private let statsStartNs = DispatchTime.now().uptimeNanoseconds
39
41
  private var lastFrameSentAtNs: UInt64 = 0
@@ -101,46 +103,16 @@ final class WebRTCPublisher {
101
103
  func sendFrame(_ pixelBuffer: CVPixelBuffer, timestamp: CMTime) {
102
104
  queue.async {
103
105
  guard self.session != nil else { return }
104
- let width = CVPixelBufferGetWidth(pixelBuffer)
105
- let height = CVPixelBufferGetHeight(pixelBuffer)
106
- if width != self.lastOutputWidth || height != self.lastOutputHeight {
107
- self.lastOutputWidth = width
108
- self.lastOutputHeight = height
109
- self.videoSource.adaptOutputFormat(
110
- toWidth: Int32(width),
111
- height: Int32(height),
112
- fps: Int32(self.maxFps)
113
- )
114
- print("[webrtc] Video source output format: \(width)x\(height) @ \(self.maxFps)fps")
115
- }
116
- if !self.loggedInputFormat {
117
- self.loggedInputFormat = true
118
- let pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer)
119
- let supported = LKRTCCVPixelBuffer.supportedPixelFormats()
120
- .contains(NSNumber(value: UInt32(pixelFormat)))
121
- print("[webrtc] Input pixel format: \(pixelFormat) cvPixelBufferSupported=\(supported); forwarding as I420")
122
- }
123
- let timeNs = self.nextFrameTimestampNs(timestamp)
124
- let cvFrame = LKRTCVideoFrame(
125
- buffer: LKRTCCVPixelBuffer(pixelBuffer: pixelBuffer),
126
- rotation: ._0,
127
- timeStampNs: timeNs
128
- )
129
- let convertStartNs = DispatchTime.now().uptimeNanoseconds
130
- let frame = cvFrame.newI420()
131
- self.videoSource.capturer(self.capturer, didCapture: frame)
132
- let convertDurationMs = Double(DispatchTime.now().uptimeNanoseconds - convertStartNs) / 1_000_000.0
133
- self.sentFrameCount += 1
134
- self.lastFrameSentAtNs = DispatchTime.now().uptimeNanoseconds
135
- self.lastI420Ms = convertDurationMs
136
- self.totalI420Ms += convertDurationMs
137
- self.maxI420Ms = max(self.maxI420Ms, convertDurationMs)
138
- if self.shouldLogFrame(self.sentFrameCount) {
139
- print(
140
- "[webrtc] Sent video frame #\(self.sentFrameCount) size=\(width)x\(height) " +
141
- "timestampNs=\(timeNs) i420Ms=\(String(format: "%.2f", convertDurationMs))"
142
- )
143
- }
106
+ self.queuedInputFrameCount += 1
107
+ self.sendFrameOnQueue(pixelBuffer, timestamp: timestamp, mode: "queued")
108
+ }
109
+ }
110
+
111
+ func sendFrameDirect(_ pixelBuffer: CVPixelBuffer, timestamp: CMTime) {
112
+ queue.sync {
113
+ guard self.session != nil else { return }
114
+ self.directInputFrameCount += 1
115
+ self.sendFrameOnQueue(pixelBuffer, timestamp: timestamp, mode: "direct")
144
116
  }
145
117
  }
146
118
 
@@ -155,6 +127,8 @@ final class WebRTCPublisher {
155
127
  "outputWidth": lastOutputWidth,
156
128
  "outputHeight": lastOutputHeight,
157
129
  "sentFrames": sentFrameCount,
130
+ "queuedInputFrames": queuedInputFrameCount,
131
+ "directInputFrames": directInputFrameCount,
158
132
  "avgSentFps": Double(sentFrameCount) / uptimeSec,
159
133
  "lastFrameAgeMs": lastFrameAgeMs,
160
134
  "i420MsLast": lastI420Ms,
@@ -178,6 +152,49 @@ final class WebRTCPublisher {
178
152
  return timestampNs
179
153
  }
180
154
 
155
+ private func sendFrameOnQueue(_ pixelBuffer: CVPixelBuffer, timestamp: CMTime, mode: String) {
156
+ let width = CVPixelBufferGetWidth(pixelBuffer)
157
+ let height = CVPixelBufferGetHeight(pixelBuffer)
158
+ if width != lastOutputWidth || height != lastOutputHeight {
159
+ lastOutputWidth = width
160
+ lastOutputHeight = height
161
+ videoSource.adaptOutputFormat(
162
+ toWidth: Int32(width),
163
+ height: Int32(height),
164
+ fps: Int32(maxFps)
165
+ )
166
+ print("[webrtc] Video source output format: \(width)x\(height) @ \(maxFps)fps")
167
+ }
168
+ if !loggedInputFormat {
169
+ loggedInputFormat = true
170
+ let pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer)
171
+ let supported = LKRTCCVPixelBuffer.supportedPixelFormats()
172
+ .contains(NSNumber(value: UInt32(pixelFormat)))
173
+ print("[webrtc] Input pixel format: \(pixelFormat) cvPixelBufferSupported=\(supported); forwarding as I420")
174
+ }
175
+ let timeNs = nextFrameTimestampNs(timestamp)
176
+ let cvFrame = LKRTCVideoFrame(
177
+ buffer: LKRTCCVPixelBuffer(pixelBuffer: pixelBuffer),
178
+ rotation: ._0,
179
+ timeStampNs: timeNs
180
+ )
181
+ let convertStartNs = DispatchTime.now().uptimeNanoseconds
182
+ let frame = cvFrame.newI420()
183
+ videoSource.capturer(capturer, didCapture: frame)
184
+ let convertDurationMs = Double(DispatchTime.now().uptimeNanoseconds - convertStartNs) / 1_000_000.0
185
+ sentFrameCount += 1
186
+ lastFrameSentAtNs = DispatchTime.now().uptimeNanoseconds
187
+ lastI420Ms = convertDurationMs
188
+ totalI420Ms += convertDurationMs
189
+ maxI420Ms = max(maxI420Ms, convertDurationMs)
190
+ if shouldLogFrame(sentFrameCount) {
191
+ print(
192
+ "[webrtc] Sent video frame #\(sentFrameCount) mode=\(mode) size=\(width)x\(height) " +
193
+ "timestampNs=\(timeNs) i420Ms=\(String(format: "%.2f", convertDurationMs))"
194
+ )
195
+ }
196
+ }
197
+
181
198
  func stop() {
182
199
  queue.sync {
183
200
  session?.close()