serve-sim-sjchmiela 0.1.56 → 0.1.57

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.
@@ -42,6 +42,7 @@ final class WebRTCPublisher {
42
42
  private var lastFrameTimestampNs: Int64 = 0
43
43
  private let statsStartNs = DispatchTime.now().uptimeNanoseconds
44
44
  private var lastFrameSentAtNs: UInt64 = 0
45
+ private let usesCustomSoftwareH264Encoder: Bool
45
46
  private var totalI420Ms = 0.0
46
47
  private var lastI420Ms = 0.0
47
48
  private var maxI420Ms = 0.0
@@ -53,6 +54,7 @@ final class WebRTCPublisher {
53
54
  private var selectedCodecName = "H264"
54
55
  private let h264PixelBufferConverter = H264WebRTCPixelBufferConverter()
55
56
  private let h264WebRTCSupport: WebRTCH264Support
57
+ private let h264FrameModeOverride: H264WebRTCFrameMode?
56
58
  private var maxFps = 30
57
59
  private var targetBitrate = 6_000_000
58
60
  private var convertedFrameCount: Int64 = 0
@@ -66,7 +68,12 @@ final class WebRTCPublisher {
66
68
 
67
69
  init() {
68
70
  h264WebRTCSupport = Self.detectH264WebRTCSupport()
69
- let encoderFactory = LKRTCDefaultVideoEncoderFactory()
71
+ h264FrameModeOverride = Self.h264FrameModeOverride()
72
+ usesCustomSoftwareH264Encoder = h264WebRTCSupport.allowed && h264WebRTCSupport.usesHardware == false
73
+ let defaultEncoderFactory = LKRTCDefaultVideoEncoderFactory()
74
+ let encoderFactory: LKRTCVideoEncoderFactory = usesCustomSoftwareH264Encoder
75
+ ? ServeSimSoftwareH264EncoderFactory(fallback: defaultEncoderFactory)
76
+ : defaultEncoderFactory
70
77
  let decoderFactory = LKRTCDefaultVideoDecoderFactory()
71
78
  factory = LKRTCPeerConnectionFactory(
72
79
  encoderFactory: encoderFactory,
@@ -81,7 +88,7 @@ final class WebRTCPublisher {
81
88
  : "disabled(\(h264WebRTCSupport.reason ?? "unsupported runtime"))"
82
89
  print(
83
90
  "[webrtc] Publisher ready (default codec factory + screen-cast video source) " +
84
- "h264=\(h264Status) senderCodecs=\(senderCodecSummary())"
91
+ "h264=\(h264Status) h264FrameMode=\(h264FrameModeDescription()) senderCodecs=\(senderCodecSummary())"
85
92
  )
86
93
  }
87
94
 
@@ -147,6 +154,8 @@ final class WebRTCPublisher {
147
154
  "selectedCodec": selectedCodecName,
148
155
  "h264WebRTCEnabled": h264WebRTCSupport.allowed,
149
156
  "h264WebRTCProbe": h264WebRTCSupport.probeSummary,
157
+ "h264WebRTCCustomEncoder": usesCustomSoftwareH264Encoder,
158
+ "h264FrameMode": h264FrameModeDescription(),
150
159
  "frameMode": lastFrameMode,
151
160
  "outputWidth": lastOutputWidth,
152
161
  "outputHeight": lastOutputHeight,
@@ -231,26 +240,45 @@ final class WebRTCPublisher {
231
240
  var frameMode = usedNativeFrame ? "native" : "i420"
232
241
 
233
242
  if selectedCodecName == "H264" {
234
- if Self.isBiPlanar420(pixelFormat) {
235
- usedNativeFrame = true
236
- frameMode = "nv12-input"
237
- } else if let converted = h264PixelBufferConverter.convert(pixelBuffer) {
238
- convertDurationMs = h264PixelBufferConverter.lastDurationMs
239
- forwardedPixelFormat = CVPixelBufferGetPixelFormatType(converted)
240
- usedFrame = LKRTCVideoFrame(
241
- buffer: LKRTCCVPixelBuffer(pixelBuffer: converted),
242
- rotation: ._0,
243
- timeStampNs: timeNs
244
- )
245
- usedNativeFrame = true
246
- frameMode = "nv12"
247
- } else {
248
- conversionFailureCount += 1
243
+ switch h264FrameMode() {
244
+ case .bgra:
245
+ usedNativeFrame = useNativePixelBufferFrames ?? false
246
+ if usedNativeFrame {
247
+ frameMode = "bgra-h264"
248
+ } else {
249
+ let convertStartNs = DispatchTime.now().uptimeNanoseconds
250
+ usedFrame = sourceFrame.newI420()
251
+ convertDurationMs = Double(DispatchTime.now().uptimeNanoseconds - convertStartNs) / 1_000_000.0
252
+ frameMode = "i420-fallback"
253
+ }
254
+ case .i420:
249
255
  let convertStartNs = DispatchTime.now().uptimeNanoseconds
250
256
  usedFrame = sourceFrame.newI420()
251
257
  convertDurationMs = Double(DispatchTime.now().uptimeNanoseconds - convertStartNs) / 1_000_000.0
252
258
  usedNativeFrame = false
253
- frameMode = "i420-fallback"
259
+ frameMode = "i420-h264"
260
+ case .nv12:
261
+ if Self.isBiPlanar420(pixelFormat) {
262
+ usedNativeFrame = true
263
+ frameMode = "nv12-input"
264
+ } else if let converted = h264PixelBufferConverter.convert(pixelBuffer) {
265
+ convertDurationMs = h264PixelBufferConverter.lastDurationMs
266
+ forwardedPixelFormat = CVPixelBufferGetPixelFormatType(converted)
267
+ usedFrame = LKRTCVideoFrame(
268
+ buffer: LKRTCCVPixelBuffer(pixelBuffer: converted),
269
+ rotation: ._0,
270
+ timeStampNs: timeNs
271
+ )
272
+ usedNativeFrame = true
273
+ frameMode = "nv12"
274
+ } else {
275
+ conversionFailureCount += 1
276
+ let convertStartNs = DispatchTime.now().uptimeNanoseconds
277
+ usedFrame = sourceFrame.newI420()
278
+ convertDurationMs = Double(DispatchTime.now().uptimeNanoseconds - convertStartNs) / 1_000_000.0
279
+ usedNativeFrame = false
280
+ frameMode = "i420-fallback"
281
+ }
254
282
  }
255
283
  } else if !usedNativeFrame {
256
284
  let convertStartNs = DispatchTime.now().uptimeNanoseconds
@@ -692,6 +720,18 @@ final class WebRTCPublisher {
692
720
  pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
693
721
  }
694
722
 
723
+ private func h264FrameMode() -> H264WebRTCFrameMode {
724
+ if let h264FrameModeOverride {
725
+ return h264FrameModeOverride
726
+ }
727
+ return h264WebRTCSupport.usesHardware == false ? .bgra : .nv12
728
+ }
729
+
730
+ private func h264FrameModeDescription() -> String {
731
+ let source = h264FrameModeOverride == nil ? "auto" : "env"
732
+ return "\(h264FrameMode().rawValue)(\(source))"
733
+ }
734
+
695
735
  private static func detectH264WebRTCSupport() -> WebRTCH264Support {
696
736
  let environment = ProcessInfo.processInfo.environment
697
737
  if envFlagEnabled(environment["SERVE_SIM_DISABLE_WEBRTC_H264"]) {
@@ -869,6 +909,23 @@ final class WebRTCPublisher {
869
909
  return pixelBuffer
870
910
  }
871
911
 
912
+ private static func h264FrameModeOverride() -> H264WebRTCFrameMode? {
913
+ guard let raw = ProcessInfo.processInfo.environment["SERVE_SIM_WEBRTC_H264_FRAME_MODE"]?.lowercased() else {
914
+ return nil
915
+ }
916
+ switch raw {
917
+ case "bgra", "native-bgra":
918
+ return .bgra
919
+ case "i420":
920
+ return .i420
921
+ case "nv12", "native", "cvpixelbuffer":
922
+ return .nv12
923
+ default:
924
+ print("[webrtc] Ignoring invalid SERVE_SIM_WEBRTC_H264_FRAME_MODE=\(raw); expected bgra, i420, or nv12")
925
+ return nil
926
+ }
927
+ }
928
+
872
929
  private static func vtSessionStringProperty(_ session: VTCompressionSession, key: CFString) -> String? {
873
930
  var value: CFTypeRef?
874
931
  let status = withUnsafeMutablePointer(to: &value) { pointer in
@@ -936,6 +993,260 @@ private struct H264VideoToolboxProbe {
936
993
  let summary: String
937
994
  }
938
995
 
996
+ private enum H264WebRTCFrameMode: String {
997
+ case bgra
998
+ case i420
999
+ case nv12
1000
+ }
1001
+
1002
+ private final class ServeSimSoftwareH264EncoderFactory: NSObject, LKRTCVideoEncoderFactory {
1003
+ private let fallback: LKRTCVideoEncoderFactory
1004
+
1005
+ init(fallback: LKRTCVideoEncoderFactory) {
1006
+ self.fallback = fallback
1007
+ }
1008
+
1009
+ func createEncoder(_ info: LKRTCVideoCodecInfo) -> (any LKRTCVideoEncoder)? {
1010
+ if info.name.caseInsensitiveCompare("H264") == .orderedSame {
1011
+ return ServeSimSoftwareH264Encoder(codecInfo: info)
1012
+ }
1013
+ return fallback.createEncoder(info)
1014
+ }
1015
+
1016
+ func supportedCodecs() -> [LKRTCVideoCodecInfo] {
1017
+ fallback.supportedCodecs()
1018
+ }
1019
+
1020
+ func implementations() -> [LKRTCVideoCodecInfo] {
1021
+ fallback.implementations?() ?? supportedCodecs()
1022
+ }
1023
+
1024
+ func encoderSelector() -> (any LKRTCVideoEncoderSelector)? {
1025
+ fallback.encoderSelector?()
1026
+ }
1027
+
1028
+ func queryCodecSupport(
1029
+ _ info: LKRTCVideoCodecInfo,
1030
+ scalabilityMode: String?
1031
+ ) -> LKRTCVideoEncoderCodecSupport {
1032
+ if let queryCodecSupport = fallback.queryCodecSupport {
1033
+ return queryCodecSupport(info, scalabilityMode)
1034
+ }
1035
+ return LKRTCVideoEncoderCodecSupport(supported: true)
1036
+ }
1037
+ }
1038
+
1039
+ private final class ServeSimSoftwareH264Encoder: NSObject, LKRTCVideoEncoder {
1040
+ private let encoder = H264Encoder()
1041
+ private var callback: RTCVideoEncoderCallback?
1042
+ private var parameterSets = Data()
1043
+ private var width: Int32 = 0
1044
+ private var height: Int32 = 0
1045
+ private var fps: UInt32 = 30
1046
+ private var bitrateKbit: UInt32 = 3_000
1047
+ private var encodedCount: Int64 = 0
1048
+ private let stateQueue = DispatchQueue(label: "serve-sim.webrtc.h264.encoder")
1049
+
1050
+ init(codecInfo: LKRTCVideoCodecInfo) {
1051
+ super.init()
1052
+ streamLog("[webrtc:h264] using serve-sim software VideoToolbox encoder parameters=\(codecInfo.parameters)")
1053
+ encoder.onEncoded = { [weak self] encoded in
1054
+ self?.emit(encoded)
1055
+ }
1056
+ }
1057
+
1058
+ func setCallback(_ callback: RTCVideoEncoderCallback?) {
1059
+ stateQueue.sync {
1060
+ self.callback = callback
1061
+ }
1062
+ }
1063
+
1064
+ func startEncode(
1065
+ with settings: LKRTCVideoEncoderSettings,
1066
+ numberOfCores: Int32
1067
+ ) -> Int {
1068
+ stateQueue.sync {
1069
+ width = Int32(settings.width)
1070
+ height = Int32(settings.height)
1071
+ fps = max(1, settings.maxFramerate)
1072
+ bitrateKbit = max(1, settings.startBitrate)
1073
+ parameterSets.removeAll(keepingCapacity: true)
1074
+ encodedCount = 0
1075
+ }
1076
+ encoder.update(fps: Int(max(1, settings.maxFramerate)), bitrate: Int(max(1, settings.startBitrate)) * 1000)
1077
+ streamLog(
1078
+ "[webrtc:h264] start width=\(settings.width) height=\(settings.height) " +
1079
+ "fps=\(settings.maxFramerate) bitrateKbit=\(settings.startBitrate) cores=\(numberOfCores)"
1080
+ )
1081
+ return 0
1082
+ }
1083
+
1084
+ func release() -> Int {
1085
+ encoder.stop()
1086
+ stateQueue.sync {
1087
+ callback = nil
1088
+ parameterSets.removeAll(keepingCapacity: false)
1089
+ encodedCount = 0
1090
+ }
1091
+ streamLog("[webrtc:h264] release")
1092
+ return 0
1093
+ }
1094
+
1095
+ func encode(
1096
+ _ frame: LKRTCVideoFrame,
1097
+ codecSpecificInfo info: (any LKRTCCodecSpecificInfo)?,
1098
+ frameTypes: [NSNumber]
1099
+ ) -> Int {
1100
+ guard let pixelBuffer = pixelBuffer(from: frame) else {
1101
+ streamLog("[webrtc:h264] drop frame: unsupported frame buffer \(type(of: frame.buffer))")
1102
+ return -1
1103
+ }
1104
+ let forceKeyframe = frameTypes.contains { $0.intValue == LKRTCFrameType.videoFrameKey.rawValue }
1105
+ stateQueue.sync {
1106
+ width = Int32(frame.width)
1107
+ height = Int32(frame.height)
1108
+ }
1109
+ encoder.encode(pixelBuffer, forceKeyframe: forceKeyframe)
1110
+ return 0
1111
+ }
1112
+
1113
+ func setBitrate(_ bitrateKbit: UInt32, framerate: UInt32) -> Int32 {
1114
+ let normalizedBitrateKbit = max(1, bitrateKbit)
1115
+ let normalizedFps = max(1, framerate)
1116
+ let shouldUpdateEncoder = stateQueue.sync { () -> Bool in
1117
+ let previousBitrateKbit = self.bitrateKbit
1118
+ self.bitrateKbit = normalizedBitrateKbit
1119
+ fps = normalizedFps
1120
+ let delta = abs(Int64(normalizedBitrateKbit) - Int64(previousBitrateKbit))
1121
+ return delta * 100 >= Int64(max(1, previousBitrateKbit)) * 25
1122
+ }
1123
+ if shouldUpdateEncoder {
1124
+ encoder.update(fps: Int(normalizedFps), bitrate: Int(normalizedBitrateKbit) * 1000)
1125
+ streamLog("[webrtc:h264] bitrate bitrateKbit=\(normalizedBitrateKbit) fps=\(normalizedFps)")
1126
+ }
1127
+ return 0
1128
+ }
1129
+
1130
+ func implementationName() -> String {
1131
+ "serve-sim-videotoolbox-h264"
1132
+ }
1133
+
1134
+ func scalingSettings() -> LKRTCVideoEncoderQpThresholds? {
1135
+ nil
1136
+ }
1137
+
1138
+ var resolutionAlignment: Int {
1139
+ 2
1140
+ }
1141
+
1142
+ var applyAlignmentToAllSimulcastLayers: Bool {
1143
+ true
1144
+ }
1145
+
1146
+ var supportsNativeHandle: Bool {
1147
+ true
1148
+ }
1149
+
1150
+ private func pixelBuffer(from frame: LKRTCVideoFrame) -> CVPixelBuffer? {
1151
+ if let buffer = frame.buffer as? LKRTCCVPixelBuffer {
1152
+ return buffer.pixelBuffer
1153
+ }
1154
+ streamLog("[webrtc:h264] frame buffer is not CVPixelBuffer-backed: \(type(of: frame.buffer))")
1155
+ return nil
1156
+ }
1157
+
1158
+ private func emit(_ encoded: H264Encoder.Encoded) {
1159
+ let imageAndInfo: (LKRTCEncodedImage, LKRTCCodecSpecificInfoH264, Int64)? = stateQueue.sync {
1160
+ if let description = encoded.description {
1161
+ parameterSets = annexBParameterSets(fromAvcC: description)
1162
+ }
1163
+ var annexB = Data()
1164
+ if encoded.kind == .keyframe {
1165
+ annexB.append(parameterSets)
1166
+ }
1167
+ appendAnnexBNals(fromAvcc: encoded.avcc, to: &annexB)
1168
+ guard !annexB.isEmpty else { return nil }
1169
+
1170
+ encodedCount += 1
1171
+ let image = LKRTCEncodedImage()
1172
+ image.buffer = annexB
1173
+ image.encodedWidth = width
1174
+ image.encodedHeight = height
1175
+ image.timeStamp = UInt32(truncatingIfNeeded: encodedCount * 90_000 / Int64(max(1, fps)))
1176
+ image.captureTimeMs = Int64(DispatchTime.now().uptimeNanoseconds / 1_000_000)
1177
+ image.ntpTimeMs = image.captureTimeMs
1178
+ image.frameType = encoded.kind == .keyframe ? .videoFrameKey : .videoFrameDelta
1179
+ image.rotation = ._0
1180
+ image.contentType = .screenshare
1181
+
1182
+ let h264Info = LKRTCCodecSpecificInfoH264()
1183
+ h264Info.packetizationMode = .nonInterleaved
1184
+ return (image, h264Info, encodedCount)
1185
+ }
1186
+ guard let (image, h264Info, nextCount) = imageAndInfo else { return }
1187
+ let callback = stateQueue.sync { self.callback }
1188
+ guard let callback else { return }
1189
+ _ = callback(image, h264Info)
1190
+ if streamShouldLog(nextCount) || image.frameType == .videoFrameKey {
1191
+ let kind = image.frameType == .videoFrameKey ? "keyframe" : "delta"
1192
+ streamLog("[webrtc:h264] emitted \(kind) #\(nextCount) bytes=\(image.buffer.count)")
1193
+ }
1194
+ }
1195
+
1196
+ private func annexBParameterSets(fromAvcC avcC: Data) -> Data {
1197
+ guard avcC.count >= 7 else { return Data() }
1198
+ var offset = 5
1199
+ let spsCount = Int(avcC[offset] & 0x1f)
1200
+ offset += 1
1201
+ var out = Data()
1202
+ for _ in 0..<spsCount {
1203
+ guard offset + 2 <= avcC.count else { return out }
1204
+ let length = (Int(avcC[offset]) << 8) | Int(avcC[offset + 1])
1205
+ offset += 2
1206
+ guard offset + length <= avcC.count else { return out }
1207
+ appendAnnexBStartCode(to: &out)
1208
+ out.append(avcC.subdata(in: offset..<(offset + length)))
1209
+ offset += length
1210
+ }
1211
+ guard offset < avcC.count else { return out }
1212
+ let ppsCount = Int(avcC[offset])
1213
+ offset += 1
1214
+ for _ in 0..<ppsCount {
1215
+ guard offset + 2 <= avcC.count else { return out }
1216
+ let length = (Int(avcC[offset]) << 8) | Int(avcC[offset + 1])
1217
+ offset += 2
1218
+ guard offset + length <= avcC.count else { return out }
1219
+ appendAnnexBStartCode(to: &out)
1220
+ out.append(avcC.subdata(in: offset..<(offset + length)))
1221
+ offset += length
1222
+ }
1223
+ return out
1224
+ }
1225
+
1226
+ private func appendAnnexBNals(fromAvcc avcc: Data, to out: inout Data) {
1227
+ var offset = 0
1228
+ while offset + 4 <= avcc.count {
1229
+ let length =
1230
+ (Int(avcc[offset]) << 24) |
1231
+ (Int(avcc[offset + 1]) << 16) |
1232
+ (Int(avcc[offset + 2]) << 8) |
1233
+ Int(avcc[offset + 3])
1234
+ offset += 4
1235
+ guard length > 0, offset + length <= avcc.count else { return }
1236
+ appendAnnexBStartCode(to: &out)
1237
+ out.append(avcc.subdata(in: offset..<(offset + length)))
1238
+ offset += length
1239
+ }
1240
+ }
1241
+
1242
+ private func appendAnnexBStartCode(to out: inout Data) {
1243
+ out.append(0)
1244
+ out.append(0)
1245
+ out.append(0)
1246
+ out.append(1)
1247
+ }
1248
+ }
1249
+
939
1250
  private final class H264WebRTCPixelBufferConverter {
940
1251
  private var pool: CVPixelBufferPool?
941
1252
  private var width = 0