serve-sim-sjchmiela 0.1.49 → 0.1.51
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.
- package/Sources/SimNative/sim-capture.swift +48 -1
- package/Sources/SimStreamHelper/FrameCapture.swift +24 -6
- package/Sources/SimStreamHelper/WebRTCPublisher.swift +123 -7
- package/dist/middleware.js +1 -1
- package/dist/native/serve-sim-native.node +0 -0
- package/dist/serve-sim.js +2 -2
- package/package.json +1 -1
|
@@ -58,11 +58,17 @@ final class CaptureEngine {
|
|
|
58
58
|
private var h264BackpressureSkips: Int64 = 0
|
|
59
59
|
private var mjpegThrottleSkips: Int64 = 0
|
|
60
60
|
private var h264ThrottleSkips: Int64 = 0
|
|
61
|
+
private var webRTCReservedCount: Int64 = 0
|
|
62
|
+
private var webRTCThrottleSkips: Int64 = 0
|
|
63
|
+
private var webRTCCopyFailures: Int64 = 0
|
|
61
64
|
private var avccNativeEmitCount: Int64 = 0
|
|
62
65
|
private var lastMjpegReservedAtNs: UInt64 = 0
|
|
63
66
|
private var lastH264ReservedAtNs: UInt64 = 0
|
|
67
|
+
private var lastWebRTCReservedAtNs: UInt64 = 0
|
|
64
68
|
private var mjpegMinFrameIntervalNs: UInt64
|
|
65
69
|
private var h264MinFrameIntervalNs: UInt64
|
|
70
|
+
private var webRTCMinFrameIntervalNs: UInt64
|
|
71
|
+
private var webRTCMaxFps: Int
|
|
66
72
|
private var maxDimension: Int
|
|
67
73
|
private var started = false
|
|
68
74
|
private var stopped = false
|
|
@@ -82,8 +88,17 @@ final class CaptureEngine {
|
|
|
82
88
|
h264Encoder = H264Encoder(fps: h264Fps, bitrate: h264Bitrate)
|
|
83
89
|
mjpegMinFrameIntervalNs = UInt64(1_000_000_000 / mjpegFps)
|
|
84
90
|
h264MinFrameIntervalNs = UInt64(1_000_000_000 / h264Fps)
|
|
91
|
+
webRTCMinFrameIntervalNs = UInt64(1_000_000_000 / h264Fps)
|
|
92
|
+
webRTCMaxFps = h264Fps
|
|
85
93
|
maxDimension = max(0, options.maxDimension)
|
|
86
94
|
webRTCPublisher.onInput = onWebRTCInput
|
|
95
|
+
webRTCPublisher.onActiveChanged = { [weak self] active in
|
|
96
|
+
guard let self else { return }
|
|
97
|
+
self.lastWebRTCReservedAtNs = 0
|
|
98
|
+
self.frameCapture.setIdleRefreshFps(active ? self.webRTCMaxFps : 5)
|
|
99
|
+
streamLog("[webrtc] active=\(active) idleRefreshFps=\(active ? self.webRTCMaxFps : 5)")
|
|
100
|
+
}
|
|
101
|
+
webRTCPublisher.updateSettings(maxFps: h264Fps, bitrate: h264Bitrate)
|
|
87
102
|
|
|
88
103
|
h264Encoder.onEncoded = { [weak self] encoded in
|
|
89
104
|
guard let self else { return }
|
|
@@ -144,7 +159,7 @@ final class CaptureEngine {
|
|
|
144
159
|
}
|
|
145
160
|
|
|
146
161
|
let h264Request = reserveH264EncodeIfNeeded()
|
|
147
|
-
let shouldSendWebRTC =
|
|
162
|
+
let shouldSendWebRTC = reserveWebRTCFrameIfNeeded()
|
|
148
163
|
let shouldEncodeJpeg = encoderReady && !encoding && reserveMjpegEncodeIfNeeded()
|
|
149
164
|
if !shouldEncodeJpeg && h264Request == nil && !shouldSendWebRTC { return }
|
|
150
165
|
|
|
@@ -153,6 +168,12 @@ final class CaptureEngine {
|
|
|
153
168
|
streamLog("[stream:avcc] failed to copy capture frame for H.264 token=\(h264Request.token)")
|
|
154
169
|
finishH264Encode(token: h264Request.token, restoreKeyframe: h264Request.forceKeyframe)
|
|
155
170
|
}
|
|
171
|
+
if shouldSendWebRTC {
|
|
172
|
+
webRTCCopyFailures += 1
|
|
173
|
+
if streamShouldLog(webRTCCopyFailures) {
|
|
174
|
+
streamLog("[webrtc] failed to copy capture frame for WebRTC")
|
|
175
|
+
}
|
|
176
|
+
}
|
|
156
177
|
return
|
|
157
178
|
}
|
|
158
179
|
|
|
@@ -336,6 +357,24 @@ final class CaptureEngine {
|
|
|
336
357
|
}
|
|
337
358
|
}
|
|
338
359
|
|
|
360
|
+
private func reserveWebRTCFrameIfNeeded() -> Bool {
|
|
361
|
+
guard webRTCPublisher.isActive else { return false }
|
|
362
|
+
let now = DispatchTime.now().uptimeNanoseconds
|
|
363
|
+
if lastWebRTCReservedAtNs != 0 && now - lastWebRTCReservedAtNs < webRTCMinFrameIntervalNs {
|
|
364
|
+
webRTCThrottleSkips += 1
|
|
365
|
+
if streamShouldLog(webRTCThrottleSkips) {
|
|
366
|
+
streamLog("[webrtc] skip frame: fps throttle")
|
|
367
|
+
}
|
|
368
|
+
return false
|
|
369
|
+
}
|
|
370
|
+
lastWebRTCReservedAtNs = now
|
|
371
|
+
webRTCReservedCount += 1
|
|
372
|
+
if streamShouldLog(webRTCReservedCount) {
|
|
373
|
+
streamLog("[webrtc] reserved frame #\(webRTCReservedCount)")
|
|
374
|
+
}
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
377
|
+
|
|
339
378
|
private func finishH264Encode(token: UInt64, restoreKeyframe: Bool = false) {
|
|
340
379
|
h264Queue.async { [weak self] in
|
|
341
380
|
guard let self, self.h264FrameToken == token else { return }
|
|
@@ -391,10 +430,17 @@ final class CaptureEngine {
|
|
|
391
430
|
}
|
|
392
431
|
h264Queue.sync {
|
|
393
432
|
self.h264MinFrameIntervalNs = UInt64(1_000_000_000 / normalizedH264Fps)
|
|
433
|
+
self.webRTCMinFrameIntervalNs = UInt64(1_000_000_000 / normalizedH264Fps)
|
|
434
|
+
self.webRTCMaxFps = normalizedH264Fps
|
|
394
435
|
self.h264Encoder.update(fps: normalizedH264Fps, bitrate: normalizedBitrate)
|
|
436
|
+
self.webRTCPublisher.updateSettings(maxFps: normalizedH264Fps, bitrate: normalizedBitrate)
|
|
437
|
+
if self.webRTCPublisher.isActive {
|
|
438
|
+
self.frameCapture.setIdleRefreshFps(normalizedH264Fps)
|
|
439
|
+
}
|
|
395
440
|
self.forceKeyframe = true
|
|
396
441
|
self.h264Encoding = false
|
|
397
442
|
self.lastH264ReservedAtNs = 0
|
|
443
|
+
self.lastWebRTCReservedAtNs = 0
|
|
398
444
|
}
|
|
399
445
|
streamLog(
|
|
400
446
|
"[stream] settings updated mjpegFps=\(normalizedMjpegFps) mjpegQuality=\(normalizedQuality) " +
|
|
@@ -405,6 +451,7 @@ final class CaptureEngine {
|
|
|
405
451
|
func handleWebRTCOffer(_ offerJson: String) throws -> String {
|
|
406
452
|
let request = try JSONDecoder().decode(WebRTCOfferPayload.self, from: Data(offerJson.utf8))
|
|
407
453
|
let answer = try webRTCPublisher.handleOffer(request)
|
|
454
|
+
lastWebRTCReservedAtNs = 0
|
|
408
455
|
let data = try JSONEncoder().encode(answer)
|
|
409
456
|
return String(decoding: data, as: UTF8.self)
|
|
410
457
|
}
|
|
@@ -20,6 +20,7 @@ final class FrameCapture {
|
|
|
20
20
|
private var idleTimer: DispatchSourceTimer?
|
|
21
21
|
private let captureQueue = DispatchQueue(label: "frame-capture", qos: .userInteractive)
|
|
22
22
|
private var lastCaptureTimeMs: UInt64 = 0
|
|
23
|
+
private var idleIntervalMs: UInt64 = 200
|
|
23
24
|
private var lastSeeds: [ObjectIdentifier: UInt32] = [:]
|
|
24
25
|
private var rewireTickCount: Int = 0
|
|
25
26
|
/// Interval at which the idle timer re-emits the current frame even when
|
|
@@ -32,7 +33,8 @@ final class FrameCapture {
|
|
|
32
33
|
/// one subscriber is due for it — a late-joining relay subscriber on an
|
|
33
34
|
/// idle sim never gets a cached frame to show.
|
|
34
35
|
/// Re-emitting at ~5 fps fixes both without meaningful CPU cost.
|
|
35
|
-
private static let
|
|
36
|
+
private static let defaultIdleRefreshFps = 5
|
|
37
|
+
private static let idleTimerTickMs: UInt64 = 33
|
|
36
38
|
|
|
37
39
|
private var descriptors: [NSObject] = []
|
|
38
40
|
private var callbackUUIDs: [ObjectIdentifier: NSUUID] = [:]
|
|
@@ -61,6 +63,16 @@ final class FrameCapture {
|
|
|
61
63
|
print("[capture] Frame callbacks registered (event-driven) + 5fps idle floor")
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
func setIdleRefreshFps(_ fps: Int) {
|
|
67
|
+
let normalizedFps = max(Self.defaultIdleRefreshFps, min(120, fps))
|
|
68
|
+
let nextIntervalMs = max(1, UInt64(1_000 / normalizedFps))
|
|
69
|
+
captureQueue.async { [weak self] in
|
|
70
|
+
guard let self, self.idleIntervalMs != nextIntervalMs else { return }
|
|
71
|
+
self.idleIntervalMs = nextIntervalMs
|
|
72
|
+
streamLog("[capture] Idle refresh fps=\(normalizedFps) intervalMs=\(nextIntervalMs)")
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
/// Find all framebuffer display descriptors, register callbacks on each,
|
|
65
77
|
/// and cache them. Safe to re-call if the cached descriptors become stale.
|
|
66
78
|
///
|
|
@@ -191,12 +203,12 @@ final class FrameCapture {
|
|
|
191
203
|
|
|
192
204
|
private func startIdleTimer() {
|
|
193
205
|
let timer = DispatchSource.makeTimerSource(queue: captureQueue)
|
|
194
|
-
timer.schedule(deadline: .now().advanced(by: .milliseconds(Int(Self.
|
|
195
|
-
repeating: .milliseconds(Int(Self.
|
|
206
|
+
timer.schedule(deadline: .now().advanced(by: .milliseconds(Int(Self.idleTimerTickMs))),
|
|
207
|
+
repeating: .milliseconds(Int(Self.idleTimerTickMs)))
|
|
196
208
|
timer.setEventHandler { [weak self] in
|
|
197
209
|
guard let self else { return }
|
|
198
210
|
let nowMs = DispatchTime.now().uptimeNanoseconds / 1_000_000
|
|
199
|
-
if (nowMs - self.lastCaptureTimeMs) >=
|
|
211
|
+
if (nowMs - self.lastCaptureTimeMs) >= self.idleIntervalMs {
|
|
200
212
|
self.captureFrame()
|
|
201
213
|
}
|
|
202
214
|
// Self-heal: if we've never captured a frame, the cached descriptor
|
|
@@ -236,7 +248,7 @@ final class FrameCapture {
|
|
|
236
248
|
let nowMs = DispatchTime.now().uptimeNanoseconds / 1_000_000
|
|
237
249
|
let sinceLastMs = nowMs &- lastCaptureTimeMs
|
|
238
250
|
let seedChanged = lastSeeds[key] != seed
|
|
239
|
-
let idleRefreshDue = frameCount > 0 && sinceLastMs >=
|
|
251
|
+
let idleRefreshDue = frameCount > 0 && sinceLastMs >= idleIntervalMs
|
|
240
252
|
if frameCount > 0, !seedChanged, !idleRefreshDue { return }
|
|
241
253
|
lastSeeds[key] = seed
|
|
242
254
|
|
|
@@ -260,7 +272,13 @@ final class FrameCapture {
|
|
|
260
272
|
|
|
261
273
|
lastCaptureTimeMs = nowMs
|
|
262
274
|
frameCount += 1
|
|
263
|
-
let
|
|
275
|
+
let nowNs = DispatchTime.now().uptimeNanoseconds
|
|
276
|
+
let timestamp = CMTime(value: CMTimeValue(nowNs), timescale: 1_000_000_000)
|
|
277
|
+
if streamShouldLog(Int64(frameCount)) {
|
|
278
|
+
let reason = seedChanged ? "changed" : "idle"
|
|
279
|
+
let interval = frameCount == 1 ? 0 : sinceLastMs
|
|
280
|
+
streamLog("[capture] frame #\(frameCount) reason=\(reason) intervalMs=\(interval) size=\(w)x\(h)")
|
|
281
|
+
}
|
|
264
282
|
onFrame?(pb, timestamp)
|
|
265
283
|
}
|
|
266
284
|
|
|
@@ -23,9 +23,10 @@ struct WebRTCAnswerPayload: Codable {
|
|
|
23
23
|
|
|
24
24
|
final class WebRTCPublisher {
|
|
25
25
|
var onInput: ((Data) -> Void)?
|
|
26
|
+
var onActiveChanged: ((Bool) -> Void)?
|
|
26
27
|
|
|
27
28
|
private let queue = DispatchQueue(label: "webrtc-publisher")
|
|
28
|
-
private let factory
|
|
29
|
+
private let factory: LKRTCPeerConnectionFactory
|
|
29
30
|
private let videoSource: LKRTCVideoSource
|
|
30
31
|
private let videoTrack: LKRTCVideoTrack
|
|
31
32
|
private let capturer: LKRTCVideoCapturer
|
|
@@ -34,16 +35,49 @@ final class WebRTCPublisher {
|
|
|
34
35
|
private var lastOutputHeight = 0
|
|
35
36
|
private var sentFrameCount: Int64 = 0
|
|
36
37
|
private var lastFrameTimestampNs: Int64 = 0
|
|
38
|
+
private var loggedInputFormat = false
|
|
39
|
+
private var maxFps = 30
|
|
40
|
+
private var targetBitrate = 6_000_000
|
|
37
41
|
var isActive: Bool {
|
|
38
42
|
queue.sync { session != nil }
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
init() {
|
|
46
|
+
let encoderFactory = LKRTCDefaultVideoEncoderFactory()
|
|
47
|
+
let decoderFactory = LKRTCDefaultVideoDecoderFactory()
|
|
48
|
+
factory = LKRTCPeerConnectionFactory(
|
|
49
|
+
encoderFactory: encoderFactory,
|
|
50
|
+
decoderFactory: decoderFactory
|
|
51
|
+
)
|
|
42
52
|
videoSource = factory.videoSource(forScreenCast: true)
|
|
43
53
|
videoTrack = factory.videoTrack(with: videoSource, trackId: "simulator-video")
|
|
44
54
|
videoTrack.isEnabled = true
|
|
45
55
|
capturer = LKRTCVideoCapturer(delegate: videoSource)
|
|
46
|
-
print(
|
|
56
|
+
print(
|
|
57
|
+
"[webrtc] Publisher ready (default codec factory + screen-cast video source) " +
|
|
58
|
+
"senderCodecs=\(senderCodecSummary())"
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func updateSettings(maxFps: Int, bitrate: Int) {
|
|
63
|
+
let normalizedFps = max(1, min(120, maxFps))
|
|
64
|
+
let normalizedBitrate = max(100_000, bitrate)
|
|
65
|
+
queue.async {
|
|
66
|
+
guard self.maxFps != normalizedFps || self.targetBitrate != normalizedBitrate else { return }
|
|
67
|
+
self.maxFps = normalizedFps
|
|
68
|
+
self.targetBitrate = normalizedBitrate
|
|
69
|
+
if self.lastOutputWidth > 0, self.lastOutputHeight > 0 {
|
|
70
|
+
self.videoSource.adaptOutputFormat(
|
|
71
|
+
toWidth: Int32(self.lastOutputWidth),
|
|
72
|
+
height: Int32(self.lastOutputHeight),
|
|
73
|
+
fps: Int32(self.maxFps)
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
if let session = self.session {
|
|
77
|
+
self.applyBitrateSettings(to: session)
|
|
78
|
+
}
|
|
79
|
+
print("[webrtc] Settings updated fps=\(normalizedFps) bitrate=\(normalizedBitrate)")
|
|
80
|
+
}
|
|
47
81
|
}
|
|
48
82
|
|
|
49
83
|
func handleOffer(_ request: WebRTCOfferPayload) throws -> WebRTCAnswerPayload {
|
|
@@ -67,19 +101,36 @@ final class WebRTCPublisher {
|
|
|
67
101
|
if width != self.lastOutputWidth || height != self.lastOutputHeight {
|
|
68
102
|
self.lastOutputWidth = width
|
|
69
103
|
self.lastOutputHeight = height
|
|
70
|
-
self.videoSource.adaptOutputFormat(
|
|
71
|
-
|
|
104
|
+
self.videoSource.adaptOutputFormat(
|
|
105
|
+
toWidth: Int32(width),
|
|
106
|
+
height: Int32(height),
|
|
107
|
+
fps: Int32(self.maxFps)
|
|
108
|
+
)
|
|
109
|
+
print("[webrtc] Video source output format: \(width)x\(height) @ \(self.maxFps)fps")
|
|
110
|
+
}
|
|
111
|
+
if !self.loggedInputFormat {
|
|
112
|
+
self.loggedInputFormat = true
|
|
113
|
+
let pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer)
|
|
114
|
+
let supported = LKRTCCVPixelBuffer.supportedPixelFormats()
|
|
115
|
+
.contains(NSNumber(value: UInt32(pixelFormat)))
|
|
116
|
+
print("[webrtc] Input pixel format: \(pixelFormat) cvPixelBufferSupported=\(supported); forwarding as I420")
|
|
72
117
|
}
|
|
73
118
|
let timeNs = self.nextFrameTimestampNs(timestamp)
|
|
74
|
-
let
|
|
119
|
+
let cvFrame = LKRTCVideoFrame(
|
|
75
120
|
buffer: LKRTCCVPixelBuffer(pixelBuffer: pixelBuffer),
|
|
76
121
|
rotation: ._0,
|
|
77
122
|
timeStampNs: timeNs
|
|
78
123
|
)
|
|
124
|
+
let convertStartNs = DispatchTime.now().uptimeNanoseconds
|
|
125
|
+
let frame = cvFrame.newI420()
|
|
79
126
|
self.videoSource.capturer(self.capturer, didCapture: frame)
|
|
127
|
+
let convertDurationMs = Double(DispatchTime.now().uptimeNanoseconds - convertStartNs) / 1_000_000.0
|
|
80
128
|
self.sentFrameCount += 1
|
|
81
129
|
if self.shouldLogFrame(self.sentFrameCount) {
|
|
82
|
-
print(
|
|
130
|
+
print(
|
|
131
|
+
"[webrtc] Sent video frame #\(self.sentFrameCount) size=\(width)x\(height) " +
|
|
132
|
+
"timestampNs=\(timeNs) i420Ms=\(String(format: "%.2f", convertDurationMs))"
|
|
133
|
+
)
|
|
83
134
|
}
|
|
84
135
|
}
|
|
85
136
|
}
|
|
@@ -98,6 +149,7 @@ final class WebRTCPublisher {
|
|
|
98
149
|
queue.sync {
|
|
99
150
|
session?.close()
|
|
100
151
|
session = nil
|
|
152
|
+
onActiveChanged?(false)
|
|
101
153
|
}
|
|
102
154
|
}
|
|
103
155
|
|
|
@@ -127,6 +179,8 @@ final class WebRTCPublisher {
|
|
|
127
179
|
)
|
|
128
180
|
let delegate = WebRTCSessionDelegate(onInput: { [weak self] data in
|
|
129
181
|
self?.onInput?(data)
|
|
182
|
+
}, onClosed: { [weak self] peerConnection in
|
|
183
|
+
self?.clearSession(peerConnection)
|
|
130
184
|
})
|
|
131
185
|
guard let peerConnection = factory.peerConnection(
|
|
132
186
|
with: config,
|
|
@@ -140,6 +194,7 @@ final class WebRTCPublisher {
|
|
|
140
194
|
let session = WebRTCSession(peerConnection: peerConnection, delegate: delegate)
|
|
141
195
|
self.session?.close()
|
|
142
196
|
self.session = session
|
|
197
|
+
onActiveChanged?(true)
|
|
143
198
|
|
|
144
199
|
let remoteDescription = LKRTCSessionDescription(type: .offer, sdp: request.sdp)
|
|
145
200
|
peerConnection.setRemoteDescription(remoteDescription) { error in
|
|
@@ -207,6 +262,11 @@ final class WebRTCPublisher {
|
|
|
207
262
|
print("[webrtc] Failed to set video transceiver direction: \(directionError.localizedDescription)")
|
|
208
263
|
}
|
|
209
264
|
applyVideoCodecPreference(codec, to: transceiver)
|
|
265
|
+
let session = self.session
|
|
266
|
+
session?.videoSender = transceiver.sender
|
|
267
|
+
if let session {
|
|
268
|
+
applyBitrateSettings(to: session)
|
|
269
|
+
}
|
|
210
270
|
}
|
|
211
271
|
|
|
212
272
|
private func createFallbackVideoTransceiver(on peerConnection: LKRTCPeerConnection) -> LKRTCRtpTransceiver? {
|
|
@@ -414,6 +474,51 @@ final class WebRTCPublisher {
|
|
|
414
474
|
print("[webrtc] Preferred video codec: \(preferredName)")
|
|
415
475
|
}
|
|
416
476
|
|
|
477
|
+
private func applyBitrateSettings(to session: WebRTCSession) {
|
|
478
|
+
guard let sender = session.videoSender else { return }
|
|
479
|
+
let parameters = sender.parameters
|
|
480
|
+
let encodings = parameters.encodings.isEmpty
|
|
481
|
+
? [LKRTCRtpEncodingParameters()]
|
|
482
|
+
: parameters.encodings
|
|
483
|
+
let maxBitrate = NSNumber(value: targetBitrate)
|
|
484
|
+
let minBitrate = NSNumber(value: max(100_000, targetBitrate / 4))
|
|
485
|
+
let fps = NSNumber(value: maxFps)
|
|
486
|
+
for encoding in encodings {
|
|
487
|
+
encoding.isActive = true
|
|
488
|
+
encoding.maxBitrateBps = maxBitrate
|
|
489
|
+
encoding.minBitrateBps = minBitrate
|
|
490
|
+
encoding.maxFramerate = fps
|
|
491
|
+
}
|
|
492
|
+
parameters.encodings = encodings
|
|
493
|
+
sender.parameters = parameters
|
|
494
|
+
let bweUpdated = session.peerConnection.setBweMinBitrateBps(
|
|
495
|
+
minBitrate,
|
|
496
|
+
currentBitrateBps: maxBitrate,
|
|
497
|
+
maxBitrateBps: maxBitrate
|
|
498
|
+
)
|
|
499
|
+
print(
|
|
500
|
+
"[webrtc] Sender parameters fps=\(maxFps) minBitrate=\(minBitrate) " +
|
|
501
|
+
"maxBitrate=\(maxBitrate) bweUpdated=\(bweUpdated)"
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private func clearSession(_ peerConnection: LKRTCPeerConnection?) {
|
|
506
|
+
queue.async {
|
|
507
|
+
guard let session = self.session, session.peerConnection === peerConnection else { return }
|
|
508
|
+
session.close()
|
|
509
|
+
self.session = nil
|
|
510
|
+
self.onActiveChanged?(false)
|
|
511
|
+
print("[webrtc] Peer connection closed; publisher inactive")
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private func senderCodecSummary() -> String {
|
|
516
|
+
let names = factory.rtpSenderCapabilities(forKind: "video").codecs.map { capability in
|
|
517
|
+
capability.mimeType.isEmpty ? capability.name : capability.mimeType
|
|
518
|
+
}
|
|
519
|
+
return names.joined(separator: ",")
|
|
520
|
+
}
|
|
521
|
+
|
|
417
522
|
private func makeError(_ message: String) -> Error {
|
|
418
523
|
NSError(domain: "serve-sim.webrtc", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
|
|
419
524
|
}
|
|
@@ -426,6 +531,7 @@ final class WebRTCPublisher {
|
|
|
426
531
|
private final class WebRTCSession {
|
|
427
532
|
let peerConnection: LKRTCPeerConnection
|
|
428
533
|
let delegate: WebRTCSessionDelegate
|
|
534
|
+
var videoSender: LKRTCRtpSender?
|
|
429
535
|
private let iceGatheringTimeout: DispatchTimeInterval = .milliseconds(3_000)
|
|
430
536
|
|
|
431
537
|
init(peerConnection: LKRTCPeerConnection, delegate: WebRTCSessionDelegate) {
|
|
@@ -466,14 +572,16 @@ private final class WebRTCSession {
|
|
|
466
572
|
|
|
467
573
|
private final class WebRTCSessionDelegate: NSObject, LKRTCPeerConnectionDelegate, LKRTCDataChannelDelegate {
|
|
468
574
|
private let onInput: (Data) -> Void
|
|
575
|
+
private let onClosed: (LKRTCPeerConnection) -> Void
|
|
469
576
|
private let iceGatheringCompleteHandlerLock = NSLock()
|
|
470
577
|
private var iceGatheringCompleteHandler: (() -> Void)?
|
|
471
578
|
private let candidatesLock = NSLock()
|
|
472
579
|
private var generatedCandidates: [LKRTCIceCandidate] = []
|
|
473
580
|
private var statsScheduled = false
|
|
474
581
|
|
|
475
|
-
init(onInput: @escaping (Data) -> Void) {
|
|
582
|
+
init(onInput: @escaping (Data) -> Void, onClosed: @escaping (LKRTCPeerConnection) -> Void) {
|
|
476
583
|
self.onInput = onInput
|
|
584
|
+
self.onClosed = onClosed
|
|
477
585
|
}
|
|
478
586
|
|
|
479
587
|
func peerConnection(_ peerConnection: LKRTCPeerConnection, didChange stateChanged: LKRTCSignalingState) {}
|
|
@@ -484,6 +592,14 @@ private final class WebRTCSessionDelegate: NSObject, LKRTCPeerConnectionDelegate
|
|
|
484
592
|
print("[webrtc] ICE connection state: \(newState.rawValue)")
|
|
485
593
|
if newState == .connected || newState == .completed {
|
|
486
594
|
scheduleOutboundStats(peerConnection)
|
|
595
|
+
} else if newState == .failed || newState == .closed {
|
|
596
|
+
onClosed(peerConnection)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
func peerConnection(_ peerConnection: LKRTCPeerConnection, didChange newState: LKRTCPeerConnectionState) {
|
|
600
|
+
print("[webrtc] Peer connection state: \(newState.rawValue)")
|
|
601
|
+
if newState == .failed || newState == .closed {
|
|
602
|
+
onClosed(peerConnection)
|
|
487
603
|
}
|
|
488
604
|
}
|
|
489
605
|
func peerConnection(_ peerConnection: LKRTCPeerConnection, didChange newState: LKRTCIceGatheringState) {
|