serve-sim-sjchmiela 0.1.46 → 0.1.48

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.
@@ -162,17 +162,27 @@ final class WebRTCPublisher {
162
162
  completion(.failure(error))
163
163
  return
164
164
  }
165
- session.waitForIceGathering {
165
+ session.waitForIceGathering { completed in
166
166
  let local = peerConnection.localDescription ?? answer
167
- let candidateCounts = self.iceCandidateCounts(in: local.sdp)
168
- if self.hasCredentialedTurnServer(request.iceServers), candidateCounts["relay", default: 0] == 0 {
167
+ let gatheredCandidates = delegate.generatedCandidatesSnapshot()
168
+ let finalSdp = self.sdpWithGatheredCandidates(
169
+ local.sdp,
170
+ candidates: gatheredCandidates
171
+ )
172
+ var candidateCounts = self.iceCandidateCounts(in: finalSdp)
173
+ if candidateCounts.isEmpty {
174
+ candidateCounts = self.iceCandidateCounts(in: gatheredCandidates)
175
+ }
176
+ if !completed {
177
+ print("[webrtc] ICE gathering timed out; proceeding with candidates gathered so far: \(candidateCounts)")
178
+ } else if self.hasCredentialedTurnServer(request.iceServers), candidateCounts["relay", default: 0] == 0 {
169
179
  print("[webrtc] WARNING: no relay ICE candidates gathered for credentialed TURN offer; counts=\(candidateCounts)")
170
180
  } else {
171
181
  print("[webrtc] ICE candidates gathered: \(candidateCounts)")
172
182
  }
173
183
  completion(.success(WebRTCAnswerPayload(
174
184
  type: LKRTCSessionDescription.string(for: local.type),
175
- sdp: local.sdp
185
+ sdp: finalSdp
176
186
  )))
177
187
  }
178
188
  }
@@ -255,8 +265,25 @@ final class WebRTCPublisher {
255
265
  private func iceCandidateCounts(in sdp: String) -> [String: Int] {
256
266
  var counts: [String: Int] = [:]
257
267
  for line in sdp.split(separator: "\n") {
258
- guard line.hasPrefix("a=candidate:") else { continue }
259
- let parts = line.split(whereSeparator: { $0 == " " || $0 == "\t" })
268
+ let trimmedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
269
+ guard trimmedLine.hasPrefix("a=candidate:") else { continue }
270
+ let parts = trimmedLine.split(whereSeparator: { $0 == " " || $0 == "\t" })
271
+ if let typeIndex = parts.firstIndex(of: "typ"), parts.indices.contains(parts.index(after: typeIndex)) {
272
+ counts[String(parts[parts.index(after: typeIndex)]), default: 0] += 1
273
+ } else {
274
+ counts["unknown", default: 0] += 1
275
+ }
276
+ }
277
+ return counts
278
+ }
279
+
280
+ private func iceCandidateCounts(in candidates: [LKRTCIceCandidate]) -> [String: Int] {
281
+ var counts: [String: Int] = [:]
282
+ for candidate in candidates {
283
+ let candidateLine = candidate.sdp.hasPrefix("a=")
284
+ ? candidate.sdp
285
+ : "a=\(candidate.sdp)"
286
+ let parts = candidateLine.split(whereSeparator: { $0 == " " || $0 == "\t" })
260
287
  if let typeIndex = parts.firstIndex(of: "typ"), parts.indices.contains(parts.index(after: typeIndex)) {
261
288
  counts[String(parts[parts.index(after: typeIndex)]), default: 0] += 1
262
289
  } else {
@@ -266,6 +293,101 @@ final class WebRTCPublisher {
266
293
  return counts
267
294
  }
268
295
 
296
+ private func sdpWithGatheredCandidates(_ sdp: String, candidates: [LKRTCIceCandidate]) -> String {
297
+ let newline = sdp.contains("\r\n") ? "\r\n" : "\n"
298
+ var lines = sdp.components(separatedBy: newline)
299
+ let hadTrailingNewline = lines.last == ""
300
+ if hadTrailingNewline {
301
+ lines.removeLast()
302
+ }
303
+ var existingCandidateLines = Set<String>()
304
+ var sectionsNeedingEndMarker = Set<Int>()
305
+ var currentSection = -1
306
+ for line in lines {
307
+ if line.hasPrefix("m=") {
308
+ currentSection += 1
309
+ } else if line.hasPrefix("a=candidate:"), currentSection >= 0 {
310
+ existingCandidateLines.insert(line)
311
+ sectionsNeedingEndMarker.insert(currentSection)
312
+ }
313
+ }
314
+ var sectionCandidates: [Int: [String]] = [:]
315
+
316
+ for candidate in candidates {
317
+ let candidateLine = candidate.sdp.hasPrefix("a=")
318
+ ? candidate.sdp
319
+ : "a=\(candidate.sdp)"
320
+ let sectionIndex = mediaSectionIndex(
321
+ in: lines,
322
+ sdpMid: candidate.sdpMid,
323
+ sdpMLineIndex: candidate.sdpMLineIndex
324
+ )
325
+ sectionsNeedingEndMarker.insert(sectionIndex)
326
+ guard !existingCandidateLines.contains(candidateLine) else { continue }
327
+ existingCandidateLines.insert(candidateLine)
328
+ sectionCandidates[sectionIndex, default: []].append(candidateLine)
329
+ }
330
+
331
+ for sectionIndex in sectionsNeedingEndMarker.sorted(by: >) {
332
+ let sectionRange = mediaSectionRange(in: lines, sectionIndex: sectionIndex)
333
+ let insertIndex = endOfCandidatesIndex(in: lines, range: sectionRange) ?? sectionRange.upperBound
334
+ var insertedLines = sectionCandidates[sectionIndex] ?? []
335
+ if endOfCandidatesIndex(in: lines, range: sectionRange) == nil {
336
+ insertedLines.append("a=end-of-candidates")
337
+ }
338
+ guard !insertedLines.isEmpty else { continue }
339
+ lines.insert(contentsOf: insertedLines, at: insertIndex)
340
+ }
341
+
342
+ let body = lines.joined(separator: newline)
343
+ return hadTrailingNewline ? "\(body)\(newline)" : body
344
+ }
345
+
346
+ private func mediaSectionIndex(
347
+ in lines: [String],
348
+ sdpMid: String?,
349
+ sdpMLineIndex: Int32
350
+ ) -> Int {
351
+ if let sdpMid {
352
+ var currentSection = -1
353
+ for line in lines {
354
+ if line.hasPrefix("m=") {
355
+ currentSection += 1
356
+ } else if line == "a=mid:\(sdpMid)", currentSection >= 0 {
357
+ return currentSection
358
+ }
359
+ }
360
+ }
361
+ let candidateIndex = Int(sdpMLineIndex)
362
+ return candidateIndex >= 0 ? candidateIndex : 0
363
+ }
364
+
365
+ private func mediaSectionRange(in lines: [String], sectionIndex: Int) -> Range<Int> {
366
+ var currentSection = -1
367
+ var start = lines.count
368
+ for (index, line) in lines.enumerated() where line.hasPrefix("m=") {
369
+ currentSection += 1
370
+ if currentSection == sectionIndex {
371
+ start = index
372
+ } else if currentSection > sectionIndex, start < lines.count {
373
+ return start..<index
374
+ }
375
+ }
376
+ if start < lines.count {
377
+ return start..<lines.count
378
+ }
379
+ return lines.count..<lines.count
380
+ }
381
+
382
+ private func endOfCandidatesIndex(in lines: [String], range: Range<Int>) -> Int? {
383
+ for index in range {
384
+ if lines[index] == "a=end-of-candidates" {
385
+ return index
386
+ }
387
+ }
388
+ return nil
389
+ }
390
+
269
391
  private func applyVideoCodecPreference(_ codec: String?, to transceiver: LKRTCRtpTransceiver) {
270
392
  let preferredName: String
271
393
  switch codec?.lowercased() {
@@ -321,18 +443,37 @@ final class WebRTCPublisher {
321
443
  private final class WebRTCSession {
322
444
  let peerConnection: LKRTCPeerConnection
323
445
  let delegate: WebRTCSessionDelegate
446
+ private let iceGatheringTimeout: DispatchTimeInterval = .milliseconds(3_000)
324
447
 
325
448
  init(peerConnection: LKRTCPeerConnection, delegate: WebRTCSessionDelegate) {
326
449
  self.peerConnection = peerConnection
327
450
  self.delegate = delegate
328
451
  }
329
452
 
330
- func waitForIceGathering(_ completion: @escaping () -> Void) {
453
+ func waitForIceGathering(_ completion: @escaping (Bool) -> Void) {
454
+ let lock = NSLock()
455
+ var finished = false
456
+ let finish = { [weak delegate] (completed: Bool) in
457
+ lock.lock()
458
+ if finished {
459
+ lock.unlock()
460
+ return
461
+ }
462
+ finished = true
463
+ delegate?.setIceGatheringCompleteHandler(nil)
464
+ lock.unlock()
465
+ completion(completed)
466
+ }
467
+ delegate.setIceGatheringCompleteHandler {
468
+ finish(true)
469
+ }
331
470
  if peerConnection.iceGatheringState == .complete {
332
- completion()
471
+ finish(true)
333
472
  return
334
473
  }
335
- delegate.onIceGatheringComplete = completion
474
+ DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + iceGatheringTimeout) {
475
+ finish(false)
476
+ }
336
477
  }
337
478
 
338
479
  func close() {
@@ -341,8 +482,11 @@ private final class WebRTCSession {
341
482
  }
342
483
 
343
484
  private final class WebRTCSessionDelegate: NSObject, LKRTCPeerConnectionDelegate, LKRTCDataChannelDelegate {
344
- var onIceGatheringComplete: (() -> Void)?
345
485
  private let onInput: (Data) -> Void
486
+ private let iceGatheringCompleteHandlerLock = NSLock()
487
+ private var iceGatheringCompleteHandler: (() -> Void)?
488
+ private let candidatesLock = NSLock()
489
+ private var generatedCandidates: [LKRTCIceCandidate] = []
346
490
  private var statsScheduled = false
347
491
 
348
492
  init(onInput: @escaping (Data) -> Void) {
@@ -362,12 +506,14 @@ private final class WebRTCSessionDelegate: NSObject, LKRTCPeerConnectionDelegate
362
506
  func peerConnection(_ peerConnection: LKRTCPeerConnection, didChange newState: LKRTCIceGatheringState) {
363
507
  print("[webrtc] ICE gathering state: \(newState.rawValue)")
364
508
  if newState == .complete {
365
- let completion = onIceGatheringComplete
366
- onIceGatheringComplete = nil
509
+ let completion = consumeIceGatheringCompleteHandler()
367
510
  completion?()
368
511
  }
369
512
  }
370
513
  func peerConnection(_ peerConnection: LKRTCPeerConnection, didGenerate candidate: LKRTCIceCandidate) {
514
+ candidatesLock.lock()
515
+ generatedCandidates.append(candidate)
516
+ candidatesLock.unlock()
371
517
  print("[webrtc] ICE candidate gathered: \(candidateSummary(candidate))")
372
518
  }
373
519
  func peerConnection(_ peerConnection: LKRTCPeerConnection, didRemove candidates: [LKRTCIceCandidate]) {}
@@ -397,6 +543,27 @@ private final class WebRTCSessionDelegate: NSObject, LKRTCPeerConnectionDelegate
397
543
  onInput(buffer.data)
398
544
  }
399
545
 
546
+ func generatedCandidatesSnapshot() -> [LKRTCIceCandidate] {
547
+ candidatesLock.lock()
548
+ let candidates = generatedCandidates
549
+ candidatesLock.unlock()
550
+ return candidates
551
+ }
552
+
553
+ func setIceGatheringCompleteHandler(_ handler: (() -> Void)?) {
554
+ iceGatheringCompleteHandlerLock.lock()
555
+ iceGatheringCompleteHandler = handler
556
+ iceGatheringCompleteHandlerLock.unlock()
557
+ }
558
+
559
+ private func consumeIceGatheringCompleteHandler() -> (() -> Void)? {
560
+ iceGatheringCompleteHandlerLock.lock()
561
+ let handler = iceGatheringCompleteHandler
562
+ iceGatheringCompleteHandler = nil
563
+ iceGatheringCompleteHandlerLock.unlock()
564
+ return handler
565
+ }
566
+
400
567
  private func candidateSummary(_ candidate: LKRTCIceCandidate) -> String {
401
568
  let parts = candidate.sdp.split(whereSeparator: { $0 == " " || $0 == "\t" })
402
569
  let protocolName = parts.indices.contains(2) ? String(parts[2]).lowercased() : "?"