serve-sim-sjchmiela 0.1.56 → 0.1.58

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
@@ -303,13 +331,8 @@ final class WebRTCPublisher {
303
331
  config.candidateNetworkPolicy = .all
304
332
  config.continualGatheringPolicy = .gatherOnce
305
333
  config.iceServers = iceServers(from: request.iceServers)
306
- if hasCredentialedTurnServer(request.iceServers) {
307
- config.iceTransportPolicy = .relay
308
- print("[webrtc] ICE transport policy: relay")
309
- } else {
310
- config.iceTransportPolicy = .all
311
- print("[webrtc] ICE transport policy: all")
312
- }
334
+ config.iceTransportPolicy = .all
335
+ print("[webrtc] ICE transport policy: all (TURN as fallback)")
313
336
  print("[webrtc] ICE servers: \(iceServerSummary(request.iceServers))")
314
337
 
315
338
  let constraints = LKRTCMediaConstraints(
@@ -692,6 +715,18 @@ final class WebRTCPublisher {
692
715
  pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
693
716
  }
694
717
 
718
+ private func h264FrameMode() -> H264WebRTCFrameMode {
719
+ if let h264FrameModeOverride {
720
+ return h264FrameModeOverride
721
+ }
722
+ return h264WebRTCSupport.usesHardware == false ? .bgra : .nv12
723
+ }
724
+
725
+ private func h264FrameModeDescription() -> String {
726
+ let source = h264FrameModeOverride == nil ? "auto" : "env"
727
+ return "\(h264FrameMode().rawValue)(\(source))"
728
+ }
729
+
695
730
  private static func detectH264WebRTCSupport() -> WebRTCH264Support {
696
731
  let environment = ProcessInfo.processInfo.environment
697
732
  if envFlagEnabled(environment["SERVE_SIM_DISABLE_WEBRTC_H264"]) {
@@ -869,6 +904,23 @@ final class WebRTCPublisher {
869
904
  return pixelBuffer
870
905
  }
871
906
 
907
+ private static func h264FrameModeOverride() -> H264WebRTCFrameMode? {
908
+ guard let raw = ProcessInfo.processInfo.environment["SERVE_SIM_WEBRTC_H264_FRAME_MODE"]?.lowercased() else {
909
+ return nil
910
+ }
911
+ switch raw {
912
+ case "bgra", "native-bgra":
913
+ return .bgra
914
+ case "i420":
915
+ return .i420
916
+ case "nv12", "native", "cvpixelbuffer":
917
+ return .nv12
918
+ default:
919
+ print("[webrtc] Ignoring invalid SERVE_SIM_WEBRTC_H264_FRAME_MODE=\(raw); expected bgra, i420, or nv12")
920
+ return nil
921
+ }
922
+ }
923
+
872
924
  private static func vtSessionStringProperty(_ session: VTCompressionSession, key: CFString) -> String? {
873
925
  var value: CFTypeRef?
874
926
  let status = withUnsafeMutablePointer(to: &value) { pointer in
@@ -936,6 +988,260 @@ private struct H264VideoToolboxProbe {
936
988
  let summary: String
937
989
  }
938
990
 
991
+ private enum H264WebRTCFrameMode: String {
992
+ case bgra
993
+ case i420
994
+ case nv12
995
+ }
996
+
997
+ private final class ServeSimSoftwareH264EncoderFactory: NSObject, LKRTCVideoEncoderFactory {
998
+ private let fallback: LKRTCVideoEncoderFactory
999
+
1000
+ init(fallback: LKRTCVideoEncoderFactory) {
1001
+ self.fallback = fallback
1002
+ }
1003
+
1004
+ func createEncoder(_ info: LKRTCVideoCodecInfo) -> (any LKRTCVideoEncoder)? {
1005
+ if info.name.caseInsensitiveCompare("H264") == .orderedSame {
1006
+ return ServeSimSoftwareH264Encoder(codecInfo: info)
1007
+ }
1008
+ return fallback.createEncoder(info)
1009
+ }
1010
+
1011
+ func supportedCodecs() -> [LKRTCVideoCodecInfo] {
1012
+ fallback.supportedCodecs()
1013
+ }
1014
+
1015
+ func implementations() -> [LKRTCVideoCodecInfo] {
1016
+ fallback.implementations?() ?? supportedCodecs()
1017
+ }
1018
+
1019
+ func encoderSelector() -> (any LKRTCVideoEncoderSelector)? {
1020
+ fallback.encoderSelector?()
1021
+ }
1022
+
1023
+ func queryCodecSupport(
1024
+ _ info: LKRTCVideoCodecInfo,
1025
+ scalabilityMode: String?
1026
+ ) -> LKRTCVideoEncoderCodecSupport {
1027
+ if let queryCodecSupport = fallback.queryCodecSupport {
1028
+ return queryCodecSupport(info, scalabilityMode)
1029
+ }
1030
+ return LKRTCVideoEncoderCodecSupport(supported: true)
1031
+ }
1032
+ }
1033
+
1034
+ private final class ServeSimSoftwareH264Encoder: NSObject, LKRTCVideoEncoder {
1035
+ private let encoder = H264Encoder()
1036
+ private var callback: RTCVideoEncoderCallback?
1037
+ private var parameterSets = Data()
1038
+ private var width: Int32 = 0
1039
+ private var height: Int32 = 0
1040
+ private var fps: UInt32 = 30
1041
+ private var bitrateKbit: UInt32 = 3_000
1042
+ private var encodedCount: Int64 = 0
1043
+ private let stateQueue = DispatchQueue(label: "serve-sim.webrtc.h264.encoder")
1044
+
1045
+ init(codecInfo: LKRTCVideoCodecInfo) {
1046
+ super.init()
1047
+ streamLog("[webrtc:h264] using serve-sim software VideoToolbox encoder parameters=\(codecInfo.parameters)")
1048
+ encoder.onEncoded = { [weak self] encoded in
1049
+ self?.emit(encoded)
1050
+ }
1051
+ }
1052
+
1053
+ func setCallback(_ callback: RTCVideoEncoderCallback?) {
1054
+ stateQueue.sync {
1055
+ self.callback = callback
1056
+ }
1057
+ }
1058
+
1059
+ func startEncode(
1060
+ with settings: LKRTCVideoEncoderSettings,
1061
+ numberOfCores: Int32
1062
+ ) -> Int {
1063
+ stateQueue.sync {
1064
+ width = Int32(settings.width)
1065
+ height = Int32(settings.height)
1066
+ fps = max(1, settings.maxFramerate)
1067
+ bitrateKbit = max(1, settings.startBitrate)
1068
+ parameterSets.removeAll(keepingCapacity: true)
1069
+ encodedCount = 0
1070
+ }
1071
+ encoder.update(fps: Int(max(1, settings.maxFramerate)), bitrate: Int(max(1, settings.startBitrate)) * 1000)
1072
+ streamLog(
1073
+ "[webrtc:h264] start width=\(settings.width) height=\(settings.height) " +
1074
+ "fps=\(settings.maxFramerate) bitrateKbit=\(settings.startBitrate) cores=\(numberOfCores)"
1075
+ )
1076
+ return 0
1077
+ }
1078
+
1079
+ func release() -> Int {
1080
+ encoder.stop()
1081
+ stateQueue.sync {
1082
+ callback = nil
1083
+ parameterSets.removeAll(keepingCapacity: false)
1084
+ encodedCount = 0
1085
+ }
1086
+ streamLog("[webrtc:h264] release")
1087
+ return 0
1088
+ }
1089
+
1090
+ func encode(
1091
+ _ frame: LKRTCVideoFrame,
1092
+ codecSpecificInfo info: (any LKRTCCodecSpecificInfo)?,
1093
+ frameTypes: [NSNumber]
1094
+ ) -> Int {
1095
+ guard let pixelBuffer = pixelBuffer(from: frame) else {
1096
+ streamLog("[webrtc:h264] drop frame: unsupported frame buffer \(type(of: frame.buffer))")
1097
+ return -1
1098
+ }
1099
+ let forceKeyframe = frameTypes.contains { $0.intValue == LKRTCFrameType.videoFrameKey.rawValue }
1100
+ stateQueue.sync {
1101
+ width = Int32(frame.width)
1102
+ height = Int32(frame.height)
1103
+ }
1104
+ encoder.encode(pixelBuffer, forceKeyframe: forceKeyframe)
1105
+ return 0
1106
+ }
1107
+
1108
+ func setBitrate(_ bitrateKbit: UInt32, framerate: UInt32) -> Int32 {
1109
+ let normalizedBitrateKbit = max(1, bitrateKbit)
1110
+ let normalizedFps = max(1, framerate)
1111
+ let shouldUpdateEncoder = stateQueue.sync { () -> Bool in
1112
+ let previousBitrateKbit = self.bitrateKbit
1113
+ self.bitrateKbit = normalizedBitrateKbit
1114
+ fps = normalizedFps
1115
+ let delta = abs(Int64(normalizedBitrateKbit) - Int64(previousBitrateKbit))
1116
+ return delta * 100 >= Int64(max(1, previousBitrateKbit)) * 25
1117
+ }
1118
+ if shouldUpdateEncoder {
1119
+ encoder.update(fps: Int(normalizedFps), bitrate: Int(normalizedBitrateKbit) * 1000)
1120
+ streamLog("[webrtc:h264] bitrate bitrateKbit=\(normalizedBitrateKbit) fps=\(normalizedFps)")
1121
+ }
1122
+ return 0
1123
+ }
1124
+
1125
+ func implementationName() -> String {
1126
+ "serve-sim-videotoolbox-h264"
1127
+ }
1128
+
1129
+ func scalingSettings() -> LKRTCVideoEncoderQpThresholds? {
1130
+ nil
1131
+ }
1132
+
1133
+ var resolutionAlignment: Int {
1134
+ 2
1135
+ }
1136
+
1137
+ var applyAlignmentToAllSimulcastLayers: Bool {
1138
+ true
1139
+ }
1140
+
1141
+ var supportsNativeHandle: Bool {
1142
+ true
1143
+ }
1144
+
1145
+ private func pixelBuffer(from frame: LKRTCVideoFrame) -> CVPixelBuffer? {
1146
+ if let buffer = frame.buffer as? LKRTCCVPixelBuffer {
1147
+ return buffer.pixelBuffer
1148
+ }
1149
+ streamLog("[webrtc:h264] frame buffer is not CVPixelBuffer-backed: \(type(of: frame.buffer))")
1150
+ return nil
1151
+ }
1152
+
1153
+ private func emit(_ encoded: H264Encoder.Encoded) {
1154
+ let imageAndInfo: (LKRTCEncodedImage, LKRTCCodecSpecificInfoH264, Int64)? = stateQueue.sync {
1155
+ if let description = encoded.description {
1156
+ parameterSets = annexBParameterSets(fromAvcC: description)
1157
+ }
1158
+ var annexB = Data()
1159
+ if encoded.kind == .keyframe {
1160
+ annexB.append(parameterSets)
1161
+ }
1162
+ appendAnnexBNals(fromAvcc: encoded.avcc, to: &annexB)
1163
+ guard !annexB.isEmpty else { return nil }
1164
+
1165
+ encodedCount += 1
1166
+ let image = LKRTCEncodedImage()
1167
+ image.buffer = annexB
1168
+ image.encodedWidth = width
1169
+ image.encodedHeight = height
1170
+ image.timeStamp = UInt32(truncatingIfNeeded: encodedCount * 90_000 / Int64(max(1, fps)))
1171
+ image.captureTimeMs = Int64(DispatchTime.now().uptimeNanoseconds / 1_000_000)
1172
+ image.ntpTimeMs = image.captureTimeMs
1173
+ image.frameType = encoded.kind == .keyframe ? .videoFrameKey : .videoFrameDelta
1174
+ image.rotation = ._0
1175
+ image.contentType = .screenshare
1176
+
1177
+ let h264Info = LKRTCCodecSpecificInfoH264()
1178
+ h264Info.packetizationMode = .nonInterleaved
1179
+ return (image, h264Info, encodedCount)
1180
+ }
1181
+ guard let (image, h264Info, nextCount) = imageAndInfo else { return }
1182
+ let callback = stateQueue.sync { self.callback }
1183
+ guard let callback else { return }
1184
+ _ = callback(image, h264Info)
1185
+ if streamShouldLog(nextCount) || image.frameType == .videoFrameKey {
1186
+ let kind = image.frameType == .videoFrameKey ? "keyframe" : "delta"
1187
+ streamLog("[webrtc:h264] emitted \(kind) #\(nextCount) bytes=\(image.buffer.count)")
1188
+ }
1189
+ }
1190
+
1191
+ private func annexBParameterSets(fromAvcC avcC: Data) -> Data {
1192
+ guard avcC.count >= 7 else { return Data() }
1193
+ var offset = 5
1194
+ let spsCount = Int(avcC[offset] & 0x1f)
1195
+ offset += 1
1196
+ var out = Data()
1197
+ for _ in 0..<spsCount {
1198
+ guard offset + 2 <= avcC.count else { return out }
1199
+ let length = (Int(avcC[offset]) << 8) | Int(avcC[offset + 1])
1200
+ offset += 2
1201
+ guard offset + length <= avcC.count else { return out }
1202
+ appendAnnexBStartCode(to: &out)
1203
+ out.append(avcC.subdata(in: offset..<(offset + length)))
1204
+ offset += length
1205
+ }
1206
+ guard offset < avcC.count else { return out }
1207
+ let ppsCount = Int(avcC[offset])
1208
+ offset += 1
1209
+ for _ in 0..<ppsCount {
1210
+ guard offset + 2 <= avcC.count else { return out }
1211
+ let length = (Int(avcC[offset]) << 8) | Int(avcC[offset + 1])
1212
+ offset += 2
1213
+ guard offset + length <= avcC.count else { return out }
1214
+ appendAnnexBStartCode(to: &out)
1215
+ out.append(avcC.subdata(in: offset..<(offset + length)))
1216
+ offset += length
1217
+ }
1218
+ return out
1219
+ }
1220
+
1221
+ private func appendAnnexBNals(fromAvcc avcc: Data, to out: inout Data) {
1222
+ var offset = 0
1223
+ while offset + 4 <= avcc.count {
1224
+ let length =
1225
+ (Int(avcc[offset]) << 24) |
1226
+ (Int(avcc[offset + 1]) << 16) |
1227
+ (Int(avcc[offset + 2]) << 8) |
1228
+ Int(avcc[offset + 3])
1229
+ offset += 4
1230
+ guard length > 0, offset + length <= avcc.count else { return }
1231
+ appendAnnexBStartCode(to: &out)
1232
+ out.append(avcc.subdata(in: offset..<(offset + length)))
1233
+ offset += length
1234
+ }
1235
+ }
1236
+
1237
+ private func appendAnnexBStartCode(to out: inout Data) {
1238
+ out.append(0)
1239
+ out.append(0)
1240
+ out.append(0)
1241
+ out.append(1)
1242
+ }
1243
+ }
1244
+
939
1245
  private final class H264WebRTCPixelBufferConverter {
940
1246
  private var pool: CVPixelBufferPool?
941
1247
  private var width = 0