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 CHANGED
@@ -75,6 +75,7 @@ let package = Package(
75
75
  .linkedFramework("VideoToolbox"),
76
76
  .linkedFramework("ImageIO"),
77
77
  .linkedFramework("AppKit"),
78
+ .linkedFramework("Accelerate"),
78
79
  ]
79
80
  ),
80
81
  ],
@@ -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 loggedInputFormat = false
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
- if !loggedInputFormat {
169
- loggedInputFormat = true
170
- let pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer)
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
- print("[webrtc] Input pixel format: \(pixelFormat) cvPixelBufferSupported=\(supported); forwarding as I420")
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 cvFrame = LKRTCVideoFrame(
202
+ let sourceFrame = LKRTCVideoFrame(
177
203
  buffer: LKRTCCVPixelBuffer(pixelBuffer: pixelBuffer),
178
204
  rotation: ._0,
179
205
  timeStampNs: timeNs
180
206
  )
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
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
- lastI420Ms = convertDurationMs
188
- totalI420Ms += convertDurationMs
189
- maxI420Ms = max(maxI420Ms, convertDurationMs)
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) size=\(width)x\(height) " +
193
- "timestampNs=\(timeNs) i420Ms=\(String(format: "%.2f", convertDurationMs))"
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: String
502
- switch codec?.lowercased() {
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 {