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.
- package/Sources/SimNative/sim-capture.swift +177 -12
- package/Sources/SimNative/sim-module.swift +4 -0
- package/Sources/SimStreamHelper/WebRTCPublisher.swift +57 -40
- package/dist/middleware.js +35 -35
- package/dist/native/serve-sim-native.node +0 -0
- package/dist/serve-sim.js +71 -71
- package/package.json +1 -1
- package/src/native.ts +31 -15
- package/src/state.ts +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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()
|