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
|
|
61
|
-
let
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|