serve-sim-sjchmiela 0.1.45 → 0.1.47

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.
@@ -30,6 +30,10 @@ final class WebRTCPublisher {
30
30
  private let videoTrack: LKRTCVideoTrack
31
31
  private let capturer: LKRTCVideoCapturer
32
32
  private var session: WebRTCSession?
33
+ private var lastOutputWidth = 0
34
+ private var lastOutputHeight = 0
35
+ private var sentFrameCount: Int64 = 0
36
+ private var lastFrameTimestampNs: Int64 = 0
33
37
  var isActive: Bool {
34
38
  queue.sync { session != nil }
35
39
  }
@@ -37,6 +41,7 @@ final class WebRTCPublisher {
37
41
  init() {
38
42
  videoSource = factory.videoSource(forScreenCast: true)
39
43
  videoTrack = factory.videoTrack(with: videoSource, trackId: "simulator-video")
44
+ videoTrack.isEnabled = true
40
45
  capturer = LKRTCVideoCapturer(delegate: videoSource)
41
46
  print("[webrtc] Publisher ready (factory + screen-cast video source)")
42
47
  }
@@ -57,19 +62,38 @@ final class WebRTCPublisher {
57
62
  func sendFrame(_ pixelBuffer: CVPixelBuffer, timestamp: CMTime) {
58
63
  queue.async {
59
64
  guard self.session != nil else { return }
60
- let captureTime = CMTimeGetSeconds(timestamp) * 1_000_000_000
61
- let timeNs = captureTime.isFinite && captureTime > 0
62
- ? Int64(captureTime)
63
- : Int64(DispatchTime.now().uptimeNanoseconds)
65
+ let width = CVPixelBufferGetWidth(pixelBuffer)
66
+ let height = CVPixelBufferGetHeight(pixelBuffer)
67
+ if width != self.lastOutputWidth || height != self.lastOutputHeight {
68
+ self.lastOutputWidth = width
69
+ self.lastOutputHeight = height
70
+ self.videoSource.adaptOutputFormat(toWidth: Int32(width), height: Int32(height), fps: 30)
71
+ print("[webrtc] Video source output format: \(width)x\(height) @ 30fps")
72
+ }
73
+ let timeNs = self.nextFrameTimestampNs(timestamp)
64
74
  let frame = LKRTCVideoFrame(
65
75
  buffer: LKRTCCVPixelBuffer(pixelBuffer: pixelBuffer),
66
76
  rotation: ._0,
67
77
  timeStampNs: timeNs
68
78
  )
69
79
  self.videoSource.capturer(self.capturer, didCapture: frame)
80
+ self.sentFrameCount += 1
81
+ if self.shouldLogFrame(self.sentFrameCount) {
82
+ print("[webrtc] Sent video frame #\(self.sentFrameCount) size=\(width)x\(height) timestampNs=\(timeNs)")
83
+ }
70
84
  }
71
85
  }
72
86
 
87
+ private func nextFrameTimestampNs(_ timestamp: CMTime) -> Int64 {
88
+ let captureTime = CMTimeGetSeconds(timestamp) * 1_000_000_000
89
+ let proposedTimestamp = captureTime.isFinite && captureTime > 0
90
+ ? Int64(captureTime)
91
+ : Int64(DispatchTime.now().uptimeNanoseconds)
92
+ let timestampNs = max(proposedTimestamp, lastFrameTimestampNs + 1)
93
+ lastFrameTimestampNs = timestampNs
94
+ return timestampNs
95
+ }
96
+
73
97
  func stop() {
74
98
  queue.sync {
75
99
  session?.close()
@@ -138,10 +162,12 @@ final class WebRTCPublisher {
138
162
  completion(.failure(error))
139
163
  return
140
164
  }
141
- session.waitForIceGathering {
165
+ session.waitForIceGathering { completed in
142
166
  let local = peerConnection.localDescription ?? answer
143
167
  let candidateCounts = self.iceCandidateCounts(in: local.sdp)
144
- if self.hasCredentialedTurnServer(request.iceServers), candidateCounts["relay", default: 0] == 0 {
168
+ if !completed {
169
+ print("[webrtc] ICE gathering timed out; proceeding with candidates gathered so far: \(candidateCounts)")
170
+ } else if self.hasCredentialedTurnServer(request.iceServers), candidateCounts["relay", default: 0] == 0 {
145
171
  print("[webrtc] WARNING: no relay ICE candidates gathered for credentialed TURN offer; counts=\(candidateCounts)")
146
172
  } else {
147
173
  print("[webrtc] ICE candidates gathered: \(candidateCounts)")
@@ -173,6 +199,7 @@ final class WebRTCPublisher {
173
199
  print("[webrtc] Failed to set video transceiver direction: \(directionError.localizedDescription)")
174
200
  }
175
201
  applyVideoCodecPreference(codec, to: transceiver)
202
+ configureVideoSender(transceiver.sender)
176
203
  }
177
204
 
178
205
  private func createFallbackVideoTransceiver(on peerConnection: LKRTCPeerConnection) -> LKRTCRtpTransceiver? {
@@ -268,26 +295,65 @@ final class WebRTCPublisher {
268
295
  print("[webrtc] Preferred video codec: \(preferredName)")
269
296
  }
270
297
 
298
+ private func configureVideoSender(_ sender: LKRTCRtpSender) {
299
+ let parameters = sender.parameters
300
+ let encodings = parameters.encodings.isEmpty
301
+ ? [LKRTCRtpEncodingParameters()]
302
+ : parameters.encodings
303
+ for encoding in encodings {
304
+ encoding.isActive = true
305
+ encoding.maxFramerate = 30
306
+ encoding.maxBitrateBps = 3_000_000
307
+ encoding.scaleResolutionDownBy = 1
308
+ }
309
+ parameters.encodings = encodings
310
+ sender.parameters = parameters
311
+ print("[webrtc] Video sender configured: encodings=\(encodings.count) active=true maxFramerate=30 maxBitrateBps=3000000")
312
+ }
313
+
271
314
  private func makeError(_ message: String) -> Error {
272
315
  NSError(domain: "serve-sim.webrtc", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
273
316
  }
317
+
318
+ private func shouldLogFrame(_ count: Int64) -> Bool {
319
+ count <= 5 || count % 120 == 0
320
+ }
274
321
  }
275
322
 
276
323
  private final class WebRTCSession {
277
324
  let peerConnection: LKRTCPeerConnection
278
325
  let delegate: WebRTCSessionDelegate
326
+ private let iceGatheringTimeout: DispatchTimeInterval = .milliseconds(3_000)
279
327
 
280
328
  init(peerConnection: LKRTCPeerConnection, delegate: WebRTCSessionDelegate) {
281
329
  self.peerConnection = peerConnection
282
330
  self.delegate = delegate
283
331
  }
284
332
 
285
- func waitForIceGathering(_ completion: @escaping () -> Void) {
333
+ func waitForIceGathering(_ completion: @escaping (Bool) -> Void) {
286
334
  if peerConnection.iceGatheringState == .complete {
287
- completion()
335
+ completion(true)
288
336
  return
289
337
  }
290
- delegate.onIceGatheringComplete = completion
338
+ let lock = NSLock()
339
+ var finished = false
340
+ let finish = { [weak delegate] (completed: Bool) in
341
+ lock.lock()
342
+ if finished {
343
+ lock.unlock()
344
+ return
345
+ }
346
+ finished = true
347
+ delegate?.onIceGatheringComplete = nil
348
+ lock.unlock()
349
+ completion(completed)
350
+ }
351
+ delegate.onIceGatheringComplete = {
352
+ finish(true)
353
+ }
354
+ DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + iceGatheringTimeout) {
355
+ finish(false)
356
+ }
291
357
  }
292
358
 
293
359
  func close() {
@@ -298,6 +364,7 @@ private final class WebRTCSession {
298
364
  private final class WebRTCSessionDelegate: NSObject, LKRTCPeerConnectionDelegate, LKRTCDataChannelDelegate {
299
365
  var onIceGatheringComplete: (() -> Void)?
300
366
  private let onInput: (Data) -> Void
367
+ private var statsScheduled = false
301
368
 
302
369
  init(onInput: @escaping (Data) -> Void) {
303
370
  self.onInput = onInput
@@ -309,6 +376,9 @@ private final class WebRTCSessionDelegate: NSObject, LKRTCPeerConnectionDelegate
309
376
  func peerConnectionShouldNegotiate(_ peerConnection: LKRTCPeerConnection) {}
310
377
  func peerConnection(_ peerConnection: LKRTCPeerConnection, didChange newState: LKRTCIceConnectionState) {
311
378
  print("[webrtc] ICE connection state: \(newState.rawValue)")
379
+ if newState == .connected || newState == .completed {
380
+ scheduleOutboundStats(peerConnection)
381
+ }
312
382
  }
313
383
  func peerConnection(_ peerConnection: LKRTCPeerConnection, didChange newState: LKRTCIceGatheringState) {
314
384
  print("[webrtc] ICE gathering state: \(newState.rawValue)")
@@ -362,4 +432,52 @@ private final class WebRTCSessionDelegate: NSObject, LKRTCPeerConnectionDelegate
362
432
  let server = candidate.serverUrl?.isEmpty == false ? " server=\(candidate.serverUrl!)" : ""
363
433
  return "type=\(type) protocol=\(protocolName) address=\(address) port=\(port)\(server)"
364
434
  }
435
+
436
+ private func scheduleOutboundStats(_ peerConnection: LKRTCPeerConnection) {
437
+ guard !statsScheduled else { return }
438
+ statsScheduled = true
439
+ logOutboundStats(peerConnection, label: "connected")
440
+ for seconds in [2.0, 5.0, 10.0] {
441
+ DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + seconds) { [weak self, weak peerConnection] in
442
+ guard let self, let peerConnection else { return }
443
+ self.logOutboundStats(peerConnection, label: "+\(Int(seconds))s")
444
+ }
445
+ }
446
+ }
447
+
448
+ private func logOutboundStats(_ peerConnection: LKRTCPeerConnection, label: String) {
449
+ peerConnection.statistics { report in
450
+ let videoStats = report.statistics.values
451
+ .filter { stat in
452
+ stat.type == "outbound-rtp" &&
453
+ ((stat.values["kind"] as? String) == "video" || (stat.values["mediaType"] as? String) == "video")
454
+ }
455
+ .map { stat in
456
+ self.statSummary(stat, keys: [
457
+ "bytesSent",
458
+ "packetsSent",
459
+ "framesEncoded",
460
+ "framesSent",
461
+ "keyFramesEncoded",
462
+ "hugeFramesSent",
463
+ "nackCount",
464
+ "firCount",
465
+ "pliCount",
466
+ ])
467
+ }
468
+ if videoStats.isEmpty {
469
+ print("[webrtc] Outbound stats \(label): no video outbound-rtp stats")
470
+ } else {
471
+ print("[webrtc] Outbound stats \(label): \(videoStats.joined(separator: " | "))")
472
+ }
473
+ }
474
+ }
475
+
476
+ private func statSummary(_ stat: LKRTCStatistics, keys: [String]) -> String {
477
+ let values = keys.compactMap { key -> String? in
478
+ guard let value = stat.values[key] else { return nil }
479
+ return "\(key)=\(value)"
480
+ }
481
+ return "\(stat.id){\(values.joined(separator: " "))}"
482
+ }
365
483
  }