replicas-computer 0.3.14 → 0.3.16

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/dist/index.js CHANGED
@@ -2937,7 +2937,7 @@ async function computerStatusCommand() {
2937
2937
  }
2938
2938
 
2939
2939
  // src/version.ts
2940
- var COMPUTER_USE_VERSION = "0.3.14";
2940
+ var COMPUTER_USE_VERSION = "0.3.16";
2941
2941
  var COMPUTER_USE_PROTOCOL_VERSION = 1;
2942
2942
 
2943
2943
  // src/program.ts
@@ -3179,69 +3179,11 @@ async function runComputerClient(argv) {
3179
3179
  // src/media-server.ts
3180
3180
  import { spawn as spawn5 } from "child_process";
3181
3181
  import { createHash as createHash3 } from "crypto";
3182
- import { createReadStream, existsSync as existsSync7, readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
3182
+ import { createReadStream, existsSync as existsSync7, readdirSync, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
3183
3183
  import { createServer as createServer2 } from "http";
3184
3184
  import { availableParallelism as availableParallelism2, loadavg } from "os";
3185
3185
  import { dirname as dirname7, extname, resolve as resolve3 } from "path";
3186
3186
  import { fileURLToPath as fileURLToPath2 } from "url";
3187
-
3188
- // ../shared/src/runtime-env.ts
3189
- function parsePosixEnvFile(content) {
3190
- const result = {};
3191
- let pos = 0;
3192
- while (pos < content.length) {
3193
- let lineStart;
3194
- if (pos === 0 || content[pos - 1] === "\n") {
3195
- lineStart = pos;
3196
- } else {
3197
- const nl2 = content.indexOf("\n", pos);
3198
- if (nl2 === -1) break;
3199
- lineStart = nl2 + 1;
3200
- }
3201
- if (!content.startsWith("export ", lineStart)) {
3202
- const nl2 = content.indexOf("\n", lineStart);
3203
- if (nl2 === -1) break;
3204
- pos = nl2 + 1;
3205
- continue;
3206
- }
3207
- const nameStart = lineStart + "export ".length;
3208
- const eqIdx = content.indexOf("=", nameStart);
3209
- if (eqIdx === -1) break;
3210
- const name = content.slice(nameStart, eqIdx).trim();
3211
- let cursor = eqIdx + 1;
3212
- if (content[cursor] !== "'") {
3213
- const nl2 = content.indexOf("\n", cursor);
3214
- pos = nl2 === -1 ? content.length : nl2 + 1;
3215
- continue;
3216
- }
3217
- cursor += 1;
3218
- let value = "";
3219
- let closed = false;
3220
- while (cursor < content.length) {
3221
- const ch = content[cursor];
3222
- if (ch === "'") {
3223
- if (content.startsWith("\\''", cursor + 1)) {
3224
- value += "'";
3225
- cursor += 4;
3226
- continue;
3227
- }
3228
- cursor += 1;
3229
- closed = true;
3230
- break;
3231
- }
3232
- value += ch;
3233
- cursor += 1;
3234
- }
3235
- if (closed && name) {
3236
- result[name] = value;
3237
- }
3238
- const nl = content.indexOf("\n", cursor);
3239
- pos = nl === -1 ? content.length : nl + 1;
3240
- }
3241
- return result;
3242
- }
3243
-
3244
- // src/media-server.ts
3245
3187
  import WebSocket2, { WebSocketServer } from "ws";
3246
3188
 
3247
3189
  // src/encoder.ts
@@ -3251,13 +3193,17 @@ import { createInterface } from "readline";
3251
3193
 
3252
3194
  // src/rtp.ts
3253
3195
  var RTP_HEADER_BYTES = 12;
3196
+ var NAL_SLICE = 1;
3254
3197
  var NAL_IDR = 5;
3198
+ var NAL_SEI = 6;
3255
3199
  var NAL_SPS = 7;
3200
+ var SEI_RECOVERY_POINT = 6;
3256
3201
  var NAL_STAP_A = 24;
3257
3202
  var NAL_FU_A = 28;
3258
3203
  var nalType = (byte) => byte & 31;
3259
3204
  var headerBytesOf = (packet) => RTP_HEADER_BYTES + (packet[0] & 15) * 4;
3260
3205
  var joinable = (nal) => nal === NAL_IDR || nal === NAL_SPS;
3206
+ var refreshFrame = (nal) => nalType(nal[0]) === NAL_SLICE || nalType(nal[0]) === NAL_SEI && nal[1] === SEI_RECOVERY_POINT;
3261
3207
  var isRtp = (data) => data.length >= RTP_HEADER_BYTES && data[0] >> 6 === 2 && (data[1] < 192 || data[1] > 223);
3262
3208
  var rtpPayload = (packet) => packet.subarray(headerBytesOf(packet));
3263
3209
  function aggregatedNals(payload) {
@@ -3286,7 +3232,10 @@ function isKeyframeStart(payload) {
3286
3232
  const fragment = fragmentHeader(payload);
3287
3233
  return fragment.start && joinable(fragment.type);
3288
3234
  }
3289
- if (type === NAL_STAP_A) return aggregatedNals(payload).nals.some((nal) => joinable(nalType(nal[0])));
3235
+ if (type === NAL_STAP_A) {
3236
+ const { nals } = aggregatedNals(payload);
3237
+ return nals.some((nal) => joinable(nalType(nal[0]))) && !nals.some(refreshFrame);
3238
+ }
3290
3239
  return joinable(type);
3291
3240
  }
3292
3241
  var RtpSplicer = class {
@@ -3363,6 +3312,8 @@ var RtpClock = class {
3363
3312
  };
3364
3313
  var startCode = new Uint8Array([0, 0, 0, 1]);
3365
3314
  var H264RtpAssembler = class {
3315
+ /** Frames abandoned for a missing or out-of-order packet; every delta after one is undecodable until a keyframe. */
3316
+ discarded = 0;
3366
3317
  clock = new RtpClock(9e4);
3367
3318
  chunks = [];
3368
3319
  key = false;
@@ -3380,6 +3331,7 @@ var H264RtpAssembler = class {
3380
3331
  }
3381
3332
  push(packet) {
3382
3333
  if (this.timestamp !== void 0 && packet.timestamp !== this.timestamp) {
3334
+ if (this.chunks.length > 0 || this.damaged) this.discarded += 1;
3383
3335
  this.clearFrame();
3384
3336
  }
3385
3337
  this.timestamp ??= packet.timestamp;
@@ -3412,6 +3364,7 @@ var H264RtpAssembler = class {
3412
3364
  } else this.damaged = true;
3413
3365
  if (!packet.marker) return null;
3414
3366
  if (this.damaged || this.fragmentType !== void 0 || this.chunks.length === 0) {
3367
+ this.discarded += 1;
3415
3368
  this.clearFrame();
3416
3369
  return null;
3417
3370
  }
@@ -3469,7 +3422,9 @@ function superviseEncoder(options) {
3469
3422
  const warmupFrames = active ? options.warmupFrames ?? 0 : 0;
3470
3423
  const command = options.command?.() ?? "ffmpeg";
3471
3424
  const args = options.args(socket.address().port, warmupFrames);
3472
- const child = spawn3(command, args, { env: options.env, stdio: ["ignore", options.onOutput ? "pipe" : "ignore", "pipe"] });
3425
+ const child = spawn3(command, args, { env: options.env, stdio: [options.controllable?.(command) ? "pipe" : "ignore", options.onOutput ? "pipe" : "ignore", "pipe"] });
3426
+ child.stdin?.on("error", () => {
3427
+ });
3473
3428
  child.stderr?.on("data", (chunk) => process.stderr.write(`[desktop-stream] ${chunk}`));
3474
3429
  const generation = { id: ++nextId, child, socket, live: true, startedAt: Date.now(), warmupFrames };
3475
3430
  candidate = generation;
@@ -3539,10 +3494,18 @@ function superviseEncoder(options) {
3539
3494
  candidate = void 0;
3540
3495
  active = void 0;
3541
3496
  });
3497
+ const control = (line) => {
3498
+ for (const generation of [candidate, active]) {
3499
+ if (generation?.live && generation.child.stdin?.writable) generation.child.stdin.write(`${line}
3500
+ `);
3501
+ }
3502
+ return Boolean(active?.live && active.child.stdin?.writable);
3503
+ };
3542
3504
  return {
3543
3505
  start,
3544
3506
  replace,
3545
3507
  stop,
3508
+ control,
3546
3509
  running: () => Boolean(active && alive(active.child)),
3547
3510
  replacing: () => Boolean(candidate),
3548
3511
  generation: () => active?.id ?? 0,
@@ -3552,13 +3515,17 @@ function superviseEncoder(options) {
3552
3515
  };
3553
3516
  }
3554
3517
 
3555
- // src/video-encoder.ts
3556
- import { existsSync as existsSync6 } from "fs";
3557
- import { availableParallelism } from "os";
3558
- import { dirname as dirname6, resolve as resolve2 } from "path";
3559
- import { fileURLToPath } from "url";
3518
+ // src/ice-servers.ts
3519
+ import { readFileSync as readFileSync6 } from "fs";
3560
3520
 
3561
3521
  // ../shared/src/desktop-stream.ts
3522
+ function isStreamIceServer(value) {
3523
+ return isRecord(value) && typeof value.urls === "string" && (value.username === void 0 || typeof value.username === "string") && (value.credential === void 0 || typeof value.credential === "string");
3524
+ }
3525
+ var DESKTOP_ICE_SERVERS_PATH = "/v1/engine/desktop/ice-servers";
3526
+ function isDesktopIceServers(value) {
3527
+ return isRecord(value) && typeof value.expiresAt === "number" && Array.isArray(value.iceServers) && value.iceServers.every(isStreamIceServer);
3528
+ }
3562
3529
  var DEFAULT_STREAM_REQUEST = {
3563
3530
  transport: "webrtc",
3564
3531
  profile: "balanced",
@@ -3615,6 +3582,144 @@ function isDesktopWebRtcFailure(value) {
3615
3582
  ].every((key) => typeof value[key] === "string" && value[key].length <= 200) && ["elapsedMs", "candidateErrors", "framesDecoded", "packetsReceived", "packetsLost", "bytesReceived", "lastFrameAgeMs", "encoderFps"].every((key) => typeof value[key] === "number" && Number.isFinite(value[key]));
3616
3583
  }
3617
3584
 
3585
+ // ../shared/src/runtime-env.ts
3586
+ var REPLICAS_RUNTIME_ENV_ALIASES = {
3587
+ monolithUrl: ["REPLICAS_MONOLITH_URL", "MONOLITH_URL"],
3588
+ workspaceId: ["REPLICAS_WORKSPACE_ID", "WORKSPACE_ID"],
3589
+ linearSessionId: ["REPLICAS_LINEAR_SESSION_ID", "LINEAR_SESSION_ID"],
3590
+ linearAccessToken: ["REPLICAS_LINEAR_ACCESS_TOKEN", "LINEAR_ACCESS_TOKEN"],
3591
+ slackBotToken: ["REPLICAS_SLACK_BOT_TOKEN", "SLACK_BOT_TOKEN"],
3592
+ slackChannelId: ["REPLICAS_SLACK_CHANNEL_ID", "SLACK_CHANNEL_ID"],
3593
+ slackThreadTs: ["REPLICAS_SLACK_THREAD_TS", "SLACK_THREAD_TS"]
3594
+ };
3595
+ function readReplicasRuntimeEnv(readEnv, [namespacedKey, legacyKey]) {
3596
+ return readEnv(namespacedKey) ?? readEnv(legacyKey);
3597
+ }
3598
+ function parsePosixEnvFile(content) {
3599
+ const result = {};
3600
+ let pos = 0;
3601
+ while (pos < content.length) {
3602
+ let lineStart;
3603
+ if (pos === 0 || content[pos - 1] === "\n") {
3604
+ lineStart = pos;
3605
+ } else {
3606
+ const nl2 = content.indexOf("\n", pos);
3607
+ if (nl2 === -1) break;
3608
+ lineStart = nl2 + 1;
3609
+ }
3610
+ if (!content.startsWith("export ", lineStart)) {
3611
+ const nl2 = content.indexOf("\n", lineStart);
3612
+ if (nl2 === -1) break;
3613
+ pos = nl2 + 1;
3614
+ continue;
3615
+ }
3616
+ const nameStart = lineStart + "export ".length;
3617
+ const eqIdx = content.indexOf("=", nameStart);
3618
+ if (eqIdx === -1) break;
3619
+ const name = content.slice(nameStart, eqIdx).trim();
3620
+ let cursor = eqIdx + 1;
3621
+ if (content[cursor] !== "'") {
3622
+ const nl2 = content.indexOf("\n", cursor);
3623
+ pos = nl2 === -1 ? content.length : nl2 + 1;
3624
+ continue;
3625
+ }
3626
+ cursor += 1;
3627
+ let value = "";
3628
+ let closed = false;
3629
+ while (cursor < content.length) {
3630
+ const ch = content[cursor];
3631
+ if (ch === "'") {
3632
+ if (content.startsWith("\\''", cursor + 1)) {
3633
+ value += "'";
3634
+ cursor += 4;
3635
+ continue;
3636
+ }
3637
+ cursor += 1;
3638
+ closed = true;
3639
+ break;
3640
+ }
3641
+ value += ch;
3642
+ cursor += 1;
3643
+ }
3644
+ if (closed && name) {
3645
+ result[name] = value;
3646
+ }
3647
+ const nl = content.indexOf("\n", cursor);
3648
+ pos = nl === -1 ? content.length : nl + 1;
3649
+ }
3650
+ return result;
3651
+ }
3652
+
3653
+ // src/ice-servers.ts
3654
+ var runtimeEnvFile = process.env.REPLICAS_RUNTIME_ENV_FILE || "/home/user/.replicas/runtime-env.sh";
3655
+ var refreshMs = 60 * 6e4;
3656
+ var retryMs = 6e4;
3657
+ var relays = [];
3658
+ var expiresAt = 0;
3659
+ var refreshAt = 0;
3660
+ var refreshing;
3661
+ async function currentIceServers() {
3662
+ let env = process.env;
3663
+ try {
3664
+ env = { ...env, ...parsePosixEnvFile(readFileSync6(runtimeEnvFile, "utf8")) };
3665
+ } catch {
3666
+ }
3667
+ const monolithUrl = readReplicasRuntimeEnv((name) => env[name], REPLICAS_RUNTIME_ENV_ALIASES.monolithUrl);
3668
+ const workspaceId = readReplicasRuntimeEnv((name) => env[name], REPLICAS_RUNTIME_ENV_ALIASES.workspaceId);
3669
+ const engineSecret = env.REPLICAS_ENGINE_SECRET;
3670
+ if (monolithUrl && workspaceId && engineSecret && Date.now() >= refreshAt) {
3671
+ const refreshed = refreshing ??= refreshRelays(monolithUrl, workspaceId, engineSecret).finally(() => {
3672
+ refreshing = void 0;
3673
+ });
3674
+ if (expiresAt <= Date.now()) await refreshed;
3675
+ }
3676
+ return toIceServers(env.REPLICAS_DESKTOP_STREAM_ICE_SERVERS, expiresAt > Date.now() ? relays : []);
3677
+ }
3678
+ async function refreshRelays(monolithUrl, workspaceId, engineSecret) {
3679
+ try {
3680
+ const response = await fetch(new URL(DESKTOP_ICE_SERVERS_PATH, monolithUrl), {
3681
+ headers: { authorization: `Bearer ${engineSecret}`, "x-workspace-id": workspaceId },
3682
+ redirect: "error",
3683
+ signal: AbortSignal.timeout(2e3)
3684
+ });
3685
+ if (response.status === 404) {
3686
+ relays = [];
3687
+ expiresAt = 0;
3688
+ } else {
3689
+ const body = response.ok ? await response.json() : void 0;
3690
+ if (!isDesktopIceServers(body)) throw new Error(`HTTP ${response.status}`);
3691
+ ({ iceServers: relays, expiresAt } = body);
3692
+ }
3693
+ refreshAt = Date.now() + refreshMs;
3694
+ } catch (error) {
3695
+ process.stderr.write(`[desktop-stream] TURN credentials unavailable: ${error instanceof Error ? error.message : String(error)}
3696
+ `);
3697
+ refreshAt = Date.now() + retryMs;
3698
+ }
3699
+ }
3700
+ function toIceServers(spec = "stun:stun.l.google.com:19302", relayServers) {
3701
+ const browser = spec.split(",").map((url) => url.trim()).filter(Boolean).map((url) => {
3702
+ const match = url.match(/^(turns?):([^:@]+):(.+)@([^@]+)$/);
3703
+ if (!match) return { urls: url };
3704
+ const [username, credential] = match.slice(2, 4).map((value) => {
3705
+ try {
3706
+ return decodeURIComponent(value);
3707
+ } catch {
3708
+ return value;
3709
+ }
3710
+ });
3711
+ return { urls: `${match[1]}:${match[4]}`, username, credential };
3712
+ }).concat(relayServers);
3713
+ const native = browser.filter(({ urls }) => !/^turns:|[?&]transport=tcp/i.test(urls)).map(({ urls, username, credential }) => username === void 0 || credential === void 0 ? urls : urls.replace(/^turn:/i, `turn:${encodeURIComponent(username)}:${encodeURIComponent(credential)}@`));
3714
+ return { native, browser };
3715
+ }
3716
+
3717
+ // src/video-encoder.ts
3718
+ import { existsSync as existsSync6 } from "fs";
3719
+ import { availableParallelism } from "os";
3720
+ import { dirname as dirname6, resolve as resolve2 } from "path";
3721
+ import { fileURLToPath } from "url";
3722
+
3618
3723
  // src/input-validation.ts
3619
3724
  var finite = (value) => typeof value === "number" && Number.isFinite(value);
3620
3725
  var optionalFinite = (value) => value === void 0 || finite(value);
@@ -3625,6 +3730,8 @@ function validInput(value) {
3625
3730
  switch (value.type) {
3626
3731
  case "release":
3627
3732
  case "keyframe":
3733
+ case "retry-webrtc":
3734
+ case "webrtc-ready":
3628
3735
  return true;
3629
3736
  case "theme":
3630
3737
  return value.theme === "light" || value.theme === "dark";
@@ -3928,22 +4035,6 @@ async function loadWebRtc() {
3928
4035
  }
3929
4036
  }
3930
4037
  var cleanupWebRtc = () => ndc?.cleanup();
3931
- function parseIceServers(spec) {
3932
- const native = (spec ?? "stun:stun.l.google.com:19302").split(",").map((server) => server.trim()).filter(Boolean);
3933
- const browser = native.map((url) => {
3934
- const match = url.match(/^(turns?):([^:@]+):(.+)@([^@]+)$/);
3935
- if (!match) return { urls: url };
3936
- const [username, credential] = match.slice(2, 4).map((value) => {
3937
- try {
3938
- return decodeURIComponent(value);
3939
- } catch {
3940
- return value;
3941
- }
3942
- });
3943
- return { urls: `${match[1]}:${match[4]}`, username, credential };
3944
- });
3945
- return { native, browser };
3946
- }
3947
4038
  var WebRtcViewer = class {
3948
4039
  constructor(options, handlers) {
3949
4040
  this.handlers = handlers;
@@ -4078,6 +4169,7 @@ function superviseVideoEncoder(options) {
4078
4169
  const encoder = superviseEncoder({
4079
4170
  label: "video",
4080
4171
  command: () => nativeCapture ? captureBinary : "ffmpeg",
4172
+ controllable: (command) => command === captureBinary,
4081
4173
  onFailure: (command) => {
4082
4174
  if (command !== captureBinary) return;
4083
4175
  nativeCapture = false;
@@ -4234,6 +4326,8 @@ function superviseVideoEncoder(options) {
4234
4326
  });
4235
4327
  return {
4236
4328
  ...encoder,
4329
+ /** Native capture takes keyframe, quality and frame-rate changes over stdin; ffmpeg needs a replacement for each. */
4330
+ controllable: () => nativeCapture,
4237
4331
  capture: () => ({
4238
4332
  captureBackend: (encoder.command() ?? (nativeCapture ? captureBinary : "ffmpeg")) === captureBinary ? "native" : "ffmpeg",
4239
4333
  captureMetrics: captureMetrics?.generation === encoder.generation() && Date.now() - captureMetrics.at <= 2500 ? captureMetrics : void 0
@@ -4452,7 +4546,6 @@ var standbyRung = 5;
4452
4546
  var port = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_PORT || String(DESKTOP_STREAM_PORT), 10);
4453
4547
  var display = process.env.REPLICAS_DESKTOP_DISPLAY?.replace(/^([^:])/, ":$1") || ":99";
4454
4548
  var webrtcEnabled = process.env.REPLICAS_DESKTOP_STREAM_WEBRTC !== "0";
4455
- var runtimeEnvFile = process.env.REPLICAS_RUNTIME_ENV_FILE || "/home/user/.replicas/runtime-env.sh";
4456
4549
  var colorMode = process.env.REPLICAS_DESKTOP_STREAM_COLOR === "legacy" ? "legacy" : "bt709";
4457
4550
  var defaultRequest = parseStreamRequest(new URLSearchParams({
4458
4551
  transport: process.env.REPLICAS_DESKTOP_STREAM_TRANSPORT ?? "",
@@ -4479,6 +4572,7 @@ var qualityLadder = (profile) => rungCaps.map((kbps, index) => ({ kbps, crf: pro
4479
4572
  var fullChromaMinKbps = 8e3;
4480
4573
  var fallbackKeyframeMs = 250;
4481
4574
  var webrtcStartupRung = 2;
4575
+ var descentMemoryMs = 18e4;
4482
4576
  var targetBacklogMs = 120;
4483
4577
  var congestedBacklogMs = 250;
4484
4578
  var congestedDecodeQueue = 3;
@@ -4507,14 +4601,6 @@ function mediaPacket(kind, timestamp, payload) {
4507
4601
  header.writeBigUInt64BE(BigInt(Math.max(0, timestamp)), 4);
4508
4602
  return Buffer.concat([header, payload]);
4509
4603
  }
4510
- function currentIceServers() {
4511
- let spec = process.env.REPLICAS_DESKTOP_STREAM_ICE_SERVERS;
4512
- try {
4513
- spec = parsePosixEnvFile(readFileSync6(runtimeEnvFile, "utf8")).REPLICAS_DESKTOP_STREAM_ICE_SERVERS ?? spec;
4514
- } catch {
4515
- }
4516
- return parseIceServers(spec);
4517
- }
4518
4604
  var appliedTheme;
4519
4605
  var pendingTheme;
4520
4606
  var applyingTheme = false;
@@ -4540,7 +4626,7 @@ function drainDesktopTheme() {
4540
4626
  function processCpuMs(pid) {
4541
4627
  if (!pid) return 0;
4542
4628
  try {
4543
- const stat = readFileSync6(`/proc/${pid}/stat`, "utf8");
4629
+ const stat = readFileSync7(`/proc/${pid}/stat`, "utf8");
4544
4630
  const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
4545
4631
  return (Number(fields[11]) + Number(fields[12])) * 10;
4546
4632
  } catch {
@@ -4558,7 +4644,8 @@ async function runMediaServer() {
4558
4644
  let activeProfile = "balanced";
4559
4645
  let adaptive = true;
4560
4646
  let rung = 0;
4561
- let descents = 0;
4647
+ let descents = [];
4648
+ let failedRung = -1;
4562
4649
  let congestedSeconds = 0;
4563
4650
  let comfortableSeconds = 0;
4564
4651
  let cleanTransportSeconds = 0;
@@ -4577,6 +4664,8 @@ async function runMediaServer() {
4577
4664
  let encoderFps = 0;
4578
4665
  let videoWanted = false;
4579
4666
  let encoderSignature = "";
4667
+ let encoderDescription = "";
4668
+ let encoderLive = { crf: 0, kbps: 0, fps: 0 };
4580
4669
  let encoderStop;
4581
4670
  let joinKeyframe;
4582
4671
  let lastVideoAt = 0;
@@ -4611,10 +4700,13 @@ async function runMediaServer() {
4611
4700
  const quality = ladder()[allStandby() ? Math.max(rung, standbyRung) : rung];
4612
4701
  const encodeFps = streamFps();
4613
4702
  const sockets = socketViewers().length > 0;
4614
- const keyframeMs = sockets ? Math.min(quality.keyframeMs, fallbackKeyframeMs) : quality.keyframeMs;
4703
+ const onDemand = videoEncoder.controllable();
4704
+ const keyframeMs = onDemand ? 0 : sockets ? Math.min(quality.keyframeMs, fallbackKeyframeMs) : quality.keyframeMs;
4615
4705
  const videoProfile = profile();
4616
- const intraRefresh = !sockets;
4706
+ const intraRefresh = !onDemand && !sockets;
4617
4707
  const pixelFormat = videoProfile === "high444" ? "yuv444p" : "yuv420p";
4708
+ const live = `rung ${rung} \xB7 crf ${quality.crf} \xB7 ${encodeFps} fps`;
4709
+ const restart = `${activeProfile} \xB7 ${preset} \xB7 ${videoProfile} \xB7 keyframe ${keyframeMs ? `${keyframeMs}ms${intraRefresh ? " refresh" : ""}` : "on demand"}`;
4618
4710
  return {
4619
4711
  quality,
4620
4712
  preset,
@@ -4624,7 +4716,8 @@ async function runMediaServer() {
4624
4716
  pixelFormat,
4625
4717
  fps: encodeFps,
4626
4718
  chromaQpOffset: profiles[activeProfile].chromaQpOffset,
4627
- signature: `rung ${rung} \xB7 ${activeProfile} crf ${quality.crf} \xB7 ${preset} \xB7 ${videoProfile} \xB7 keyframe ${keyframeMs}ms${intraRefresh ? " refresh" : ""} \xB7 ${encodeFps} fps`
4719
+ signature: onDemand ? restart : `${live} \xB7 ${restart}`,
4720
+ description: `${live} \xB7 ${restart}`
4628
4721
  };
4629
4722
  };
4630
4723
  const budgetBps = () => ladder()[rung].kbps * 1e3 * budgetFactor;
@@ -4645,6 +4738,7 @@ async function runMediaServer() {
4645
4738
  if (!keyframe2 && (viewer.waitingForKeyframe || queueMs(viewer) > targetBacklogMs || socket.bufferedAmount > fallbackBufferedBytes)) {
4646
4739
  viewer.waitingForKeyframe = true;
4647
4740
  viewer.withheldFrames += 1;
4741
+ if (videoEncoder.controllable() && queueMs(viewer) <= targetBacklogMs && socket.bufferedAmount <= fallbackBufferedBytes) requestKeyframe("socket viewer caught up", void 0, viewer);
4648
4742
  continue;
4649
4743
  }
4650
4744
  if (keyframe2) viewer.waitingForKeyframe = false;
@@ -4676,7 +4770,7 @@ async function runMediaServer() {
4676
4770
  let sockets = false;
4677
4771
  for (const viewer of viewers.values()) {
4678
4772
  if (viewer.fallback) sockets = true;
4679
- else viewer.rtc?.enqueueVideo(rtp ??= withPlayoutDelay(packet, PLAYOUT_DELAY_ID), frameId, keyframe, last);
4773
+ viewer.rtc?.enqueueVideo(rtp ??= withPlayoutDelay(packet, PLAYOUT_DELAY_ID), frameId, keyframe, last);
4680
4774
  }
4681
4775
  frameStart = last;
4682
4776
  if (last) {
@@ -4685,7 +4779,12 @@ async function runMediaServer() {
4685
4779
  }
4686
4780
  if (!sockets) return;
4687
4781
  const parsed = parseRtpPacket(packet);
4782
+ const discarded = videoAssembler.discarded;
4688
4783
  const frame = parsed && videoAssembler.push(parsed);
4784
+ if (videoAssembler.discarded !== discarded) {
4785
+ for (const viewer of socketViewers()) viewer.waitingForKeyframe = true;
4786
+ if (videoEncoder.controllable()) requestKeyframe("socket frame lost");
4787
+ }
4689
4788
  if (frame) broadcast(mediaPacket(frame.key ? 1 /* VideoKey */ : 2 /* VideoDelta */, frame.timestamp, frame.data), frame.key);
4690
4789
  };
4691
4790
  const onAudioPacket = (packet, generation) => {
@@ -4696,7 +4795,7 @@ async function runMediaServer() {
4696
4795
  let sockets = false;
4697
4796
  for (const viewer of viewers.values()) {
4698
4797
  if (viewer.fallback) sockets = true;
4699
- else viewer.rtc?.sendAudio(packet);
4798
+ viewer.rtc?.sendAudio(packet);
4700
4799
  }
4701
4800
  if (!sockets) return;
4702
4801
  const parsed = parseRtpPacket(packet);
@@ -4770,20 +4869,30 @@ async function runMediaServer() {
4770
4869
  const ensureEncoders = () => {
4771
4870
  clearTimeout(encoderStop);
4772
4871
  void audioEncoder.start();
4773
- const { signature } = encoderParams();
4872
+ const { signature, description, quality, fps: encodeFps } = encoderParams();
4873
+ const live = { crf: quality.crf, kbps: quality.kbps, fps: encodeFps };
4774
4874
  if (!videoWanted) {
4775
4875
  videoWanted = true;
4776
4876
  encoderSignature = signature;
4777
- record("encoder", signature);
4877
+ encoderDescription = description;
4878
+ encoderLive = live;
4879
+ record("encoder", description);
4778
4880
  void videoEncoder.start();
4779
4881
  return false;
4780
4882
  }
4781
- if (signature === encoderSignature) return false;
4782
- encoderSignature = signature;
4783
- lastSwapAt = Date.now();
4784
- record("encoder", signature);
4785
- void videoEncoder.replace();
4786
- return true;
4883
+ if (description !== encoderDescription) record("encoder", description);
4884
+ encoderDescription = description;
4885
+ if (signature !== encoderSignature) {
4886
+ encoderSignature = signature;
4887
+ encoderLive = live;
4888
+ lastSwapAt = Date.now();
4889
+ void videoEncoder.replace();
4890
+ return true;
4891
+ }
4892
+ if (live.crf !== encoderLive.crf || live.kbps !== encoderLive.kbps) videoEncoder.control(`quality ${live.crf} ${live.kbps}`);
4893
+ if (live.fps !== encoderLive.fps) videoEncoder.control(`fps ${live.fps}`);
4894
+ encoderLive = live;
4895
+ return false;
4787
4896
  };
4788
4897
  const syncEncoder = () => videoWanted && viewers.size > 0 && ensureEncoders();
4789
4898
  const stopEncodersSoon = () => {
@@ -4801,8 +4910,9 @@ async function runMediaServer() {
4801
4910
  const quality = ladder()[next];
4802
4911
  record(next > rung ? "quality down" : "quality up", `rung ${rung} \u2192 ${next} (${quality.kbps} kbps cap, crf ${quality.crf}): ${reason}`);
4803
4912
  if (next > rung) {
4804
- if (now - lastDescentAt > 1e4) descents += 1;
4913
+ if (now - lastDescentAt > 1e4) descents.push(now);
4805
4914
  lastDescentAt = now;
4915
+ failedRung = rung;
4806
4916
  if (now - lastAscentAt < 15e3) {
4807
4917
  blockedRung = rung;
4808
4918
  blockedUntil = now + 12e4;
@@ -4818,7 +4928,8 @@ async function runMediaServer() {
4818
4928
  const resetLadder = (request) => {
4819
4929
  rung = request.transport === "webrtc" && request.adaptive ? webrtcStartupRung : 0;
4820
4930
  cleanTransportSeconds = 0;
4821
- descents = 0;
4931
+ descents = [];
4932
+ failedRung = -1;
4822
4933
  lastDescentAt = 0;
4823
4934
  lastAscentAt = 0;
4824
4935
  congestedSeconds = 0;
@@ -4838,6 +4949,7 @@ async function runMediaServer() {
4838
4949
  const sent = Math.min(...recentVideoKbps);
4839
4950
  return remb !== void 0 && earlier !== void 0 && remb <= earlier.kbps && sent > 500 && remb < sent * 0.9;
4840
4951
  };
4952
+ const loaded = () => Math.min(...recentVideoKbps) > 500;
4841
4953
  const signals = (viewer, stats) => {
4842
4954
  const now = Date.now();
4843
4955
  if (viewer.fallback) {
@@ -4848,22 +4960,24 @@ async function runMediaServer() {
4848
4960
  }
4849
4961
  const stableSince = stableRttWindows.get(viewer)?.since;
4850
4962
  return [
4851
- ...viewer.rttFloorMs > viewer.rtcBaselineMs + 40 && (stableSince === void 0 || now - stableSince < 2e3) ? [`rtt floor ${Math.round(viewer.rttFloorMs)} ms over ${Math.round(viewer.rtcBaselineMs)} ms baseline`] : [],
4963
+ ...viewer.rttFloorMs > viewer.rtcBaselineMs + 40 && (stableSince === void 0 || now - stableSince < 2e3) && loaded() ? [`rtt floor ${Math.round(viewer.rttFloorMs)} ms over ${Math.round(viewer.rtcBaselineMs)} ms baseline`] : [],
4852
4964
  ...stats.videoFps < expectedFps() * 0.6 && viewer.lowFpsReports >= 2 ? [`decoded ${stats.videoFps.toFixed(0)} fps`] : [],
4853
- ...(stats.lossPercent ?? 0) > 3 ? [`loss ${(stats.lossPercent ?? 0).toFixed(1)}%`] : [],
4854
- ...(stats.jitterBufferMs ?? 0) > 80 ? [`jitter buffer ${(stats.jitterBufferMs ?? 0).toFixed(0)} ms`] : []
4965
+ ...(stats.lossPercent ?? 0) > 3 && loaded() ? [`loss ${(stats.lossPercent ?? 0).toFixed(1)}%`] : [],
4966
+ ...(stats.jitterBufferMs ?? 0) > 80 && loaded() ? [`jitter buffer ${(stats.jitterBufferMs ?? 0).toFixed(0)} ms`] : []
4855
4967
  ];
4856
4968
  };
4857
4969
  const lowFpsObservationAt = /* @__PURE__ */ new WeakMap();
4858
- const severe = (viewer, stats) => !viewer.fallback && ((stats.lossPercent ?? 0) > 10 || stats.videoFps < expectedFps() * 0.25 && viewer.lowFpsReports >= 2);
4970
+ const severe = (viewer, stats) => !viewer.fallback && ((stats.lossPercent ?? 0) > 10 && loaded() || stats.videoFps < expectedFps() * 0.25 && viewer.lowFpsReports >= 2);
4859
4971
  let lastSevereDescentAt = 0;
4860
4972
  const descendSevere = (now, trigger) => {
4861
4973
  comfortableSeconds = 0;
4862
4974
  congestedSeconds = 0;
4863
4975
  if (!adaptive || now - lastSevereDescentAt < 1e3) return;
4864
4976
  lastSevereDescentAt = now;
4977
+ const sent = Math.min(...recentVideoKbps);
4865
4978
  const estimates = [...viewers.values()].flatMap((viewer) => {
4866
- const reported = viewer.stats && now - viewer.stats.at < 750 ? viewer.stats.availableKbps ?? 0 : 0;
4979
+ const available = viewer.stats && now - viewer.stats.at < 750 ? viewer.stats.availableKbps ?? 0 : 0;
4980
+ const reported = sent > 500 && available < sent * 0.9 ? available : 0;
4867
4981
  const remb = rembBelowSent(viewer, now) ? freshRemb(viewer, now) ?? 0 : 0;
4868
4982
  return [...reported > 0 ? [reported] : [], ...remb > 0 ? [remb] : []];
4869
4983
  });
@@ -4880,9 +4994,12 @@ async function runMediaServer() {
4880
4994
  videoKbps = Math.round((videoBytes - bytesAtTick) * 8 / 1e3);
4881
4995
  bytesAtTick = videoBytes;
4882
4996
  if (recentVideoKbps.push(videoKbps) > 3) recentVideoKbps.shift();
4883
- if (viewers.size === 0) return;
4997
+ if (viewers.size === 0) {
4998
+ starvedSeconds = 0;
4999
+ return;
5000
+ }
4884
5001
  const { captureBackend, captureMetrics } = videoEncoder.capture();
4885
- const captureBusy = captureBackend !== "native" || !captureMetrics || captureMetrics.busyMs / captureMetrics.elapsedMs > 0.8;
5002
+ const captureBusy = captureBackend !== "native" || (captureMetrics ? captureMetrics.busyMs / captureMetrics.elapsedMs > 0.8 : videoEncoder.activeForMs() > 2500);
4886
5003
  if (videoEncoder.running() && encoderFps < streamFps() * 0.9 && captureBusy) {
4887
5004
  cleanTransportSeconds = 0;
4888
5005
  congestedSeconds = 0;
@@ -4931,15 +5048,16 @@ async function runMediaServer() {
4931
5048
  return;
4932
5049
  }
4933
5050
  congestedSeconds = Math.max(0, congestedSeconds - 0.5);
4934
- if (descents === 0 && reporting.some((viewer) => !viewer.fallback && (viewer.rttFloorMs > viewer.rtcBaselineMs + 20 || (viewer.stats.playbackDroppedPercent ?? 0) > 5 && cleanTransportSeconds < 15))) {
5051
+ descents = descents.filter((at) => now - at < descentMemoryMs);
5052
+ if (descents.length === 0 && reporting.some((viewer) => !viewer.fallback && (viewer.rttFloorMs > viewer.rtcBaselineMs + 20 && loaded() || (viewer.stats.playbackDroppedPercent ?? 0) > 5 && cleanTransportSeconds < 15))) {
4935
5053
  comfortableSeconds = 0;
4936
5054
  return;
4937
5055
  }
4938
5056
  comfortableSeconds += 1;
4939
- const startupSeconds = reporting.every((viewer) => !viewer.fallback) ? 2 : 3;
4940
- const recoverySeconds = lastAscentAt > lastDescentAt && cleanTransportSeconds >= 15 ? 15 : Math.min(60, 15 * 2 ** Math.min(descents, 2));
4941
- if (comfortableSeconds < (descents === 0 ? startupSeconds : recoverySeconds)) return;
4942
5057
  const target = Math.max(0, rung - 1);
5058
+ const startupSeconds = reporting.every((viewer) => !viewer.fallback) ? 2 : 3;
5059
+ const retrySeconds = lastAscentAt > lastDescentAt && cleanTransportSeconds >= 15 ? 15 : Math.min(60, 15 * 2 ** Math.min(descents.length - 1, 2));
5060
+ if (comfortableSeconds < (descents.length > 0 && target <= failedRung ? retrySeconds : startupSeconds)) return;
4943
5061
  if (target === blockedRung && now < blockedUntil) return;
4944
5062
  setRung(target, `${comfortableSeconds}s comfortable, ${cleanTransportSeconds}s clean transport`);
4945
5063
  }, 1e3);
@@ -5019,9 +5137,18 @@ async function runMediaServer() {
5019
5137
  `);
5020
5138
  }, telemetryIntervalMs);
5021
5139
  telemetry.unref();
5140
+ let lastKeyframeRequestAt = 0;
5022
5141
  const requestKeyframe = (reason, joinedAt, requestingViewer) => {
5023
5142
  const now = Date.now();
5024
5143
  if (!videoWanted || viewers.size === 0 || videoEncoder.replacing() || lastKeyframeAt < now - videoEncoder.activeForMs() || joinedAt !== void 0 && lastKeyframeAt > joinedAt) return;
5144
+ if (videoEncoder.controllable()) {
5145
+ const rttMs = requestingViewer?.rtc?.rttMs ?? requestingViewer?.stats?.rttMs ?? 0;
5146
+ if (joinedAt === void 0 && now - Math.max(lastKeyframeAt, lastKeyframeRequestAt) < Math.max(300, rttMs + 150)) return;
5147
+ if (!videoEncoder.control("keyframe")) return;
5148
+ lastKeyframeRequestAt = now;
5149
+ record("keyframe", reason);
5150
+ return;
5151
+ }
5025
5152
  const startup = requestingViewer && !requestingViewer.hasDecodedFrame && now - requestingViewer.connectedAt < 1e4;
5026
5153
  const wait = (joinedAt === void 0 ? startup ? 500 : 2e3 : 350) - (now - lastSwapAt);
5027
5154
  if (wait > 0) {
@@ -5031,7 +5158,9 @@ async function runMediaServer() {
5031
5158
  }
5032
5159
  return;
5033
5160
  }
5034
- if (!startup && encoderParams().keyframeMs - (now - lastKeyframeAt) <= 350) return;
5161
+ const { keyframeMs, intraRefresh } = encoderParams();
5162
+ const untilPeriodic = keyframeMs - (now - lastKeyframeAt);
5163
+ if (!startup && !intraRefresh && untilPeriodic >= 0 && untilPeriodic <= 350) return;
5035
5164
  lastSwapAt = now;
5036
5165
  record("keyframe", reason);
5037
5166
  void videoEncoder.replace();
@@ -5044,16 +5173,51 @@ async function runMediaServer() {
5044
5173
  process.stderr.write(`[desktop-stream] webrtc fallback diagnostics: ${JSON.stringify(entry)}
5045
5174
  `);
5046
5175
  }
5047
- if (viewer.fallback) return;
5176
+ const retried = viewer.rtc;
5177
+ viewer.rtc?.close();
5178
+ viewer.rtc = void 0;
5179
+ if (viewer.fallback) {
5180
+ if (retried) record("webrtc retry failed", reason);
5181
+ return;
5182
+ }
5048
5183
  viewer.fallback = true;
5049
5184
  viewer.waitingForKeyframe = true;
5050
5185
  viewer.input.release();
5051
- viewer.rtc?.close();
5052
- viewer.rtc = void 0;
5053
5186
  record("fallback to socket", reason);
5054
5187
  if (!syncEncoder()) requestKeyframe("socket fallback");
5055
5188
  };
5056
5189
  const optional = (value, max) => value === void 0 ? void 0 : clamp(value, 0, max);
5190
+ const openWebRtc = (viewer, reply, iceServers) => {
5191
+ try {
5192
+ viewer.rtc = new WebRtcViewer({ iceServers, iceTransportPolicy: "all", portRange: icePorts, budgetBps: budgetBps() }, {
5193
+ offer: (sdp) => reply({ type: "offer", sdp }),
5194
+ candidate: (candidate, mid) => reply({ type: "candidate", candidate, mid }),
5195
+ message: (message, answer) => handleMessage(viewer, message, answer),
5196
+ keyframe: (reason) => {
5197
+ if (reason === "stale" && Date.now() - lastDescentAt > 1e3) descendSevere(Date.now(), "send queue went stale and was cut back to a keyframe");
5198
+ requestKeyframe(reason === "pli" ? "receiver pli" : "stale send queue", void 0, viewer);
5199
+ },
5200
+ remb: (bitsPerSecond) => {
5201
+ if (viewer.rembs.push({ kbps: bitsPerSecond / 1e3, at: Date.now() }) > 16) viewer.rembs.shift();
5202
+ },
5203
+ connected: () => {
5204
+ record("webrtc connected", viewer.rtc?.path ?? void 0);
5205
+ requestKeyframe("viewer joined", Date.now());
5206
+ },
5207
+ // A viewer that loses its peer cannot send the key-ups for whatever it
5208
+ // was holding, and its socket may stay open for a long time yet. A socket
5209
+ // viewer trying WebRTC again still sends its input over the socket.
5210
+ disconnected: (state) => {
5211
+ record(`webrtc ${state}`);
5212
+ if (!viewer.fallback) viewer.input.release();
5213
+ if (state === "failed") reply({ type: "webrtc-failed" });
5214
+ }
5215
+ });
5216
+ } catch (error) {
5217
+ record("webrtc error", `could not create a peer connection: ${error instanceof Error ? error.message : String(error)}`);
5218
+ reply({ type: "webrtc-failed" });
5219
+ }
5220
+ };
5057
5221
  const handleMessage = (viewer, message, reply) => {
5058
5222
  switch (message.type) {
5059
5223
  case "ping":
@@ -5131,7 +5295,7 @@ async function runMediaServer() {
5131
5295
  }
5132
5296
  if (viewer.stats.videoFps > 0) viewer.hasDecodedFrame = true;
5133
5297
  if (viewer.stats.videoFps >= expectedFps() * 0.8) viewer.everHealthy = true;
5134
- if (encoderHealthyTicks >= 3 && videoEncoder.running() && [...viewers.values()].every((v) => !v.fallback && v.stats && now - v.stats.at < 750 && now - v.connectedAt > 2500 && (v.stats.videoPacketsPerSecond ?? 0) > 0 && (v.stats.lossPercent ?? 0) > 10)) descendSevere(now, "every viewer reports over 10% loss");
5298
+ if (encoderHealthyTicks >= 3 && videoEncoder.running() && loaded() && [...viewers.values()].every((v) => !v.fallback && v.stats && now - v.stats.at < 750 && now - v.connectedAt > 2500 && (v.stats.videoPacketsPerSecond ?? 0) > 0 && (v.stats.lossPercent ?? 0) > 10)) descendSevere(now, "every viewer reports over 10% loss");
5135
5299
  return;
5136
5300
  }
5137
5301
  case "answer":
@@ -5144,8 +5308,28 @@ async function runMediaServer() {
5144
5308
  case "fallback":
5145
5309
  enterFallback(viewer, message.reason, message.failure);
5146
5310
  return;
5311
+ // A socket viewer whose WebRTC attempt failed tries again in the background;
5312
+ // the socket keeps carrying its media until the new peer shows video.
5313
+ case "retry-webrtc": {
5314
+ const retryable = () => webrtc && viewer.fallback && !viewer.rtc && viewer.request.transport === "webrtc" && viewer.socket.readyState === WebSocket2.OPEN;
5315
+ if (!retryable()) return;
5316
+ void currentIceServers().then((ice) => {
5317
+ if (!retryable()) return;
5318
+ record("retrying webrtc", "socket media continues meanwhile");
5319
+ openWebRtc(viewer, reply, ice.native);
5320
+ });
5321
+ return;
5322
+ }
5323
+ case "webrtc-ready":
5324
+ if (!viewer.fallback || !viewer.rtc?.connected) return;
5325
+ viewer.fallback = false;
5326
+ viewer.withheldFrames = 0;
5327
+ record("back on webrtc", viewer.rtc.path ?? void 0);
5328
+ syncEncoder();
5329
+ return;
5147
5330
  case "keyframe":
5148
- if (!viewer.fallback) requestKeyframe("viewer watchdog", void 0, viewer);
5331
+ if (viewer.fallback) viewer.waitingForKeyframe = true;
5332
+ if (!viewer.fallback || videoEncoder.controllable()) requestKeyframe(viewer.fallback ? "socket viewer decoder reset" : "viewer watchdog", void 0, viewer);
5149
5333
  return;
5150
5334
  case "theme":
5151
5335
  applyDesktopTheme(message.theme);
@@ -5237,10 +5421,11 @@ async function runMediaServer() {
5237
5421
  if (extname(path) === ".html") {
5238
5422
  ensureEncoders();
5239
5423
  if (viewers.size === 0) stopEncodersSoon();
5424
+ if (webrtc) void currentIceServers();
5240
5425
  response.setHeader("cache-control", "no-store");
5241
5426
  const digest = createHash3("sha1");
5242
- for (const name of readdirSync(webroot).filter((file) => /\.(js|css)$/.test(file)).sort()) digest.update(readFileSync6(resolve3(webroot, name)));
5243
- response.end(readFileSync6(path, "utf8").replaceAll("__VERSION__", `${COMPUTER_USE_VERSION}-${digest.digest("hex").slice(0, 8)}`));
5427
+ for (const name of readdirSync(webroot).filter((file) => /\.(js|css)$/.test(file)).sort()) digest.update(readFileSync7(resolve3(webroot, name)));
5428
+ response.end(readFileSync7(path, "utf8").replaceAll("__VERSION__", `${COMPUTER_USE_VERSION}-${digest.digest("hex").slice(0, 8)}`));
5244
5429
  return;
5245
5430
  }
5246
5431
  response.setHeader("cache-control", "private, max-age=31536000, immutable");
@@ -5253,9 +5438,10 @@ async function runMediaServer() {
5253
5438
  socket.destroy();
5254
5439
  return;
5255
5440
  }
5256
- webSocketServer.handleUpgrade(request, socket, head, (client) => webSocketServer.emit("connection", client, parseStreamRequest(url.searchParams, defaultRequest), url.searchParams.get("standby") === "1"));
5441
+ const streamRequest = parseStreamRequest(url.searchParams, defaultRequest);
5442
+ void (webrtc && streamRequest.transport === "webrtc" ? currentIceServers() : Promise.resolve(void 0)).then((ice) => webSocketServer.handleUpgrade(request, socket, head, (client) => webSocketServer.emit("connection", client, streamRequest, url.searchParams.get("standby") === "1", ice)));
5257
5443
  });
5258
- webSocketServer.on("connection", (socket, request, standby) => {
5444
+ webSocketServer.on("connection", (socket, request, standby, ice) => {
5259
5445
  const viewer = {
5260
5446
  socket,
5261
5447
  input: new InputBridge(display, width, height),
@@ -5289,41 +5475,10 @@ async function runMediaServer() {
5289
5475
  const reply = (message) => {
5290
5476
  if (socket.readyState === WebSocket2.OPEN) socket.send(JSON.stringify(message));
5291
5477
  };
5292
- const offerWebRtc = webrtc && request.transport === "webrtc";
5293
- const ice = offerWebRtc ? currentIceServers() : void 0;
5294
5478
  const fallbackProfile = profile() === "high444" ? "main" : profile();
5295
5479
  const configMessage = { ...config, videoCodec: H264_PROFILES[fallbackProfile].codec, iceServers: ice?.browser, request };
5296
5480
  reply(configMessage);
5297
- if (ice) {
5298
- try {
5299
- viewer.rtc = new WebRtcViewer({ iceServers: ice.native, iceTransportPolicy: "all", portRange: icePorts, budgetBps: budgetBps() }, {
5300
- offer: (sdp) => reply({ type: "offer", sdp }),
5301
- candidate: (candidate, mid) => reply({ type: "candidate", candidate, mid }),
5302
- message: (message, answer) => handleMessage(viewer, message, answer),
5303
- keyframe: (reason) => {
5304
- if (reason === "stale") descendSevere(Date.now(), "send queue went stale and was cut back to a keyframe");
5305
- requestKeyframe(reason === "pli" ? "receiver pli" : "stale send queue", void 0, viewer);
5306
- },
5307
- remb: (bitsPerSecond) => {
5308
- if (viewer.rembs.push({ kbps: bitsPerSecond / 1e3, at: Date.now() }) > 16) viewer.rembs.shift();
5309
- },
5310
- connected: () => {
5311
- record("webrtc connected", viewer.rtc?.path ?? void 0);
5312
- requestKeyframe("viewer joined", Date.now());
5313
- },
5314
- // A viewer that loses its peer cannot send the key-ups for whatever it
5315
- // was holding, and its socket may stay open for a long time yet.
5316
- disconnected: (state) => {
5317
- record(`webrtc ${state}`);
5318
- viewer.input.release();
5319
- if (state === "failed") reply({ type: "webrtc-failed" });
5320
- }
5321
- });
5322
- } catch (error) {
5323
- record("webrtc error", `could not create a peer connection: ${error instanceof Error ? error.message : String(error)}`);
5324
- reply({ type: "webrtc-failed" });
5325
- }
5326
- }
5481
+ if (ice) openWebRtc(viewer, reply, ice.native);
5327
5482
  if (!viewer.rtc) enterFallback(viewer, request.transport === "webrtc" ? "webrtc unavailable" : "websocket requested");
5328
5483
  ensureEncoders();
5329
5484
  socket.on("message", (raw, binary) => {