replicas-computer 0.3.13 → 0.3.14

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/README.md CHANGED
@@ -17,7 +17,7 @@ The versioned computer-use runtime for Replicas workspace images.
17
17
  - `xsettingsd` broadcasts theme changes to running GTK apps and Chrome. Chrome follows the dashboard theme without reopening tabs; launch it without a `GTK_THEME` override.
18
18
  - `assets/desktop/` owns the desktop theme: `brand-wallpaper.svg` (regenerate with `bun scripts/render-wallpaper.ts`), `tint2rc` dock, `openbox-theme/`, `gtk-settings.ini`, the `replicas-terminal` wrapper, `themes/` palettes that `start-services.sh` renders into those templates (`--theme light|dark` switches a running desktop), and the bundled fonts (static TTF instances of General Sans and Geist Mono; variable and WOFF2 builds render with wrong metrics in Pango and VTE).
19
19
 
20
- The viewer and agent command paths are independent. One on-demand encoder pair is shared by all viewers, stops when the last viewer disconnects, and restarts without reloading the page.
20
+ The viewer and agent command paths are independent. One on-demand encoder pair is shared by all viewers, stops when the last viewer disconnects, and restarts without reloading the page. A viewer out of sight (the dashboard keeps one connected while the Desktop tab is hidden) reports `standby`, detected with an `IntersectionObserver` because a hidden iframe keeps its last layout size; while every viewer is in standby the encoder runs at 5 fps on the 1.5 Mbps rung, and the first viewer to become visible swaps it back to full rate without a gap.
21
21
 
22
22
  Chrome disables GPU compositing when no `/dev/dri/renderD*` device is available. This avoids Mesa's CPU-rendered compositor overhead before capture while retaining WebGL support. Desktops with a hardware render device keep Chrome's normal compositing path.
23
23
 
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.13";
2940
+ var COMPUTER_USE_VERSION = "0.3.14";
2941
2941
  var COMPUTER_USE_PROTOCOL_VERSION = 1;
2942
2942
 
2943
2943
  // src/program.ts
@@ -3628,6 +3628,8 @@ function validInput(value) {
3628
3628
  return true;
3629
3629
  case "theme":
3630
3630
  return value.theme === "light" || value.theme === "dark";
3631
+ case "standby":
3632
+ return typeof value.standby === "boolean";
3631
3633
  case "ping":
3632
3634
  case "pong":
3633
3635
  return finite(value.sentAt);
@@ -4069,7 +4071,7 @@ var WebRtcViewer = class {
4069
4071
  // src/video-encoder.ts
4070
4072
  var captureBinary = resolve2(dirname6(fileURLToPath(import.meta.url)), "..", "dist", "replicas-capture");
4071
4073
  function superviseVideoEncoder(options) {
4072
- const { display: display2, width: width2, height: height2, fps: fps2, colorMode: colorMode2 } = options;
4074
+ const { display: display2, width: width2, height: height2, colorMode: colorMode2 } = options;
4073
4075
  const env = { ...process.env, DISPLAY: display2 };
4074
4076
  let nativeCapture = process.env.REPLICAS_DESKTOP_STREAM_CAPTURE !== "ffmpeg" && existsSync6(captureBinary);
4075
4077
  let captureMetrics;
@@ -4098,7 +4100,7 @@ function superviseVideoEncoder(options) {
4098
4100
  onPacket: options.onPacket,
4099
4101
  joinable: (packet) => isKeyframeStart(rtpPayload(packet)),
4100
4102
  args: (udpPort, warmupFrames) => {
4101
- const { quality, keyframeMs, profile: videoProfile, intraRefresh, pixelFormat, chromaQpOffset, preset } = options.params();
4103
+ const { quality, keyframeMs, fps: fps2, profile: videoProfile, intraRefresh, pixelFormat, chromaQpOffset, preset } = options.params();
4102
4104
  const keyframeInterval = Math.max(1, Math.round(fps2 * keyframeMs / 1e3));
4103
4105
  if (nativeCapture) return [
4104
4106
  "--width",
@@ -4445,6 +4447,8 @@ var width = Number.parseInt(process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP
4445
4447
  var height = Number.parseInt(process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT), 10);
4446
4448
  var basePreset = availableParallelism2() >= 4 ? "superfast" : "ultrafast";
4447
4449
  var fps = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_FPS || "", 10) || 60;
4450
+ var standbyFps = 5;
4451
+ var standbyRung = 5;
4448
4452
  var port = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_PORT || String(DESKTOP_STREAM_PORT), 10);
4449
4453
  var display = process.env.REPLICAS_DESKTOP_DISPLAY?.replace(/^([^:])/, ":$1") || ":99";
4450
4454
  var webrtcEnabled = process.env.REPLICAS_DESKTOP_STREAM_WEBRTC !== "0";
@@ -4601,8 +4605,11 @@ async function runMediaServer() {
4601
4605
  if (accepted.length === 0) return "main";
4602
4606
  return PROFILE_ORDER.find((candidate) => (!requires420 || candidate !== "high444") && accepted.every((set) => set.has(candidate))) ?? "baseline";
4603
4607
  };
4608
+ const allStandby = () => viewers.size > 0 && [...viewers.values()].every((viewer) => viewer.standby);
4609
+ const streamFps = () => allStandby() ? Math.min(fps, standbyFps) : fps;
4604
4610
  const encoderParams = () => {
4605
- const quality = ladder()[rung];
4611
+ const quality = ladder()[allStandby() ? Math.max(rung, standbyRung) : rung];
4612
+ const encodeFps = streamFps();
4606
4613
  const sockets = socketViewers().length > 0;
4607
4614
  const keyframeMs = sockets ? Math.min(quality.keyframeMs, fallbackKeyframeMs) : quality.keyframeMs;
4608
4615
  const videoProfile = profile();
@@ -4615,8 +4622,9 @@ async function runMediaServer() {
4615
4622
  keyframeMs,
4616
4623
  intraRefresh,
4617
4624
  pixelFormat,
4625
+ fps: encodeFps,
4618
4626
  chromaQpOffset: profiles[activeProfile].chromaQpOffset,
4619
- signature: `rung ${rung} \xB7 ${activeProfile} crf ${quality.crf} \xB7 ${preset} \xB7 ${videoProfile} \xB7 keyframe ${keyframeMs}ms${intraRefresh ? " refresh" : ""}`
4627
+ signature: `rung ${rung} \xB7 ${activeProfile} crf ${quality.crf} \xB7 ${preset} \xB7 ${videoProfile} \xB7 keyframe ${keyframeMs}ms${intraRefresh ? " refresh" : ""} \xB7 ${encodeFps} fps`
4620
4628
  };
4621
4629
  };
4622
4630
  const budgetBps = () => ladder()[rung].kbps * 1e3 * budgetFactor;
@@ -4663,7 +4671,7 @@ async function runMediaServer() {
4663
4671
  }
4664
4672
  }
4665
4673
  const last = Boolean(packet[1] & 128);
4666
- videoSplicer.splice(packet, generation, Math.round(9e4 / fps));
4674
+ videoSplicer.splice(packet, generation, Math.round(9e4 / streamFps()));
4667
4675
  let rtp;
4668
4676
  let sockets = false;
4669
4677
  for (const viewer of viewers.values()) {
@@ -4699,13 +4707,12 @@ async function runMediaServer() {
4699
4707
  display,
4700
4708
  width,
4701
4709
  height,
4702
- fps,
4703
4710
  colorMode,
4704
4711
  params: encoderParams,
4705
4712
  onPacket: onVideoPacket,
4706
4713
  record
4707
4714
  });
4708
- const expectedFps = () => videoEncoder.capture().captureBackend === "native" ? Math.max(1, Math.min(fps, encoderFps)) : fps;
4715
+ const expectedFps = () => videoEncoder.capture().captureBackend === "native" ? Math.max(1, Math.min(streamFps(), encoderFps)) : streamFps();
4709
4716
  const audioEncoder = superviseEncoder({
4710
4717
  label: "audio",
4711
4718
  log: (message) => record("audio encoder", message),
@@ -4876,7 +4883,7 @@ async function runMediaServer() {
4876
4883
  if (viewers.size === 0) return;
4877
4884
  const { captureBackend, captureMetrics } = videoEncoder.capture();
4878
4885
  const captureBusy = captureBackend !== "native" || !captureMetrics || captureMetrics.busyMs / captureMetrics.elapsedMs > 0.8;
4879
- if (videoEncoder.running() && encoderFps < fps * 0.9 && captureBusy) {
4886
+ if (videoEncoder.running() && encoderFps < streamFps() * 0.9 && captureBusy) {
4880
4887
  cleanTransportSeconds = 0;
4881
4888
  congestedSeconds = 0;
4882
4889
  comfortableSeconds = 0;
@@ -5143,6 +5150,12 @@ async function runMediaServer() {
5143
5150
  case "theme":
5144
5151
  applyDesktopTheme(message.theme);
5145
5152
  return;
5153
+ case "standby":
5154
+ if (viewer.standby === message.standby) return;
5155
+ viewer.standby = message.standby;
5156
+ record(message.standby ? "viewer standby" : "viewer active", `${[...viewers.values()].filter((v) => v.standby).length} of ${viewers.size} in standby`);
5157
+ syncEncoder();
5158
+ return;
5146
5159
  default:
5147
5160
  viewer.input.handle(message);
5148
5161
  }
@@ -5185,6 +5198,8 @@ async function runMediaServer() {
5185
5198
  audioBytes,
5186
5199
  videoKbps,
5187
5200
  encoderFps,
5201
+ streamFps: streamFps(),
5202
+ standbyViewers: all.filter((viewer) => viewer.standby).length,
5188
5203
  ...videoEncoder.capture(),
5189
5204
  cpu,
5190
5205
  clientStats: all.flatMap((viewer) => viewer.stats ? [viewer.stats] : []),
@@ -5238,13 +5253,14 @@ async function runMediaServer() {
5238
5253
  socket.destroy();
5239
5254
  return;
5240
5255
  }
5241
- webSocketServer.handleUpgrade(request, socket, head, (client) => webSocketServer.emit("connection", client, parseStreamRequest(url.searchParams, defaultRequest)));
5256
+ webSocketServer.handleUpgrade(request, socket, head, (client) => webSocketServer.emit("connection", client, parseStreamRequest(url.searchParams, defaultRequest), url.searchParams.get("standby") === "1"));
5242
5257
  });
5243
- webSocketServer.on("connection", (socket, request) => {
5258
+ webSocketServer.on("connection", (socket, request, standby) => {
5244
5259
  const viewer = {
5245
5260
  socket,
5246
5261
  input: new InputBridge(display, width, height),
5247
5262
  request,
5263
+ standby,
5248
5264
  fallback: false,
5249
5265
  waitingForKeyframe: true,
5250
5266
  withheldFrames: 0,
@@ -5269,7 +5285,7 @@ async function runMediaServer() {
5269
5285
  if (!adaptive) rung = 0;
5270
5286
  }
5271
5287
  viewers.set(socket, viewer);
5272
- record("viewer connected", `${request.transport} requested, ${viewers.size} connected`);
5288
+ record("viewer connected", `${request.transport} requested${standby ? " in standby" : ""}, ${viewers.size} connected`);
5273
5289
  const reply = (message) => {
5274
5290
  if (socket.readyState === WebSocket2.OPEN) socket.send(JSON.stringify(message));
5275
5291
  };
@@ -110,6 +110,8 @@ try {
110
110
  } catch {
111
111
  }
112
112
  var socket;
113
+ var standby = innerWidth === 0 || innerHeight === 0;
114
+ var standbyTimer;
113
115
  var config;
114
116
  var transport;
115
117
  var transportReason = "";
@@ -646,6 +648,7 @@ var connect = () => {
646
648
  socket?.close();
647
649
  showStatus(reconnectAttempt ? "reconnecting\u2026" : "connecting\u2026", true);
648
650
  const query = new URLSearchParams([...params].filter(([key]) => ["transport", "profile", "adapt", "ice"].includes(key)));
651
+ if (standby) query.set("standby", "1");
649
652
  const nextSocket = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/stream?${query}`);
650
653
  nextSocket.binaryType = "arraybuffer";
651
654
  socket = nextSocket;
@@ -749,6 +752,21 @@ window.addEventListener("keyup", (event) => {
749
752
  }, { capture: true });
750
753
  window.addEventListener("blur", () => send({ type: "release" }));
751
754
  window.addEventListener("resize", fitScreen);
755
+ var setStandby = (next) => {
756
+ clearTimeout(standbyTimer);
757
+ if (next === standby) return;
758
+ standby = next;
759
+ note(standby ? "standby" : "active");
760
+ sendSocket({ type: "standby", standby });
761
+ if (!standby && !audioPrompt.hidden) unlockAudio();
762
+ };
763
+ new IntersectionObserver(([entry]) => {
764
+ if (entry.isIntersecting) setStandby(false);
765
+ else if (!standby) {
766
+ clearTimeout(standbyTimer);
767
+ standbyTimer = setTimeout(() => setStandby(true), 1e3);
768
+ }
769
+ }).observe(document.documentElement);
752
770
  window.addEventListener("online", connect);
753
771
  document.addEventListener("visibilitychange", () => {
754
772
  if (document.visibilityState === "hidden") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-computer",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "description": "Replicas workspace computer-use runtime",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",