serve-sim-sjchmiela 0.1.53 → 0.1.55
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/Package.swift +1 -0
- package/Sources/SimStreamHelper/WebRTCPublisher.swift +266 -24
- package/dist/middleware.js +36 -34
- package/dist/native/serve-sim-native.node +0 -0
- package/dist/serve-sim.js +65 -65
- package/package.json +1 -1
- package/src/middleware.ts +41 -2
package/Package.swift
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import CoreVideo
|
|
3
3
|
import CoreMedia
|
|
4
|
+
import Accelerate
|
|
4
5
|
import LiveKitWebRTC
|
|
5
6
|
|
|
6
7
|
struct WebRTCIceServerPayload: Codable {
|
|
@@ -42,9 +43,19 @@ final class WebRTCPublisher {
|
|
|
42
43
|
private var totalI420Ms = 0.0
|
|
43
44
|
private var lastI420Ms = 0.0
|
|
44
45
|
private var maxI420Ms = 0.0
|
|
45
|
-
private var
|
|
46
|
+
private var lastInputPixelFormat: OSType?
|
|
47
|
+
private var lastForwardedPixelFormat: OSType?
|
|
48
|
+
private var lastFrameMode = "none"
|
|
49
|
+
private var useNativePixelBufferFrames: Bool?
|
|
50
|
+
private var selectedCodecName = "H264"
|
|
51
|
+
private let h264PixelBufferConverter = H264WebRTCPixelBufferConverter()
|
|
46
52
|
private var maxFps = 30
|
|
47
53
|
private var targetBitrate = 6_000_000
|
|
54
|
+
private var convertedFrameCount: Int64 = 0
|
|
55
|
+
private var conversionFailureCount: Int64 = 0
|
|
56
|
+
private var totalConversionMs = 0.0
|
|
57
|
+
private var lastConversionMs = 0.0
|
|
58
|
+
private var maxConversionMs = 0.0
|
|
48
59
|
var isActive: Bool {
|
|
49
60
|
queue.sync { session != nil }
|
|
50
61
|
}
|
|
@@ -124,6 +135,8 @@ final class WebRTCPublisher {
|
|
|
124
135
|
"active": session != nil,
|
|
125
136
|
"targetFps": maxFps,
|
|
126
137
|
"targetBitrate": targetBitrate,
|
|
138
|
+
"selectedCodec": selectedCodecName,
|
|
139
|
+
"frameMode": lastFrameMode,
|
|
127
140
|
"outputWidth": lastOutputWidth,
|
|
128
141
|
"outputHeight": lastOutputHeight,
|
|
129
142
|
"sentFrames": sentFrameCount,
|
|
@@ -131,10 +144,21 @@ final class WebRTCPublisher {
|
|
|
131
144
|
"directInputFrames": directInputFrameCount,
|
|
132
145
|
"avgSentFps": Double(sentFrameCount) / uptimeSec,
|
|
133
146
|
"lastFrameAgeMs": lastFrameAgeMs,
|
|
147
|
+
"convertedFrames": convertedFrameCount,
|
|
148
|
+
"conversionFailures": conversionFailureCount,
|
|
149
|
+
"conversionMsLast": lastConversionMs,
|
|
150
|
+
"conversionMsAvg": convertedFrameCount > 0 ? totalConversionMs / Double(convertedFrameCount) : 0.0,
|
|
151
|
+
"conversionMsMax": maxConversionMs,
|
|
134
152
|
"i420MsLast": lastI420Ms,
|
|
135
153
|
"i420MsAvg": sentFrameCount > 0 ? totalI420Ms / Double(sentFrameCount) : 0.0,
|
|
136
154
|
"i420MsMax": maxI420Ms,
|
|
137
155
|
]
|
|
156
|
+
if let lastInputPixelFormat {
|
|
157
|
+
stats["inputPixelFormat"] = pixelFormatDescription(lastInputPixelFormat)
|
|
158
|
+
}
|
|
159
|
+
if let lastForwardedPixelFormat {
|
|
160
|
+
stats["forwardedPixelFormat"] = pixelFormatDescription(lastForwardedPixelFormat)
|
|
161
|
+
}
|
|
138
162
|
if let session {
|
|
139
163
|
stats["outboundRtp"] = session.delegate.outboundStatsSnapshot()
|
|
140
164
|
}
|
|
@@ -165,32 +189,77 @@ final class WebRTCPublisher {
|
|
|
165
189
|
)
|
|
166
190
|
print("[webrtc] Video source output format: \(width)x\(height) @ \(maxFps)fps")
|
|
167
191
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
192
|
+
let pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer)
|
|
193
|
+
if lastInputPixelFormat != pixelFormat {
|
|
194
|
+
lastInputPixelFormat = pixelFormat
|
|
171
195
|
let supported = LKRTCCVPixelBuffer.supportedPixelFormats()
|
|
172
196
|
.contains(NSNumber(value: UInt32(pixelFormat)))
|
|
173
|
-
|
|
197
|
+
useNativePixelBufferFrames = supported
|
|
198
|
+
let frameMode = supported ? "native CVPixelBuffer" : "I420 fallback"
|
|
199
|
+
print("[webrtc] Input pixel format: \(pixelFormat) cvPixelBufferSupported=\(supported); forwarding as \(frameMode)")
|
|
174
200
|
}
|
|
175
201
|
let timeNs = nextFrameTimestampNs(timestamp)
|
|
176
|
-
let
|
|
202
|
+
let sourceFrame = LKRTCVideoFrame(
|
|
177
203
|
buffer: LKRTCCVPixelBuffer(pixelBuffer: pixelBuffer),
|
|
178
204
|
rotation: ._0,
|
|
179
205
|
timeStampNs: timeNs
|
|
180
206
|
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
207
|
+
var convertDurationMs = 0.0
|
|
208
|
+
var usedFrame = sourceFrame
|
|
209
|
+
var usedNativeFrame = useNativePixelBufferFrames ?? false
|
|
210
|
+
var forwardedPixelFormat = pixelFormat
|
|
211
|
+
var frameMode = usedNativeFrame ? "native" : "i420"
|
|
212
|
+
|
|
213
|
+
if selectedCodecName == "H264" {
|
|
214
|
+
if Self.isBiPlanar420(pixelFormat) {
|
|
215
|
+
usedNativeFrame = true
|
|
216
|
+
frameMode = "nv12-input"
|
|
217
|
+
} else if let converted = h264PixelBufferConverter.convert(pixelBuffer) {
|
|
218
|
+
convertDurationMs = h264PixelBufferConverter.lastDurationMs
|
|
219
|
+
forwardedPixelFormat = CVPixelBufferGetPixelFormatType(converted)
|
|
220
|
+
usedFrame = LKRTCVideoFrame(
|
|
221
|
+
buffer: LKRTCCVPixelBuffer(pixelBuffer: converted),
|
|
222
|
+
rotation: ._0,
|
|
223
|
+
timeStampNs: timeNs
|
|
224
|
+
)
|
|
225
|
+
usedNativeFrame = true
|
|
226
|
+
frameMode = "nv12"
|
|
227
|
+
} else {
|
|
228
|
+
conversionFailureCount += 1
|
|
229
|
+
let convertStartNs = DispatchTime.now().uptimeNanoseconds
|
|
230
|
+
usedFrame = sourceFrame.newI420()
|
|
231
|
+
convertDurationMs = Double(DispatchTime.now().uptimeNanoseconds - convertStartNs) / 1_000_000.0
|
|
232
|
+
usedNativeFrame = false
|
|
233
|
+
frameMode = "i420-fallback"
|
|
234
|
+
}
|
|
235
|
+
} else if !usedNativeFrame {
|
|
236
|
+
let convertStartNs = DispatchTime.now().uptimeNanoseconds
|
|
237
|
+
usedFrame = sourceFrame.newI420()
|
|
238
|
+
convertDurationMs = Double(DispatchTime.now().uptimeNanoseconds - convertStartNs) / 1_000_000.0
|
|
239
|
+
frameMode = "i420-fallback"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
videoSource.capturer(capturer, didCapture: usedFrame)
|
|
243
|
+
if convertDurationMs > 0 {
|
|
244
|
+
convertedFrameCount += 1
|
|
245
|
+
totalConversionMs += convertDurationMs
|
|
246
|
+
maxConversionMs = max(maxConversionMs, convertDurationMs)
|
|
247
|
+
}
|
|
185
248
|
sentFrameCount += 1
|
|
186
249
|
lastFrameSentAtNs = DispatchTime.now().uptimeNanoseconds
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
250
|
+
lastForwardedPixelFormat = forwardedPixelFormat
|
|
251
|
+
lastFrameMode = frameMode
|
|
252
|
+
lastConversionMs = convertDurationMs
|
|
253
|
+
lastI420Ms = frameMode == "i420-fallback" ? convertDurationMs : 0.0
|
|
254
|
+
totalI420Ms += lastI420Ms
|
|
255
|
+
maxI420Ms = max(maxI420Ms, lastI420Ms)
|
|
190
256
|
if shouldLogFrame(sentFrameCount) {
|
|
191
257
|
print(
|
|
192
|
-
"[webrtc] Sent video frame #\(sentFrameCount) mode=\(mode)
|
|
193
|
-
"timestampNs=\(timeNs)
|
|
258
|
+
"[webrtc] Sent video frame #\(sentFrameCount) mode=\(mode) codec=\(selectedCodecName) " +
|
|
259
|
+
"size=\(width)x\(height) timestampNs=\(timeNs) frameMode=\(frameMode) " +
|
|
260
|
+
"inputFormat=\(pixelFormatDescription(pixelFormat)) " +
|
|
261
|
+
"forwardedFormat=\(pixelFormatDescription(forwardedPixelFormat)) " +
|
|
262
|
+
"native=\(usedNativeFrame) conversionMs=\(String(format: "%.2f", convertDurationMs))"
|
|
194
263
|
)
|
|
195
264
|
}
|
|
196
265
|
}
|
|
@@ -498,15 +567,8 @@ final class WebRTCPublisher {
|
|
|
498
567
|
}
|
|
499
568
|
|
|
500
569
|
private func applyVideoCodecPreference(_ codec: String?, to transceiver: LKRTCRtpTransceiver) {
|
|
501
|
-
let preferredName
|
|
502
|
-
|
|
503
|
-
case "vp8":
|
|
504
|
-
preferredName = "VP8"
|
|
505
|
-
case "vp9":
|
|
506
|
-
preferredName = "VP9"
|
|
507
|
-
default:
|
|
508
|
-
preferredName = "H264"
|
|
509
|
-
}
|
|
570
|
+
let preferredName = Self.preferredVideoCodecName(codec)
|
|
571
|
+
selectedCodecName = preferredName
|
|
510
572
|
let capabilities = factory.rtpSenderCapabilities(forKind: "video")
|
|
511
573
|
let preferredCodecs = capabilities.codecs.filter {
|
|
512
574
|
$0.name.caseInsensitiveCompare(preferredName) == .orderedSame ||
|
|
@@ -576,6 +638,186 @@ final class WebRTCPublisher {
|
|
|
576
638
|
private func shouldLogFrame(_ count: Int64) -> Bool {
|
|
577
639
|
count <= 5 || count % 120 == 0
|
|
578
640
|
}
|
|
641
|
+
|
|
642
|
+
private static func preferredVideoCodecName(_ codec: String?) -> String {
|
|
643
|
+
switch codec?.lowercased() {
|
|
644
|
+
case "vp8":
|
|
645
|
+
return "VP8"
|
|
646
|
+
case "vp9":
|
|
647
|
+
return "VP9"
|
|
648
|
+
default:
|
|
649
|
+
return "H264"
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private static func isBiPlanar420(_ pixelFormat: OSType) -> Bool {
|
|
654
|
+
pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange ||
|
|
655
|
+
pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private final class H264WebRTCPixelBufferConverter {
|
|
660
|
+
private var pool: CVPixelBufferPool?
|
|
661
|
+
private var width = 0
|
|
662
|
+
private var height = 0
|
|
663
|
+
private var conversionInfo = vImage_ARGBToYpCbCr()
|
|
664
|
+
private var conversionReady = false
|
|
665
|
+
private(set) var lastDurationMs = 0.0
|
|
666
|
+
|
|
667
|
+
init() {
|
|
668
|
+
var pixelRange = vImage_YpCbCrPixelRange(
|
|
669
|
+
Yp_bias: 0,
|
|
670
|
+
CbCr_bias: 128,
|
|
671
|
+
YpRangeMax: 255,
|
|
672
|
+
CbCrRangeMax: 255,
|
|
673
|
+
YpMax: 255,
|
|
674
|
+
YpMin: 1,
|
|
675
|
+
CbCrMax: 255,
|
|
676
|
+
CbCrMin: 0
|
|
677
|
+
)
|
|
678
|
+
let status = vImageConvert_ARGBToYpCbCr_GenerateConversion(
|
|
679
|
+
kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2,
|
|
680
|
+
&pixelRange,
|
|
681
|
+
&conversionInfo,
|
|
682
|
+
kvImageARGB8888,
|
|
683
|
+
kvImage420Yp8_CbCr8,
|
|
684
|
+
vImage_Flags(kvImageNoFlags)
|
|
685
|
+
)
|
|
686
|
+
conversionReady = status == kvImageNoError
|
|
687
|
+
if !conversionReady {
|
|
688
|
+
streamLog("[webrtc] H.264 NV12 conversion setup failed status=\(status)")
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
func convert(_ source: CVPixelBuffer) -> CVPixelBuffer? {
|
|
693
|
+
guard conversionReady else { return nil }
|
|
694
|
+
let sourceFormat = CVPixelBufferGetPixelFormatType(source)
|
|
695
|
+
guard sourceFormat == kCVPixelFormatType_32BGRA else {
|
|
696
|
+
streamLog("[webrtc] H.264 NV12 conversion unsupported input format=\(pixelFormatDescription(sourceFormat))")
|
|
697
|
+
return nil
|
|
698
|
+
}
|
|
699
|
+
let sourceWidth = CVPixelBufferGetWidth(source)
|
|
700
|
+
let sourceHeight = CVPixelBufferGetHeight(source)
|
|
701
|
+
guard sourceWidth > 1, sourceHeight > 1 else { return nil }
|
|
702
|
+
guard let output = makePixelBuffer(width: sourceWidth, height: sourceHeight) else { return nil }
|
|
703
|
+
|
|
704
|
+
CVPixelBufferLockBaseAddress(source, .readOnly)
|
|
705
|
+
CVPixelBufferLockBaseAddress(output, [])
|
|
706
|
+
defer {
|
|
707
|
+
CVPixelBufferUnlockBaseAddress(output, [])
|
|
708
|
+
CVPixelBufferUnlockBaseAddress(source, .readOnly)
|
|
709
|
+
}
|
|
710
|
+
guard
|
|
711
|
+
let sourceAddress = CVPixelBufferGetBaseAddress(source),
|
|
712
|
+
CVPixelBufferGetPlaneCount(output) >= 2,
|
|
713
|
+
let yAddress = CVPixelBufferGetBaseAddressOfPlane(output, 0),
|
|
714
|
+
let cbCrAddress = CVPixelBufferGetBaseAddressOfPlane(output, 1)
|
|
715
|
+
else {
|
|
716
|
+
return nil
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
var sourceBuffer = vImage_Buffer(
|
|
720
|
+
data: sourceAddress,
|
|
721
|
+
height: vImagePixelCount(sourceHeight),
|
|
722
|
+
width: vImagePixelCount(sourceWidth),
|
|
723
|
+
rowBytes: CVPixelBufferGetBytesPerRow(source)
|
|
724
|
+
)
|
|
725
|
+
var yBuffer = vImage_Buffer(
|
|
726
|
+
data: yAddress,
|
|
727
|
+
height: vImagePixelCount(CVPixelBufferGetHeightOfPlane(output, 0)),
|
|
728
|
+
width: vImagePixelCount(CVPixelBufferGetWidthOfPlane(output, 0)),
|
|
729
|
+
rowBytes: CVPixelBufferGetBytesPerRowOfPlane(output, 0)
|
|
730
|
+
)
|
|
731
|
+
var cbCrBuffer = vImage_Buffer(
|
|
732
|
+
data: cbCrAddress,
|
|
733
|
+
height: vImagePixelCount(CVPixelBufferGetHeightOfPlane(output, 1)),
|
|
734
|
+
width: vImagePixelCount(CVPixelBufferGetWidthOfPlane(output, 1)),
|
|
735
|
+
rowBytes: CVPixelBufferGetBytesPerRowOfPlane(output, 1)
|
|
736
|
+
)
|
|
737
|
+
var bgraPermuteMap: [UInt8] = [3, 2, 1, 0]
|
|
738
|
+
let startNs = DispatchTime.now().uptimeNanoseconds
|
|
739
|
+
let status = vImageConvert_ARGB8888To420Yp8_CbCr8(
|
|
740
|
+
&sourceBuffer,
|
|
741
|
+
&yBuffer,
|
|
742
|
+
&cbCrBuffer,
|
|
743
|
+
&conversionInfo,
|
|
744
|
+
&bgraPermuteMap,
|
|
745
|
+
vImage_Flags(kvImageNoFlags)
|
|
746
|
+
)
|
|
747
|
+
lastDurationMs = Double(DispatchTime.now().uptimeNanoseconds - startNs) / 1_000_000.0
|
|
748
|
+
guard status == kvImageNoError else {
|
|
749
|
+
streamLog("[webrtc] H.264 NV12 conversion failed status=\(status)")
|
|
750
|
+
return nil
|
|
751
|
+
}
|
|
752
|
+
attachColorMetadata(to: output)
|
|
753
|
+
return output
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
private func makePixelBuffer(width nextWidth: Int, height nextHeight: Int) -> CVPixelBuffer? {
|
|
757
|
+
if pool == nil || width != nextWidth || height != nextHeight {
|
|
758
|
+
width = nextWidth
|
|
759
|
+
height = nextHeight
|
|
760
|
+
let attrs: [String: Any] = [
|
|
761
|
+
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
|
|
762
|
+
kCVPixelBufferWidthKey as String: nextWidth,
|
|
763
|
+
kCVPixelBufferHeightKey as String: nextHeight,
|
|
764
|
+
kCVPixelBufferIOSurfacePropertiesKey as String: [:],
|
|
765
|
+
kCVPixelBufferMetalCompatibilityKey as String: true,
|
|
766
|
+
]
|
|
767
|
+
var newPool: CVPixelBufferPool?
|
|
768
|
+
let status = CVPixelBufferPoolCreate(kCFAllocatorDefault, nil, attrs as CFDictionary, &newPool)
|
|
769
|
+
guard status == kCVReturnSuccess, let newPool else {
|
|
770
|
+
streamLog("[webrtc] H.264 NV12 pixel buffer pool create failed status=\(status) size=\(nextWidth)x\(nextHeight)")
|
|
771
|
+
pool = nil
|
|
772
|
+
return nil
|
|
773
|
+
}
|
|
774
|
+
pool = newPool
|
|
775
|
+
streamLog("[webrtc] H.264 NV12 pixel buffer pool ready size=\(nextWidth)x\(nextHeight)")
|
|
776
|
+
}
|
|
777
|
+
guard let pool else { return nil }
|
|
778
|
+
var output: CVPixelBuffer?
|
|
779
|
+
let status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &output)
|
|
780
|
+
guard status == kCVReturnSuccess, let output else {
|
|
781
|
+
streamLog("[webrtc] H.264 NV12 pixel buffer allocation failed status=\(status)")
|
|
782
|
+
return nil
|
|
783
|
+
}
|
|
784
|
+
return output
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private func attachColorMetadata(to pixelBuffer: CVPixelBuffer) {
|
|
788
|
+
CVBufferSetAttachment(
|
|
789
|
+
pixelBuffer,
|
|
790
|
+
kCVImageBufferYCbCrMatrixKey,
|
|
791
|
+
kCVImageBufferYCbCrMatrix_ITU_R_709_2,
|
|
792
|
+
.shouldPropagate
|
|
793
|
+
)
|
|
794
|
+
CVBufferSetAttachment(
|
|
795
|
+
pixelBuffer,
|
|
796
|
+
kCVImageBufferColorPrimariesKey,
|
|
797
|
+
kCVImageBufferColorPrimaries_ITU_R_709_2,
|
|
798
|
+
.shouldPropagate
|
|
799
|
+
)
|
|
800
|
+
CVBufferSetAttachment(
|
|
801
|
+
pixelBuffer,
|
|
802
|
+
kCVImageBufferTransferFunctionKey,
|
|
803
|
+
kCVImageBufferTransferFunction_sRGB,
|
|
804
|
+
.shouldPropagate
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
private func pixelFormatDescription(_ pixelFormat: OSType) -> String {
|
|
810
|
+
var value = pixelFormat.bigEndian
|
|
811
|
+
let text = withUnsafeBytes(of: &value) { rawBuffer -> String in
|
|
812
|
+
let bytes = rawBuffer.map { byte -> UInt8 in
|
|
813
|
+
if byte >= 32 && byte <= 126 {
|
|
814
|
+
return byte
|
|
815
|
+
}
|
|
816
|
+
return UInt8(ascii: ".")
|
|
817
|
+
}
|
|
818
|
+
return String(bytes: bytes, encoding: .ascii) ?? "\(pixelFormat)"
|
|
819
|
+
}
|
|
820
|
+
return "\(text)(\(pixelFormat))"
|
|
579
821
|
}
|
|
580
822
|
|
|
581
823
|
private final class WebRTCSession {
|