replicas-computer 0.2.3 → 0.2.5

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.
@@ -39,6 +39,8 @@ JSON
39
39
  touch "$MARKER"
40
40
  fi
41
41
 
42
+ # password-store skips a keyring D-Bus probe that stalls first launch for seconds
43
+ # on a headless image; the network flags keep first-run downloads off the critical path.
42
44
  exec /usr/bin/google-chrome-stable \
43
45
  --user-data-dir="$PROFILE_DIR" \
44
46
  --profile-directory=Default \
@@ -46,6 +48,10 @@ exec /usr/bin/google-chrome-stable \
46
48
  --no-default-browser-check \
47
49
  --disable-search-engine-choice-screen \
48
50
  --disable-sync \
51
+ --password-store=basic \
52
+ --disable-component-update \
53
+ --disable-background-networking \
54
+ --no-pings \
49
55
  --remote-debugging-address=127.0.0.1 \
50
56
  --remote-debugging-port="$DEBUG_PORT" \
51
57
  --start-maximized \
@@ -384,8 +384,17 @@ fi
384
384
 
385
385
  kill_pids "$(legacy_stream_websockify_pids)"
386
386
  clear_bridge_pid_if_unhealthy stream has_stream stream_listener_pids stream_pids
387
+ # Forwarded only when set, so the stream server keeps its own default.
387
388
  start_bg stream \
388
389
  "has_stream" \
389
- "exec env REPLICAS_DESKTOP_DISPLAY=:$DISPLAY_NUM REPLICAS_DESKTOP_WIDTH=$WIDTH REPLICAS_DESKTOP_HEIGHT=$HEIGHT REPLICAS_DESKTOP_STREAM_FPS=${REPLICAS_DESKTOP_STREAM_FPS:-60} REPLICAS_DESKTOP_STREAM_PORT=$STREAM_PORT REPLICAS_DESKTOP_STREAM_WEBROOT=${REPLICAS_DESKTOP_STREAM_WEBROOT:-$REPLICAS_DESKTOP_TEMPLATES/stream} replicas-computer stream"
390
+ "exec env REPLICAS_DESKTOP_DISPLAY=:$DISPLAY_NUM REPLICAS_DESKTOP_WIDTH=$WIDTH REPLICAS_DESKTOP_HEIGHT=$HEIGHT ${REPLICAS_DESKTOP_STREAM_FPS:+REPLICAS_DESKTOP_STREAM_FPS=$REPLICAS_DESKTOP_STREAM_FPS }REPLICAS_DESKTOP_STREAM_PORT=$STREAM_PORT REPLICAS_DESKTOP_STREAM_WEBROOT=${REPLICAS_DESKTOP_STREAM_WEBROOT:-$REPLICAS_DESKTOP_TEMPLATES/stream} replicas-computer stream"
391
+
392
+ # Takes Chrome's cold-start cost at boot instead of on the user's first click;
393
+ # later launches join this instance through the profile singleton.
394
+ if [ "${REPLICAS_DESKTOP_CHROME_PREWARM:-1}" = "1" ] && command -v replicas-chrome >/dev/null 2>&1; then
395
+ start_bg chrome-prewarm \
396
+ "curl -fsS --max-time 1 http://127.0.0.1:${REPLICAS_DESKTOP_CHROME_DEBUG_PORT:-9222}/json/version" \
397
+ "DISPLAY=:$DISPLAY_NUM REPLICAS_DESKTOP_CHROME_PROFILE_DIR=\"$CHROME_PROFILE_DIR\" exec replicas-chrome --no-startup-window"
398
+ fi
390
399
 
391
400
  exit 0
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import { dirname, isAbsolute, resolve } from "path";
18
18
  var DESKTOP_STREAM_PORT = 6080;
19
19
  function getDesktopViewerUrl(publicUrl) {
20
20
  const url = new URL(`${publicUrl.replace(/\/$/, "")}/`);
21
- url.searchParams.set("v", "14");
21
+ url.searchParams.set("v", "15");
22
22
  return url.toString();
23
23
  }
24
24
  var DESKTOP_VIEWER_WIDTH = 1920;
@@ -2694,11 +2694,13 @@ async function computerLaunchCommand(app, args) {
2694
2694
  const fullArgs = [...baseArgs.slice(1), ...args];
2695
2695
  const expectsNewChromePage = args.some((arg) => !arg.startsWith("-"));
2696
2696
  const existingChromePageIds = /* @__PURE__ */ new Set();
2697
+ let chromeAlreadyRunning = false;
2697
2698
  if (app === "chrome") {
2698
2699
  try {
2699
2700
  for (const page of await getChromePages()) {
2700
2701
  if (page.id) existingChromePageIds.add(page.id);
2701
2702
  }
2703
+ chromeAlreadyRunning = true;
2702
2704
  } catch {
2703
2705
  }
2704
2706
  }
@@ -2729,10 +2731,12 @@ async function computerLaunchCommand(app, args) {
2729
2731
  }
2730
2732
  }
2731
2733
  if (readyPage) break;
2732
- try {
2733
- if (child.pid) process.kill(child.pid, 0);
2734
- } catch {
2735
- fail(`Chrome exited before its desktop control channel became ready.`);
2734
+ if (!chromeAlreadyRunning) {
2735
+ try {
2736
+ if (child.pid) process.kill(child.pid, 0);
2737
+ } catch {
2738
+ fail(`Chrome exited before its desktop control channel became ready.`);
2739
+ }
2736
2740
  }
2737
2741
  await sleep(100);
2738
2742
  }
@@ -2921,7 +2925,7 @@ async function computerStatusCommand() {
2921
2925
  }
2922
2926
 
2923
2927
  // src/version.ts
2924
- var COMPUTER_USE_VERSION = "0.2.3";
2928
+ var COMPUTER_USE_VERSION = "0.2.5";
2925
2929
  var COMPUTER_USE_PROTOCOL_VERSION = 1;
2926
2930
 
2927
2931
  // src/program.ts
@@ -3165,6 +3169,7 @@ import { spawn as spawn4 } from "child_process";
3165
3169
  import { createSocket } from "dgram";
3166
3170
  import { createReadStream, existsSync as existsSync6, statSync as statSync2 } from "fs";
3167
3171
  import { createServer as createServer2 } from "http";
3172
+ import { availableParallelism } from "os";
3168
3173
  import { dirname as dirname6, extname, resolve as resolve2 } from "path";
3169
3174
  import { fileURLToPath } from "url";
3170
3175
 
@@ -3357,7 +3362,16 @@ var RtpClock = class {
3357
3362
  base;
3358
3363
  last;
3359
3364
  wraps = 0;
3365
+ origin = 0;
3366
+ emitted = 0;
3367
+ /**
3368
+ * A fresh encoder starts RTP timestamps from a new random base, so the
3369
+ * timeline continues from the last value emitted rather than jumping
3370
+ * backwards — decoders and the viewer's media element both require
3371
+ * monotonic presentation timestamps.
3372
+ */
3360
3373
  reset() {
3374
+ this.origin = this.emitted;
3361
3375
  this.base = void 0;
3362
3376
  this.last = void 0;
3363
3377
  this.wraps = 0;
@@ -3367,7 +3381,8 @@ var RtpClock = class {
3367
3381
  this.last = timestamp;
3368
3382
  const extended = timestamp + this.wraps * 4294967296;
3369
3383
  this.base ??= extended;
3370
- return Math.round((extended - this.base) * 1e6 / this.rate);
3384
+ this.emitted = this.origin + Math.round((extended - this.base) * 1e6 / this.rate);
3385
+ return this.emitted;
3371
3386
  }
3372
3387
  };
3373
3388
  var startCode = new Uint8Array([0, 0, 0, 1]);
@@ -3461,9 +3476,30 @@ var H264RtpAssembler = class {
3461
3476
  import WebSocket2, { WebSocketServer } from "ws";
3462
3477
  var width = Number.parseInt(process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH), 10);
3463
3478
  var height = Number.parseInt(process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT), 10);
3464
- var fps = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_FPS || "60", 10);
3465
- var keyframeInterval = fps;
3479
+ var cores = availableParallelism();
3480
+ var fps = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_FPS || "", 10) || 60;
3481
+ var x264Preset = cores >= 4 ? "superfast" : "ultrafast";
3466
3482
  var port = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_PORT || String(DESKTOP_STREAM_PORT), 10);
3483
+ var configuredTopKbps = (() => {
3484
+ const raw = process.env.REPLICAS_DESKTOP_STREAM_VIDEO_BITRATE?.trim();
3485
+ const match = raw?.match(/^(\d+(?:\.\d+)?)([MmKk]?)$/);
3486
+ if (!match) return 2e4;
3487
+ const value = Number.parseFloat(match[1]);
3488
+ return Math.max(1, Math.round(match[2].toLowerCase() === "m" ? value * 1e3 : value));
3489
+ })();
3490
+ var qualityLadder = [
3491
+ { fps, kbps: configuredTopKbps, crf: 18 },
3492
+ { fps: Math.max(1, Math.round(fps / 2)), kbps: 6e3, crf: 20 },
3493
+ { fps: Math.max(1, Math.round(fps / 2)), kbps: 3e3, crf: 23 },
3494
+ { fps: Math.max(1, Math.round(fps / 4)), kbps: 1200, crf: 26 }
3495
+ ];
3496
+ var keyframeMs = 250;
3497
+ var probeIntervalMs = 250;
3498
+ var targetBacklogMs = 120;
3499
+ var congestedBacklogMs = 250;
3500
+ var congestedDecodeQueue = 3;
3501
+ var fallbackBufferedBytes = 512 * 1024;
3502
+ var maximumBufferedBytes = 4 * 1024 * 1024;
3467
3503
  var display = process.env.REPLICAS_DESKTOP_DISPLAY?.replace(/^([^:])/, ":$1") || ":99";
3468
3504
  var videoCodec = "avc1.42E02A";
3469
3505
  var config = {
@@ -3508,7 +3544,7 @@ function validInput(value) {
3508
3544
  if (!value || typeof value !== "object" || !("type" in value) || typeof value.type !== "string") return false;
3509
3545
  if (value.type === "release") return true;
3510
3546
  if (value.type === "stats") return "videoFps" in value && typeof value.videoFps === "number" && Number.isFinite(value.videoFps) && "audioPacketsPerSecond" in value && typeof value.audioPacketsPerSecond === "number" && Number.isFinite(value.audioPacketsPerSecond) && "decodeQueue" in value && typeof value.decodeQueue === "number" && Number.isFinite(value.decodeQueue) && "rttMs" in value && typeof value.rttMs === "number" && Number.isFinite(value.rttMs);
3511
- if (value.type === "ping") return "sentAt" in value && typeof value.sentAt === "number";
3547
+ if (value.type === "ping" || value.type === "pong") return "sentAt" in value && typeof value.sentAt === "number";
3512
3548
  if (value.type === "key") return "code" in value && typeof value.code === "string" && value.code.length < 40 && "down" in value && typeof value.down === "boolean";
3513
3549
  if (!("x" in value) || !("y" in value) || typeof value.x !== "number" || typeof value.y !== "number") return false;
3514
3550
  if (value.type === "pointer") return true;
@@ -3524,23 +3560,30 @@ async function runMediaServer() {
3524
3560
  const videoAssembler = new H264RtpAssembler();
3525
3561
  const audioClock = new RtpClock(48e3);
3526
3562
  const clients = /* @__PURE__ */ new Map();
3527
- let encoder;
3528
- let encoderRestart;
3529
3563
  let encoderStop;
3564
+ let rung = 0;
3565
+ let descents = 0;
3566
+ let congestedSeconds = 0;
3567
+ let comfortableSeconds = 0;
3530
3568
  let lastVideoAt = 0;
3531
3569
  let lastAudioAt = 0;
3532
3570
  let videoFrames = 0;
3533
3571
  let audioPackets = 0;
3534
3572
  let videoBytes = 0;
3535
3573
  let audioBytes = 0;
3574
+ const queueMs = (state) => {
3575
+ if (!state.probeAnswered) return 0;
3576
+ const outstanding = state.probeSentAt ? Date.now() - state.probeSentAt : 0;
3577
+ return Math.max(0, Math.max(state.probeAckedMs, outstanding) - state.baselineMs);
3578
+ };
3536
3579
  const broadcast = (packet, keyframe = false) => {
3537
3580
  for (const [client, state] of clients) {
3538
3581
  if (client.readyState !== WebSocket2.OPEN) continue;
3539
- if (client.bufferedAmount > 4 * 1024 * 1024) {
3582
+ if (client.bufferedAmount > maximumBufferedBytes) {
3540
3583
  client.close(1013, "stream client is too slow");
3541
3584
  continue;
3542
3585
  }
3543
- if (!keyframe && (state.waitingForKeyframe || client.bufferedAmount > 512 * 1024)) {
3586
+ if (!keyframe && (state.waitingForKeyframe || queueMs(state) > targetBacklogMs || client.bufferedAmount > fallbackBufferedBytes)) {
3544
3587
  state.waitingForKeyframe = true;
3545
3588
  continue;
3546
3589
  }
@@ -3567,14 +3610,78 @@ async function runMediaServer() {
3567
3610
  audioBytes += rtp.payload.byteLength;
3568
3611
  broadcast(mediaPacket(3 /* Audio */, audioClock.microseconds(rtp.timestamp), rtp.payload));
3569
3612
  });
3570
- const startEncoder = () => {
3571
- clearTimeout(encoderStop);
3572
- if (encoder && encoder.exitCode === null) return;
3573
- clearTimeout(encoderRestart);
3613
+ const superviseEncoder = (label, prepare, buildArgs, niceness = 0) => {
3614
+ let child;
3615
+ let retiring;
3616
+ let restart;
3617
+ let intent = 0;
3618
+ const alive = (process2) => Boolean(process2 && process2.exitCode === null && process2.signalCode === null);
3619
+ const start = () => {
3620
+ clearTimeout(encoderStop);
3621
+ if (alive(child)) return;
3622
+ if (retiring) return;
3623
+ clearTimeout(restart);
3624
+ prepare();
3625
+ const argv = niceness > 0 ? ["nice", "-n", String(niceness), "ffmpeg", ...buildArgs()] : ["ffmpeg", ...buildArgs()];
3626
+ const spawned = spawn4(argv[0], argv.slice(1), { env: { ...process.env, DISPLAY: display }, stdio: ["ignore", "ignore", "pipe"] });
3627
+ spawned.stderr?.on("data", (chunk) => process.stderr.write(`[desktop-stream] ${chunk}`));
3628
+ child = spawned;
3629
+ spawned.once("exit", (code, signal) => {
3630
+ if (child !== spawned) return;
3631
+ child = void 0;
3632
+ if (clients.size === 0) return;
3633
+ process.stderr.write(`[desktop-stream] ${label} encoder exited (${signal || code}); restarting
3634
+ `);
3635
+ restart = setTimeout(start, 500);
3636
+ });
3637
+ };
3638
+ return {
3639
+ start,
3640
+ running: () => alive(child),
3641
+ stop: () => {
3642
+ intent += 1;
3643
+ clearTimeout(restart);
3644
+ child?.kill("SIGTERM");
3645
+ retiring?.kill("SIGKILL");
3646
+ child = void 0;
3647
+ retiring = void 0;
3648
+ },
3649
+ replace: () => {
3650
+ const issued = intent += 1;
3651
+ clearTimeout(restart);
3652
+ const previous = child ?? retiring;
3653
+ child = void 0;
3654
+ retiring = void 0;
3655
+ if (!alive(previous)) {
3656
+ start();
3657
+ return;
3658
+ }
3659
+ retiring = previous;
3660
+ let settled = false;
3661
+ const resume = () => {
3662
+ if (settled) return;
3663
+ settled = true;
3664
+ clearTimeout(forceExit);
3665
+ clearTimeout(abandonWait);
3666
+ if (intent !== issued) return;
3667
+ retiring = void 0;
3668
+ if (clients.size === 0) return;
3669
+ start();
3670
+ };
3671
+ const forceExit = setTimeout(() => previous.kill("SIGKILL"), 2e3);
3672
+ const abandonWait = setTimeout(resume, 4e3);
3673
+ previous.once("exit", resume);
3674
+ previous.kill("SIGTERM");
3675
+ }
3676
+ };
3677
+ };
3678
+ const videoEncoder = superviseEncoder("video", () => {
3574
3679
  videoAssembler.reset();
3575
- audioClock.reset();
3576
- const videoBitrate = process.env.REPLICAS_DESKTOP_STREAM_VIDEO_BITRATE || "12M";
3577
- encoder = spawn4("ffmpeg", [
3680
+ for (const state of clients.values()) state.waitingForKeyframe = true;
3681
+ }, () => {
3682
+ const quality = qualityLadder[rung];
3683
+ const keyframeInterval = Math.max(1, Math.round(quality.fps * keyframeMs / 1e3));
3684
+ return [
3578
3685
  "-hide_banner",
3579
3686
  "-loglevel",
3580
3687
  "warning",
@@ -3582,16 +3689,10 @@ async function runMediaServer() {
3582
3689
  "nobuffer",
3583
3690
  "-flags",
3584
3691
  "low_delay",
3585
- "-thread_queue_size",
3586
- "1024",
3587
- "-f",
3588
- "pulse",
3589
- "-fragment_size",
3590
- "2048",
3591
- "-use_wallclock_as_timestamps",
3592
- "1",
3593
- "-i",
3594
- process.env.REPLICAS_DESKTOP_STREAM_AUDIO_SOURCE || "default",
3692
+ "-probesize",
3693
+ "32",
3694
+ "-analyzeduration",
3695
+ "0",
3595
3696
  "-thread_queue_size",
3596
3697
  "1024",
3597
3698
  "-f",
@@ -3599,20 +3700,18 @@ async function runMediaServer() {
3599
3700
  "-draw_mouse",
3600
3701
  "0",
3601
3702
  "-framerate",
3602
- String(fps),
3703
+ String(quality.fps),
3603
3704
  "-video_size",
3604
3705
  `${width}x${height}`,
3605
3706
  "-use_wallclock_as_timestamps",
3606
3707
  "1",
3607
3708
  "-i",
3608
3709
  `${display}.0`,
3609
- "-map",
3610
- "1:v:0",
3611
3710
  "-an",
3612
3711
  "-c:v",
3613
3712
  "libx264",
3614
3713
  "-preset",
3615
- "ultrafast",
3714
+ x264Preset,
3616
3715
  "-tune",
3617
3716
  "zerolatency",
3618
3717
  "-profile:v",
@@ -3630,11 +3729,11 @@ async function runMediaServer() {
3630
3729
  "-sc_threshold",
3631
3730
  "0",
3632
3731
  "-crf",
3633
- "18",
3732
+ String(quality.crf),
3634
3733
  "-maxrate",
3635
- videoBitrate,
3734
+ `${quality.kbps}k`,
3636
3735
  "-bufsize",
3637
- "1M",
3736
+ `${Math.round(quality.kbps * keyframeMs / 1e3)}k`,
3638
3737
  "-x264-params",
3639
3738
  `repeat-headers=1:sliced-threads=1:slices=4:keyint=${keyframeInterval}:min-keyint=${keyframeInterval}:scenecut=0`,
3640
3739
  "-fps_mode",
@@ -3645,57 +3744,111 @@ async function runMediaServer() {
3645
3744
  "rtp",
3646
3745
  "-payload_type",
3647
3746
  "96",
3648
- `rtp://127.0.0.1:${videoPort}?pkt_size=1200&connect=1&rtcpport=${videoPort}`,
3649
- "-map",
3650
- "0:a:0",
3651
- "-vn",
3652
- "-af",
3653
- "aresample=async=1:first_pts=0",
3654
- "-c:a",
3655
- "libopus",
3656
- "-application",
3657
- "lowdelay",
3658
- "-frame_duration",
3659
- "20",
3660
- "-b:a",
3661
- "128k",
3662
- "-ar",
3663
- "48000",
3664
- "-ac",
3665
- "2",
3666
- "-flush_packets",
3667
- "1",
3668
- "-f",
3669
- "rtp",
3670
- "-payload_type",
3671
- "111",
3672
- `rtp://127.0.0.1:${audioPort}?pkt_size=1200&connect=1&rtcpport=${audioPort}`
3673
- ], { env: { ...process.env, DISPLAY: display }, stdio: ["ignore", "ignore", "pipe"] });
3674
- encoder.stderr?.on("data", (chunk) => process.stderr.write(`[desktop-stream] ${chunk}`));
3675
- encoder.once("exit", (code, signal) => {
3676
- encoder = void 0;
3677
- if (clients.size > 0) {
3678
- process.stderr.write(`[desktop-stream] encoder exited (${signal || code}); restarting
3747
+ `rtp://127.0.0.1:${videoPort}?pkt_size=1200&connect=1&rtcpport=${videoPort}`
3748
+ ];
3749
+ }, 10);
3750
+ const audioEncoder = superviseEncoder("audio", () => audioClock.reset(), () => [
3751
+ "-hide_banner",
3752
+ "-loglevel",
3753
+ "warning",
3754
+ "-fflags",
3755
+ "nobuffer",
3756
+ "-flags",
3757
+ "low_delay",
3758
+ "-probesize",
3759
+ "32",
3760
+ "-analyzeduration",
3761
+ "0",
3762
+ "-thread_queue_size",
3763
+ "1024",
3764
+ "-f",
3765
+ "pulse",
3766
+ "-fragment_size",
3767
+ "2048",
3768
+ "-use_wallclock_as_timestamps",
3769
+ "1",
3770
+ "-i",
3771
+ process.env.REPLICAS_DESKTOP_STREAM_AUDIO_SOURCE || "default",
3772
+ "-vn",
3773
+ "-af",
3774
+ "aresample=async=1:first_pts=0",
3775
+ "-c:a",
3776
+ "libopus",
3777
+ "-application",
3778
+ "lowdelay",
3779
+ "-frame_duration",
3780
+ "20",
3781
+ "-b:a",
3782
+ "128k",
3783
+ "-ar",
3784
+ "48000",
3785
+ "-ac",
3786
+ "2",
3787
+ "-flush_packets",
3788
+ "1",
3789
+ "-f",
3790
+ "rtp",
3791
+ "-payload_type",
3792
+ "111",
3793
+ `rtp://127.0.0.1:${audioPort}?pkt_size=1200&connect=1&rtcpport=${audioPort}`
3794
+ ]);
3795
+ const startEncoders = () => {
3796
+ videoEncoder.start();
3797
+ audioEncoder.start();
3798
+ };
3799
+ const setRung = (next) => {
3800
+ if (next === rung) return;
3801
+ if (next > rung) descents += 1;
3802
+ rung = next;
3803
+ congestedSeconds = 0;
3804
+ comfortableSeconds = 0;
3805
+ process.stderr.write(`[desktop-stream] quality rung ${rung} (${qualityLadder[rung].fps} fps, ${qualityLadder[rung].kbps}k)
3679
3806
  `);
3680
- encoderRestart = setTimeout(startEncoder, 500);
3681
- }
3682
- });
3807
+ videoEncoder.replace();
3683
3808
  };
3684
- const stopEncoderSoon = () => {
3809
+ const stopEncodersSoon = () => {
3685
3810
  clearTimeout(encoderStop);
3686
3811
  encoderStop = setTimeout(() => {
3687
3812
  if (clients.size > 0) return;
3688
- encoder?.kill("SIGTERM");
3689
- encoder = void 0;
3813
+ videoEncoder.stop();
3814
+ audioEncoder.stop();
3690
3815
  }, 5e3);
3691
3816
  };
3817
+ const probe = setInterval(() => {
3818
+ for (const [client, state] of clients) {
3819
+ if (client.readyState !== WebSocket2.OPEN || state.probeSentAt) continue;
3820
+ state.probeSentAt = Date.now();
3821
+ client.send(JSON.stringify({ type: "ping", sentAt: state.probeSentAt }));
3822
+ }
3823
+ }, probeIntervalMs);
3824
+ probe.unref();
3825
+ const control = setInterval(() => {
3826
+ const states = [...clients.values()];
3827
+ if (states.length === 0) return;
3828
+ const struggling = states.filter((state) => queueMs(state) > congestedBacklogMs || (state.stats?.decodeQueue ?? 0) > congestedDecodeQueue).length;
3829
+ if (struggling === states.length) {
3830
+ comfortableSeconds = 0;
3831
+ congestedSeconds += 1;
3832
+ if (congestedSeconds >= 2) setRung(Math.min(qualityLadder.length - 1, rung + 1));
3833
+ return;
3834
+ }
3835
+ congestedSeconds = 0;
3836
+ if (struggling > 0) {
3837
+ comfortableSeconds = 0;
3838
+ return;
3839
+ }
3840
+ comfortableSeconds += 1;
3841
+ if (comfortableSeconds >= Math.min(300, 15 * 2 ** descents)) setRung(Math.max(0, rung - 1));
3842
+ }, 1e3);
3843
+ control.unref();
3692
3844
  const httpServer = createServer2((request, response) => {
3693
3845
  const url = new URL(request.url || "/", "http://localhost");
3694
3846
  if (url.pathname === "/health") {
3695
3847
  response.setHeader("content-type", "application/json");
3696
3848
  response.end(JSON.stringify({
3697
3849
  clients: clients.size,
3698
- encoder: Boolean(encoder && encoder.exitCode === null),
3850
+ encoder: videoEncoder.running(),
3851
+ audioEncoder: audioEncoder.running(),
3699
3852
  lastVideoFrameMsAgo: lastVideoAt ? Date.now() - lastVideoAt : null,
3700
3853
  lastAudioPacketMsAgo: lastAudioAt ? Date.now() - lastAudioAt : null,
3701
3854
  videoFrames,
@@ -3703,9 +3856,12 @@ async function runMediaServer() {
3703
3856
  videoBytes,
3704
3857
  audioBytes,
3705
3858
  clientStats: [...clients.values()].flatMap((client) => client.stats ? [client.stats] : []),
3859
+ clientQueueMs: [...clients.values()].map((client) => Math.round(queueMs(client))),
3860
+ clientBaselineMs: [...clients.values()].map((client) => Number.isFinite(client.baselineMs) ? Math.round(client.baselineMs) : null),
3861
+ qualityRung: rung,
3706
3862
  width,
3707
3863
  height,
3708
- fps
3864
+ fps: qualityLadder[rung].fps
3709
3865
  }));
3710
3866
  return;
3711
3867
  }
@@ -3716,7 +3872,11 @@ async function runMediaServer() {
3716
3872
  response.end("Not found");
3717
3873
  return;
3718
3874
  }
3719
- response.setHeader("cache-control", "no-store");
3875
+ if (extname(path) === ".html") {
3876
+ startEncoders();
3877
+ if (clients.size === 0) stopEncodersSoon();
3878
+ }
3879
+ response.setHeader("cache-control", extname(path) === ".html" ? "no-store" : "private, max-age=3600");
3720
3880
  response.setHeader("content-type", contentTypes[extname(path)] || "application/octet-stream");
3721
3881
  createReadStream(path).pipe(response);
3722
3882
  });
@@ -3729,9 +3889,16 @@ async function runMediaServer() {
3729
3889
  webSocketServer.handleUpgrade(request, socket, head, (client) => webSocketServer.emit("connection", client, request));
3730
3890
  });
3731
3891
  webSocketServer.on("connection", (client) => {
3732
- clients.set(client, { waitingForKeyframe: true, input: new InputBridge(display, width, height) });
3892
+ clients.set(client, {
3893
+ waitingForKeyframe: true,
3894
+ input: new InputBridge(display, width, height),
3895
+ probeSentAt: 0,
3896
+ probeAckedMs: 0,
3897
+ probeAnswered: false,
3898
+ baselineMs: Number.POSITIVE_INFINITY
3899
+ });
3733
3900
  client.send(JSON.stringify(config));
3734
- startEncoder();
3901
+ startEncoders();
3735
3902
  client.on("message", (raw, binary) => {
3736
3903
  if (binary) return;
3737
3904
  let message;
@@ -3742,21 +3909,33 @@ async function runMediaServer() {
3742
3909
  }
3743
3910
  if (!validInput(message)) return;
3744
3911
  if (message.type === "ping") client.send(JSON.stringify({ type: "pong", sentAt: message.sentAt }));
3745
- else if (message.type === "stats") {
3912
+ else if (message.type === "pong") {
3913
+ const state = clients.get(client);
3914
+ if (!state || message.sentAt !== state.probeSentAt) return;
3915
+ state.probeAckedMs = Date.now() - state.probeSentAt;
3916
+ state.probeSentAt = 0;
3917
+ state.probeAnswered = true;
3918
+ state.baselineMs = Math.min(state.baselineMs, state.probeAckedMs);
3919
+ } else if (message.type === "stats") {
3746
3920
  const state = clients.get(client);
3747
3921
  if (!state) return;
3748
3922
  state.stats = {
3749
- videoFps: message.videoFps,
3750
- audioPacketsPerSecond: message.audioPacketsPerSecond,
3751
- decodeQueue: message.decodeQueue,
3752
- rttMs: message.rttMs
3923
+ videoFps: clamp(message.videoFps, 0, 1e3),
3924
+ audioPacketsPerSecond: clamp(message.audioPacketsPerSecond, 0, 1e3),
3925
+ decodeQueue: clamp(message.decodeQueue, 0, 1e3),
3926
+ rttMs: clamp(message.rttMs, 0, 6e5)
3753
3927
  };
3754
3928
  } else clients.get(client)?.input.handle(message);
3755
3929
  });
3756
3930
  client.once("close", () => {
3757
3931
  clients.get(client)?.input.close();
3758
3932
  clients.delete(client);
3759
- if (clients.size === 0) stopEncoderSoon();
3933
+ if (clients.size > 0) return;
3934
+ rung = 0;
3935
+ descents = 0;
3936
+ congestedSeconds = 0;
3937
+ comfortableSeconds = 0;
3938
+ stopEncodersSoon();
3760
3939
  });
3761
3940
  });
3762
3941
  await new Promise((resolveServer, reject) => {
@@ -3772,13 +3951,15 @@ async function runMediaServer() {
3772
3951
  const close = () => {
3773
3952
  if (closing) return;
3774
3953
  closing = true;
3775
- clearTimeout(encoderRestart);
3954
+ clearInterval(probe);
3955
+ clearInterval(control);
3776
3956
  clearTimeout(encoderStop);
3777
3957
  for (const [client, state] of clients) {
3778
3958
  state.input.close();
3779
3959
  client.close(1001, "server shutting down");
3780
3960
  }
3781
- encoder?.kill("SIGTERM");
3961
+ videoEncoder.stop();
3962
+ audioEncoder.stop();
3782
3963
  videoSocket.close();
3783
3964
  audioSocket.close();
3784
3965
  webSocketServer.close();
@@ -166,7 +166,7 @@ var handleBinary = (data) => {
166
166
  }
167
167
  if (!videoDecoder || !activeVideoConfig || videoDecoder.state !== "configured") return;
168
168
  const key = kind === 1 /* VideoKey */;
169
- if (videoDecoder.decodeQueueSize > 12) {
169
+ if (videoDecoder.decodeQueueSize > 3) {
170
170
  videoDecoder.reset();
171
171
  videoDecoder.configure(activeVideoConfig);
172
172
  waitingForKeyframe = true;
@@ -205,6 +205,10 @@ var connect = () => {
205
205
  rtt = performance.now() - message.sentAt;
206
206
  return;
207
207
  }
208
+ if (message.type === "ping") {
209
+ send({ type: "pong", sentAt: message.sentAt });
210
+ return;
211
+ }
208
212
  if (message.type !== "config" || message.protocolVersion !== STREAM_PROTOCOL_VERSION) return;
209
213
  config = message;
210
214
  fitScreen();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-computer",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Replicas workspace computer-use runtime",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",