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
|
|
168
|
-
|
|
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:
|
|
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
|
-
|
|
259
|
-
|
|
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
|
-
|
|
471
|
+
finish(true)
|
|
333
472
|
return
|
|
334
473
|
}
|
|
335
|
-
|
|
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 =
|
|
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() : "?"
|