Skip to content

Commit 31e0c56

Browse files
authored
Raise the local stream cap to 120 fps (#15)
* Improve stream reconnect handling * Tighten local WebRTC recovery * Raise local stream cap to 120 fps * Fix docs formatting for CI * Stabilize JS API text entry integration
1 parent 1a620e2 commit 31e0c56

17 files changed

Lines changed: 264 additions & 84 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ more important than full-resolution smoothness:
116116
simdeck daemon start --video-codec software --low-latency
117117
```
118118

119+
Local browser streams default to 60 fps. On high-refresh local displays, opt in
120+
to a paced hardware stream up to 120 fps:
121+
122+
```sh
123+
simdeck daemon restart --local-stream-fps 120
124+
```
125+
119126
Restart the CoreSimulator service layer when `simctl` reports a stale service
120127
version or the live display gets stuck before the first frame:
121128

cli/XCWH264Encoder.m

Lines changed: 67 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
static const int32_t XCWTargetRealTimeFrameRate = 60;
1515
static const int32_t XCWTargetRealtimeHardwareFrameRate = 30;
1616
static const int32_t XCWTargetSoftwareFrameRate = 60;
17+
static const int32_t XCWMinimumLocalStreamFrameRate = 15;
18+
static const int32_t XCWMaximumLocalStreamFrameRate = 120;
1719
static const int32_t XCWTargetLowLatencySoftwareFrameRate = 15;
1820
static const NSUInteger XCWMaximumInFlightFrames = 2;
1921
static const int32_t XCWMinimumAverageBitRate = 18000000;
@@ -137,6 +139,22 @@ static uint64_t XCWRealtimeMaximumFrameIntervalUs(void) {
137139
return MAX(XCWRealtimeFrameIntervalUs() * 2, XCWRealtimeFrameIntervalUs());
138140
}
139141

142+
static int32_t XCWLocalStreamTargetFrameRate(void) {
143+
return XCWIntFromEnvironment(@"SIMDECK_LOCAL_STREAM_FPS",
144+
XCWTargetRealTimeFrameRate,
145+
XCWMinimumLocalStreamFrameRate,
146+
XCWMaximumLocalStreamFrameRate);
147+
}
148+
149+
static uint64_t XCWLocalStreamFrameIntervalUs(void) {
150+
int32_t fps = MAX(1, XCWLocalStreamTargetFrameRate());
151+
return (uint64_t)llround(1000000.0 / (double)fps);
152+
}
153+
154+
static uint64_t XCWLocalStreamMaximumFrameIntervalUs(void) {
155+
return MAX(XCWSoftwareMaximumFrameIntervalUs, XCWLocalStreamFrameIntervalUs());
156+
}
157+
140158
static int64_t XCWRealtimeBitsPerPixelBudgetValue(void) {
141159
return XCWInt64FromEnvironment(@"SIMDECK_REALTIME_BITS_PER_PIXEL",
142160
XCWRealtimeBitsPerPixelBudget,
@@ -490,10 +508,10 @@ @implementation XCWH264Encoder {
490508
uint64_t _lastSoftwareSubmissionUs;
491509
NSUInteger _softwarePacedFrameCount;
492510
NSUInteger _softwareHealthyFrameCount;
493-
uint64_t _realtimeHardwareFrameIntervalUs;
494-
uint64_t _lastRealtimeHardwareSubmissionUs;
495-
NSUInteger _realtimeHardwarePacedFrameCount;
496-
NSUInteger _realtimeHardwareHealthyFrameCount;
511+
uint64_t _hardwareFrameIntervalUs;
512+
uint64_t _lastHardwareSubmissionUs;
513+
NSUInteger _hardwarePacedFrameCount;
514+
NSUInteger _hardwareHealthyFrameCount;
497515
NSString *_selectedEncoderID;
498516
NSInteger _lastSessionStatus;
499517
NSInteger _lastPrepareStatus;
@@ -518,7 +536,7 @@ - (instancetype)initWithOutputHandler:(XCWH264EncoderOutputHandler)outputHandler
518536
_realtimeStreamMode = XCWRealtimeStreamModeFromEnvironment() || _lowLatencyMode;
519537
_codecType = XCWVideoCodecTypeForMode(_encoderMode);
520538
_softwareFrameIntervalUs = [self initialSoftwareFrameIntervalUsLocked];
521-
_realtimeHardwareFrameIntervalUs = [self initialHardwareFrameIntervalUsLocked];
539+
_hardwareFrameIntervalUs = [self initialHardwareFrameIntervalUsLocked];
522540
return self;
523541
}
524542

@@ -568,9 +586,9 @@ - (void)reconfigureForStreamQualityChange {
568586
self->_softwareFrameIntervalUs = [self initialSoftwareFrameIntervalUsLocked];
569587
self->_softwarePacedFrameCount = 0;
570588
self->_softwareHealthyFrameCount = 0;
571-
self->_realtimeHardwareFrameIntervalUs = [self initialHardwareFrameIntervalUsLocked];
572-
self->_realtimeHardwarePacedFrameCount = 0;
573-
self->_realtimeHardwareHealthyFrameCount = 0;
589+
self->_hardwareFrameIntervalUs = [self initialHardwareFrameIntervalUsLocked];
590+
self->_hardwarePacedFrameCount = 0;
591+
self->_hardwareHealthyFrameCount = 0;
574592
});
575593
}
576594

@@ -597,9 +615,13 @@ - (NSDictionary *)statsRepresentation {
597615
@"softwareFrameIntervalUs": @(self->_softwareFrameIntervalUs),
598616
@"softwareTargetFps": @(self->_softwareFrameIntervalUs > 0 ? (1000000.0 / (double)self->_softwareFrameIntervalUs) : 0.0),
599617
@"softwarePacedFrames": @(self->_softwarePacedFrameCount),
600-
@"realtimeHardwareFrameIntervalUs": @(self->_realtimeHardwareFrameIntervalUs),
601-
@"realtimeHardwareTargetFps": @(self->_realtimeHardwareFrameIntervalUs > 0 ? (1000000.0 / (double)self->_realtimeHardwareFrameIntervalUs) : 0.0),
602-
@"realtimeHardwarePacedFrames": @(self->_realtimeHardwarePacedFrameCount),
618+
@"localStreamTargetFps": @(XCWLocalStreamTargetFrameRate()),
619+
@"hardwareFrameIntervalUs": @(self->_hardwareFrameIntervalUs),
620+
@"hardwareTargetFps": @(self->_hardwareFrameIntervalUs > 0 ? (1000000.0 / (double)self->_hardwareFrameIntervalUs) : 0.0),
621+
@"hardwarePacedFrames": @(self->_hardwarePacedFrameCount),
622+
@"realtimeHardwareFrameIntervalUs": @(self->_hardwareFrameIntervalUs),
623+
@"realtimeHardwareTargetFps": @(self->_hardwareFrameIntervalUs > 0 ? (1000000.0 / (double)self->_hardwareFrameIntervalUs) : 0.0),
624+
@"realtimeHardwarePacedFrames": @(self->_hardwarePacedFrameCount),
603625
@"transportCodec": XCWCodecName(self->_codecType),
604626
@"encoderMode": XCWVideoEncoderModeName(self->_encoderMode),
605627
@"lowLatencyMode": @(self->_lowLatencyMode),
@@ -665,15 +687,15 @@ - (NSUInteger)softwareHealthyFrameWindowLocked {
665687
}
666688

667689
- (uint64_t)minimumHardwareFrameIntervalUsLocked {
668-
return _realtimeStreamMode ? XCWRealtimeFrameIntervalUs() : XCWSoftwareMinimumFrameIntervalUs;
690+
return _realtimeStreamMode ? XCWRealtimeFrameIntervalUs() : XCWLocalStreamFrameIntervalUs();
669691
}
670692

671693
- (uint64_t)initialHardwareFrameIntervalUsLocked {
672-
return _realtimeStreamMode ? XCWRealtimeFrameIntervalUs() : XCWSoftwareInitialFrameIntervalUs;
694+
return _realtimeStreamMode ? XCWRealtimeFrameIntervalUs() : XCWLocalStreamFrameIntervalUs();
673695
}
674696

675697
- (uint64_t)maximumHardwareFrameIntervalUsLocked {
676-
return _realtimeStreamMode ? XCWRealtimeMaximumFrameIntervalUs() : XCWSoftwareMaximumFrameIntervalUs;
698+
return _realtimeStreamMode ? XCWRealtimeMaximumFrameIntervalUs() : XCWLocalStreamMaximumFrameIntervalUs();
677699
}
678700

679701
- (int32_t)expectedFrameRateLocked {
@@ -686,24 +708,24 @@ - (int32_t)expectedFrameRateLocked {
686708
if (_realtimeStreamMode) {
687709
return XCWRealtimeTargetFrameRate();
688710
}
689-
return XCWTargetRealTimeFrameRate;
711+
return XCWLocalStreamTargetFrameRate();
690712
}
691713

692-
- (BOOL)shouldPaceRealtimeHardwareFrameAtTimeUs:(uint64_t)nowUs {
693-
if ((_encoderMode != XCWVideoEncoderModeAuto && _encoderMode != XCWVideoEncoderModeH264Hardware) || !_realtimeStreamMode || _needsKeyFrame) {
714+
- (BOOL)shouldPaceHardwareFrameAtTimeUs:(uint64_t)nowUs {
715+
if ((_encoderMode != XCWVideoEncoderModeAuto && _encoderMode != XCWVideoEncoderModeH264Hardware) || _needsKeyFrame) {
694716
return NO;
695717
}
696-
if (_realtimeHardwareFrameIntervalUs == 0) {
697-
_realtimeHardwareFrameIntervalUs = [self initialHardwareFrameIntervalUsLocked];
718+
if (_hardwareFrameIntervalUs == 0) {
719+
_hardwareFrameIntervalUs = [self initialHardwareFrameIntervalUsLocked];
698720
}
699-
if (_lastRealtimeHardwareSubmissionUs == 0) {
721+
if (_lastHardwareSubmissionUs == 0) {
700722
return NO;
701723
}
702-
uint64_t elapsedUs = nowUs >= _lastRealtimeHardwareSubmissionUs ? nowUs - _lastRealtimeHardwareSubmissionUs : 0;
703-
if (elapsedUs >= _realtimeHardwareFrameIntervalUs) {
724+
uint64_t elapsedUs = nowUs >= _lastHardwareSubmissionUs ? nowUs - _lastHardwareSubmissionUs : 0;
725+
if (elapsedUs >= _hardwareFrameIntervalUs) {
704726
return NO;
705727
}
706-
_realtimeHardwarePacedFrameCount += 1;
728+
_hardwarePacedFrameCount += 1;
707729
return YES;
708730
}
709731

@@ -764,41 +786,41 @@ - (void)adaptSoftwarePacingForLatencyUs:(uint64_t)latencyUs {
764786
_softwareHealthyFrameCount = 0;
765787
}
766788

767-
- (void)adaptRealtimeHardwarePacingForLatencyUs:(uint64_t)latencyUs {
768-
if ((_encoderMode != XCWVideoEncoderModeAuto && _encoderMode != XCWVideoEncoderModeH264Hardware) || !_realtimeStreamMode || latencyUs == 0) {
789+
- (void)adaptHardwarePacingForLatencyUs:(uint64_t)latencyUs {
790+
if ((_encoderMode != XCWVideoEncoderModeAuto && _encoderMode != XCWVideoEncoderModeH264Hardware) || latencyUs == 0) {
769791
return;
770792
}
771-
if (_realtimeHardwareFrameIntervalUs == 0) {
772-
_realtimeHardwareFrameIntervalUs = [self initialHardwareFrameIntervalUsLocked];
793+
if (_hardwareFrameIntervalUs == 0) {
794+
_hardwareFrameIntervalUs = [self initialHardwareFrameIntervalUsLocked];
773795
}
774796

775797
uint64_t minimumIntervalUs = [self minimumHardwareFrameIntervalUsLocked];
776798
uint64_t maximumIntervalUs = [self maximumHardwareFrameIntervalUsLocked];
777-
if (latencyUs > _realtimeHardwareFrameIntervalUs) {
778-
uint64_t nextIntervalUs = _realtimeHardwareFrameIntervalUs + XCWRealtimeHardwareFrameIntervalStepUs;
799+
if (latencyUs > _hardwareFrameIntervalUs) {
800+
uint64_t nextIntervalUs = _hardwareFrameIntervalUs + XCWRealtimeHardwareFrameIntervalStepUs;
779801
uint64_t latencyBoundIntervalUs = latencyUs + XCWRealtimeHardwareFrameIntervalStepUs;
780802
if (nextIntervalUs < latencyBoundIntervalUs) {
781803
nextIntervalUs = latencyBoundIntervalUs;
782804
}
783-
_realtimeHardwareFrameIntervalUs = MIN(nextIntervalUs, maximumIntervalUs);
784-
_realtimeHardwareHealthyFrameCount = 0;
805+
_hardwareFrameIntervalUs = MIN(nextIntervalUs, maximumIntervalUs);
806+
_hardwareHealthyFrameCount = 0;
785807
return;
786808
}
787809

788-
if (latencyUs < _realtimeHardwareFrameIntervalUs &&
789-
_realtimeHardwareFrameIntervalUs > minimumIntervalUs) {
790-
_realtimeHardwareHealthyFrameCount += 1;
791-
if (_realtimeHardwareHealthyFrameCount >= XCWRealtimeHardwareHealthyFrameWindow) {
792-
uint64_t nextIntervalUs = _realtimeHardwareFrameIntervalUs > XCWRealtimeHardwareFrameIntervalStepUs
793-
? _realtimeHardwareFrameIntervalUs - XCWRealtimeHardwareFrameIntervalStepUs
810+
if (latencyUs < _hardwareFrameIntervalUs &&
811+
_hardwareFrameIntervalUs > minimumIntervalUs) {
812+
_hardwareHealthyFrameCount += 1;
813+
if (_hardwareHealthyFrameCount >= XCWRealtimeHardwareHealthyFrameWindow) {
814+
uint64_t nextIntervalUs = _hardwareFrameIntervalUs > XCWRealtimeHardwareFrameIntervalStepUs
815+
? _hardwareFrameIntervalUs - XCWRealtimeHardwareFrameIntervalStepUs
794816
: minimumIntervalUs;
795-
_realtimeHardwareFrameIntervalUs = MAX(nextIntervalUs, minimumIntervalUs);
796-
_realtimeHardwareHealthyFrameCount = 0;
817+
_hardwareFrameIntervalUs = MAX(nextIntervalUs, minimumIntervalUs);
818+
_hardwareHealthyFrameCount = 0;
797819
}
798820
return;
799821
}
800822

801-
_realtimeHardwareHealthyFrameCount = 0;
823+
_hardwareHealthyFrameCount = 0;
802824
}
803825

804826
- (void)drainPendingFramesLocked {
@@ -839,7 +861,7 @@ - (BOOL)encodePixelBufferLocked:(CVPixelBufferRef)pixelBuffer {
839861
}
840862

841863
uint64_t nowUs = (uint64_t)(CACurrentMediaTime() * 1000000.0);
842-
if ([self shouldPaceSoftwareFrameAtTimeUs:nowUs] || [self shouldPaceRealtimeHardwareFrameAtTimeUs:nowUs]) {
864+
if ([self shouldPaceSoftwareFrameAtTimeUs:nowUs] || [self shouldPaceHardwareFrameAtTimeUs:nowUs]) {
843865
return YES;
844866
}
845867

@@ -885,8 +907,8 @@ - (BOOL)encodePixelBufferLocked:(CVPixelBufferRef)pixelBuffer {
885907
_submittedFrameCount += 1;
886908
if (_encoderMode == XCWVideoEncoderModeH264Software) {
887909
_lastSoftwareSubmissionUs = nowUs;
888-
} else if ((_encoderMode == XCWVideoEncoderModeAuto || _encoderMode == XCWVideoEncoderModeH264Hardware) && _realtimeStreamMode) {
889-
_lastRealtimeHardwareSubmissionUs = nowUs;
910+
} else if (_encoderMode == XCWVideoEncoderModeAuto || _encoderMode == XCWVideoEncoderModeH264Hardware) {
911+
_lastHardwareSubmissionUs = nowUs;
890912
}
891913
_maxInFlightFrameCount = MAX(_maxInFlightFrameCount, _inFlightFrameCount);
892914
if (_encoderMode == XCWVideoEncoderModeH264Software || !_realtimeStreamMode) {
@@ -1013,7 +1035,7 @@ - (void)invalidateCompressionSessionLocked {
10131035
_timestampOriginUs = 0;
10141036
_inFlightFrameCount = 0;
10151037
_lastSoftwareSubmissionUs = 0;
1016-
_lastRealtimeHardwareSubmissionUs = 0;
1038+
_lastHardwareSubmissionUs = 0;
10171039
_hardwareAccelerated = NO;
10181040
_selectedEncoderID = nil;
10191041
[self invalidateScalingResourcesLocked];
@@ -1203,7 +1225,7 @@ - (void)handleEncodedSampleBuffer:(CMSampleBufferRef)sampleBuffer
12031225
uint64_t nowUs = (uint64_t)(CACurrentMediaTime() * 1000000.0);
12041226
_latestEncodeLatencyUs = nowUs >= submittedAtUs ? nowUs - submittedAtUs : 0;
12051227
[self adaptSoftwarePacingForLatencyUs:_latestEncodeLatencyUs];
1206-
[self adaptRealtimeHardwarePacingForLatencyUs:_latestEncodeLatencyUs];
1228+
[self adaptHardwarePacingForLatencyUs:_latestEncodeLatencyUs];
12071229
}
12081230
NSString *codec = nil;
12091231
NSData *decoderConfig = nil;

client/src/features/stream/streamTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Size } from "../viewport/types";
22

33
export interface StreamConnectTarget {
4+
clientId?: string;
45
remote?: boolean;
56
udid: string;
67
}

client/src/features/stream/streamWorkerClient.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ const HAVE_CURRENT_DATA = 2;
1212
const WEBRTC_CONTROL_CHANNEL_LABEL = "simdeck-control";
1313
const WEBRTC_TELEMETRY_CHANNEL_LABEL = "simdeck-telemetry";
1414
const WEBRTC_FIRST_FRAME_TIMEOUT_MS = 10000;
15-
const WEBRTC_STALLED_FRAME_TIMEOUT_MS = 8000;
16-
const WEBRTC_DISCONNECTED_GRACE_MS = 8000;
15+
const WEBRTC_STALLED_FRAME_TIMEOUT_MS = 15000;
16+
const WEBRTC_REMOTE_RECEIVER_BUFFER_SECONDS = 0.06;
17+
const WEBRTC_DISCONNECTED_GRACE_MS = 30000;
1718
const WEBRTC_RECONNECT_BASE_DELAY_MS = 3000;
1819
const WEBRTC_RECONNECT_MAX_DELAY_MS = 10000;
1920

@@ -47,9 +48,9 @@ function sendDataChannelMessage(
4748

4849
export function buildStreamTarget(
4950
udid: string,
50-
options: { remote?: boolean } = {},
51+
options: { clientId?: string; remote?: boolean } = {},
5152
): StreamConnectTarget {
52-
return { remote: options.remote, udid };
53+
return { clientId: options.clientId, remote: options.remote, udid };
5354
}
5455

5556
export function canUseWebRtc(): boolean {
@@ -145,7 +146,10 @@ class WebRtcStreamClient implements StreamClientBackend {
145146
direction: "recvonly",
146147
});
147148
configureReceiverCodecPreferences(transceiver);
148-
configureLowLatencyReceiver(transceiver.receiver);
149+
configureLowLatencyReceiver(
150+
transceiver.receiver,
151+
target.remote ? WEBRTC_REMOTE_RECEIVER_BUFFER_SECONDS : null,
152+
);
149153
const controlChannel = peerConnection.createDataChannel(
150154
WEBRTC_CONTROL_CHANNEL_LABEL,
151155
{
@@ -180,7 +184,10 @@ class WebRtcStreamClient implements StreamClientBackend {
180184
}
181185
event.track.contentHint = "motion";
182186
for (const receiver of peerConnection.getReceivers()) {
183-
configureLowLatencyReceiver(receiver);
187+
configureLowLatencyReceiver(
188+
receiver,
189+
target.remote ? WEBRTC_REMOTE_RECEIVER_BUFFER_SECONDS : null,
190+
);
184191
}
185192
const stream = event.streams[0] ?? new MediaStream([event.track]);
186193
const video = document.createElement("video");
@@ -431,14 +438,22 @@ class WebRtcStreamClient implements StreamClientBackend {
431438
const hasRenderedFrame = this.stats.renderedFrames > 0;
432439
const frameAgeMs =
433440
this.lastVideoFrameAt > 0 ? now - this.lastVideoFrameAt : Infinity;
434-
if (!hasRenderedFrame || frameAgeMs > WEBRTC_STALLED_FRAME_TIMEOUT_MS) {
441+
if (!hasRenderedFrame) {
435442
this.handleConnectionError(
436443
target,
437444
generation,
438445
new Error("WebRTC video stalled before rendering fresh frames."),
439446
);
440447
return;
441448
}
449+
if (frameAgeMs > WEBRTC_STALLED_FRAME_TIMEOUT_MS) {
450+
this.handleConnectionError(
451+
target,
452+
generation,
453+
new Error("WebRTC video stalled after rendering frames."),
454+
);
455+
return;
456+
}
442457
this.scheduleFrameWatchdog(target, generation);
443458
},
444459
this.stats.renderedFrames > 0
@@ -575,7 +590,7 @@ class WebRtcStreamClient implements StreamClientBackend {
575590
private postDiagnostics(target: StreamConnectTarget, detail: string) {
576591
const payload = {
577592
...this.stats,
578-
clientId: "webrtc-page",
593+
clientId: target.clientId ?? "webrtc-page",
579594
connectionId: this.connectGeneration,
580595
detail,
581596
iceConnectionState: this.diagnostics.iceConnectionState,
@@ -778,16 +793,22 @@ function postWebRtcOffer(
778793
);
779794
}
780795

781-
function configureLowLatencyReceiver(receiver: RTCRtpReceiver) {
796+
function configureLowLatencyReceiver(
797+
receiver: RTCRtpReceiver,
798+
bufferSeconds: number | null,
799+
) {
800+
if (!bufferSeconds || bufferSeconds <= 0) {
801+
return;
802+
}
782803
const lowLatencyReceiver = receiver as RTCRtpReceiver & {
783804
jitterBufferTarget?: number;
784805
playoutDelayHint?: number;
785806
};
786807
if ("jitterBufferTarget" in lowLatencyReceiver) {
787-
lowLatencyReceiver.jitterBufferTarget = 0.001;
808+
lowLatencyReceiver.jitterBufferTarget = bufferSeconds;
788809
}
789810
if ("playoutDelayHint" in lowLatencyReceiver) {
790-
lowLatencyReceiver.playoutDelayHint = 0.001;
811+
lowLatencyReceiver.playoutDelayHint = bufferSeconds;
791812
}
792813
}
793814

client/src/features/stream/useLiveStream.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,12 @@ export function useLiveStream({
242242
return;
243243
}
244244

245-
workerClient.connect(buildStreamTarget(simulator.udid, { remote }));
245+
workerClient.connect(
246+
buildStreamTarget(simulator.udid, {
247+
clientId: clientTelemetryIdRef.current,
248+
remote,
249+
}),
250+
);
246251
return () => {
247252
workerClient.disconnect();
248253
};

docs/api/health.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Returns the static bootstrap information the browser client needs, plus a freshn
2828
| `videoCodec` | Requested encoder mode. One of `auto`, `hardware`, or `software`. See [Video Pipeline](/guide/video). |
2929
| `lowLatency` | `true` when software H.264 low-latency mode was enabled at daemon startup. |
3030
| `realtimeStream` | `true` when the WebRTC stream is configured to favor freshness and realtime pacing. |
31+
| `localStreamFps` | Local quality stream frame cap, from 15 to 120 fps. Defaults to 60. |
3132
| `streamQuality` | Active realtime quality profile and encoder limits such as `maxEdge`, `fps`, and bitrate. |
3233
| `webRtc.iceServers` | ICE servers the browser should use when creating the WebRTC peer connection. |
3334
| `webRtc.iceTransportPolicy` | Browser ICE transport policy. One of `all` or `relay`. |

0 commit comments

Comments
 (0)