replicas-computer 0.3.7 → 0.3.9

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
@@ -176,7 +176,7 @@ function configuredDesktopDimensions() {
176
176
  function clamp(n, min, max) {
177
177
  return Math.min(max, Math.max(min, n));
178
178
  }
179
- var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
179
+ var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
180
180
 
181
181
  // src/program.ts
182
182
  import { format } from "util";
@@ -1152,14 +1152,14 @@ async function openChromeSession(webSocketDebuggerUrl) {
1152
1152
  });
1153
1153
  socket.addEventListener("error", () => fail2(new Error("Chrome DevTools WebSocket failed")));
1154
1154
  socket.addEventListener("close", () => fail2(new Error("Chrome DevTools WebSocket closed unexpectedly")));
1155
- await new Promise((resolve4, reject) => {
1155
+ await new Promise((resolve5, reject) => {
1156
1156
  const timeout = setTimeout(() => {
1157
1157
  socket.close();
1158
1158
  reject(new Error("Chrome DevTools connection timed out"));
1159
1159
  }, 1e4);
1160
1160
  socket.addEventListener("open", () => {
1161
1161
  clearTimeout(timeout);
1162
- resolve4();
1162
+ resolve5();
1163
1163
  }, { once: true });
1164
1164
  socket.addEventListener("error", () => {
1165
1165
  clearTimeout(timeout);
@@ -1167,7 +1167,7 @@ async function openChromeSession(webSocketDebuggerUrl) {
1167
1167
  }, { once: true });
1168
1168
  });
1169
1169
  return {
1170
- send: ({ method, params }) => new Promise((resolve4, reject) => {
1170
+ send: ({ method, params }) => new Promise((resolve5, reject) => {
1171
1171
  if (socket.readyState !== WebSocket.OPEN) {
1172
1172
  reject(new Error("Chrome DevTools connection is not open"));
1173
1173
  return;
@@ -1177,7 +1177,7 @@ async function openChromeSession(webSocketDebuggerUrl) {
1177
1177
  pending.delete(id);
1178
1178
  reject(new Error(`Chrome DevTools ${method} timed out`));
1179
1179
  }, 1e4);
1180
- pending.set(id, { resolve: resolve4, reject, timeout });
1180
+ pending.set(id, { resolve: resolve5, reject, timeout });
1181
1181
  socket.send(JSON.stringify({ id, method, params }));
1182
1182
  }),
1183
1183
  close: () => fail2(new Error("Chrome DevTools connection closed"))
@@ -2897,7 +2897,7 @@ async function waitForDesktopViewerUrl(timeoutMs) {
2897
2897
  while (Date.now() < deadline) {
2898
2898
  const url = await lookupDesktopViewerUrl();
2899
2899
  if (url) return url;
2900
- await new Promise((resolve4) => setTimeout(resolve4, INFO_WAIT_INTERVAL_MS));
2900
+ await new Promise((resolve5) => setTimeout(resolve5, INFO_WAIT_INTERVAL_MS));
2901
2901
  }
2902
2902
  return await lookupDesktopViewerUrl();
2903
2903
  }
@@ -2937,7 +2937,7 @@ async function computerStatusCommand() {
2937
2937
  }
2938
2938
 
2939
2939
  // src/version.ts
2940
- var COMPUTER_USE_VERSION = "0.3.7";
2940
+ var COMPUTER_USE_VERSION = "0.3.9";
2941
2941
  var COMPUTER_USE_PROTOCOL_VERSION = 1;
2942
2942
 
2943
2943
  // src/program.ts
@@ -3026,15 +3026,15 @@ function writeResult(socket, result) {
3026
3026
  }
3027
3027
  async function hasSocketListener() {
3028
3028
  if (!existsSync5(COMPUTER_USE_SOCKET_PATH)) return false;
3029
- return await new Promise((resolve4) => {
3029
+ return await new Promise((resolve5) => {
3030
3030
  const socket = createConnection(COMPUTER_USE_SOCKET_PATH);
3031
3031
  socket.setTimeout(500, () => socket.destroy());
3032
3032
  socket.once("connect", () => {
3033
3033
  socket.destroy();
3034
- resolve4(true);
3034
+ resolve5(true);
3035
3035
  });
3036
- socket.once("error", () => resolve4(false));
3037
- socket.once("close", () => resolve4(false));
3036
+ socket.once("error", () => resolve5(false));
3037
+ socket.once("close", () => resolve5(false));
3038
3038
  });
3039
3039
  }
3040
3040
  async function runComputerDaemon() {
@@ -3088,13 +3088,13 @@ async function runComputerDaemon() {
3088
3088
  });
3089
3089
  });
3090
3090
  });
3091
- await new Promise((resolve4, reject) => {
3091
+ await new Promise((resolve5, reject) => {
3092
3092
  server.once("error", reject);
3093
3093
  server.listen(COMPUTER_USE_SOCKET_PATH, () => {
3094
3094
  server.off("error", reject);
3095
3095
  chmodSync(COMPUTER_USE_SOCKET_PATH, 384);
3096
3096
  console.log(`replicas-computer ${COMPUTER_USE_VERSION} listening on ${COMPUTER_USE_SOCKET_PATH}`);
3097
- resolve4();
3097
+ resolve5();
3098
3098
  });
3099
3099
  });
3100
3100
  const healthTimer = setInterval(() => {
@@ -3105,13 +3105,13 @@ async function runComputerDaemon() {
3105
3105
  }
3106
3106
  }, 5e3);
3107
3107
  healthTimer.unref();
3108
- await new Promise((resolve4) => {
3108
+ await new Promise((resolve5) => {
3109
3109
  let closing = false;
3110
3110
  const close = () => {
3111
3111
  if (closing) return;
3112
3112
  closing = true;
3113
3113
  clearInterval(healthTimer);
3114
- server.close(() => resolve4());
3114
+ server.close(() => resolve5());
3115
3115
  };
3116
3116
  process.once("SIGTERM", close);
3117
3117
  process.once("SIGINT", close);
@@ -3125,7 +3125,7 @@ var DAEMON_STARTUP_TIMEOUT_MS = 5e3;
3125
3125
  var ComputerDaemonUnavailableError = class extends Error {
3126
3126
  };
3127
3127
  function sendComputerRequest(argv) {
3128
- return new Promise((resolve4, reject) => {
3128
+ return new Promise((resolve5, reject) => {
3129
3129
  const socket = createConnection2(COMPUTER_USE_SOCKET_PATH);
3130
3130
  let response = "";
3131
3131
  let settled = false;
@@ -3133,7 +3133,7 @@ function sendComputerRequest(argv) {
3133
3133
  if (settled) return;
3134
3134
  settled = true;
3135
3135
  if (error) reject(error);
3136
- else if (result) resolve4(result);
3136
+ else if (result) resolve5(result);
3137
3137
  };
3138
3138
  socket.setEncoding("utf8");
3139
3139
  socket.setTimeout(10 * 60 * 1e3, () => socket.destroy(new Error("Computer-use command timed out")));
@@ -3171,7 +3171,7 @@ async function runComputerClient(argv) {
3171
3171
  return await sendComputerRequest(argv);
3172
3172
  } catch (error) {
3173
3173
  if (!(error instanceof ComputerDaemonUnavailableError) || Date.now() >= deadline) throw error;
3174
- await new Promise((resolve4) => setTimeout(resolve4, 100));
3174
+ await new Promise((resolve5) => setTimeout(resolve5, 100));
3175
3175
  }
3176
3176
  }
3177
3177
  }
@@ -3179,11 +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 existsSync6, readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
3182
+ import { createReadStream, existsSync as existsSync7, readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
3183
3183
  import { createServer as createServer2 } from "http";
3184
- import { availableParallelism, loadavg } from "os";
3185
- import { dirname as dirname6, extname, resolve as resolve2 } from "path";
3186
- import { fileURLToPath } from "url";
3184
+ import { availableParallelism as availableParallelism2, loadavg } from "os";
3185
+ import { dirname as dirname7, extname, resolve as resolve3 } from "path";
3186
+ import { fileURLToPath as fileURLToPath2 } from "url";
3187
3187
 
3188
3188
  // ../shared/src/runtime-env.ts
3189
3189
  function parsePosixEnvFile(content) {
@@ -3247,6 +3247,7 @@ import WebSocket2, { WebSocketServer } from "ws";
3247
3247
  // src/encoder.ts
3248
3248
  import { spawn as spawn3 } from "child_process";
3249
3249
  import { createSocket } from "dgram";
3250
+ import { createInterface } from "readline";
3250
3251
 
3251
3252
  // src/rtp.ts
3252
3253
  var RTP_HEADER_BYTES = 12;
@@ -3449,9 +3450,9 @@ function superviseEncoder(options) {
3449
3450
  const log = options.log ?? ((message) => process.stderr.write(`[desktop-stream] ${options.label} ${message}
3450
3451
  `));
3451
3452
  const retire = (generation) => {
3452
- if (!generation) return;
3453
+ if (!generation?.live) return;
3453
3454
  generation.live = false;
3454
- if (!alive(generation.child)) {
3455
+ if (!generation.child.pid || !alive(generation.child)) {
3455
3456
  generation.socket.close();
3456
3457
  return;
3457
3458
  }
@@ -3463,13 +3464,18 @@ function superviseEncoder(options) {
3463
3464
  generation.socket.close();
3464
3465
  });
3465
3466
  };
3466
- const spawnGeneration = async () => {
3467
+ const spawnCandidate = async () => {
3467
3468
  const socket = await bindUdp();
3468
3469
  const warmupFrames = active ? options.warmupFrames ?? 0 : 0;
3470
+ const command = options.command?.() ?? "ffmpeg";
3469
3471
  const args = options.args(socket.address().port, warmupFrames);
3470
- const child = spawn3("ffmpeg", args, { env: options.env, stdio: ["ignore", "ignore", "pipe"] });
3472
+ const child = spawn3(command, args, { env: options.env, stdio: ["ignore", options.onOutput ? "pipe" : "ignore", "pipe"] });
3471
3473
  child.stderr?.on("data", (chunk) => process.stderr.write(`[desktop-stream] ${chunk}`));
3472
3474
  const generation = { id: ++nextId, child, socket, live: true, startedAt: Date.now(), warmupFrames };
3475
+ candidate = generation;
3476
+ if (child.stdout) createInterface({ input: child.stdout }).on("line", (line) => {
3477
+ if (generation === active && generation.live) options.onOutput?.(line);
3478
+ });
3473
3479
  socket.on("message", (data) => {
3474
3480
  if (!generation.live || !isRtp(data)) return;
3475
3481
  if (generation === candidate) {
@@ -3485,23 +3491,25 @@ function superviseEncoder(options) {
3485
3491
  } else if (generation !== active) return;
3486
3492
  options.onPacket(data, generation.id);
3487
3493
  });
3488
- child.once("exit", (code, signal) => {
3494
+ const failed = (reason) => {
3495
+ if (!generation.live) return;
3489
3496
  if (generation === candidate) {
3490
3497
  clearTimeout(candidateDeadline);
3491
3498
  candidate = void 0;
3492
- retire(generation);
3493
- log(`replacement exited (${signal || code}); keeping the current encoder`);
3494
- if (!active && wanted) restart = setTimeout(() => void start(), 500);
3495
- return;
3496
3499
  }
3497
- if (generation !== active) return;
3500
+ if (generation === active) active = void 0;
3498
3501
  retire(generation);
3499
- active = void 0;
3502
+ options.onFailure?.(command);
3503
+ log(`encoder failed (${reason})${active ? "; keeping the current encoder" : ""}`);
3500
3504
  if (!wanted) return;
3501
- log(`encoder exited (${signal || code}); restarting`);
3502
- restart = setTimeout(() => void start(), 500);
3503
- });
3504
- return generation;
3505
+ if (!active || (options.command?.() ?? "ffmpeg") !== command) {
3506
+ clearTimeout(restart);
3507
+ restart = setTimeout(() => void (active ? replace() : start()), 500);
3508
+ }
3509
+ };
3510
+ child.once("exit", (code, signal) => failed(String(signal || code)));
3511
+ child.once("error", (error) => failed(error.message));
3512
+ candidateDeadline = setTimeout(() => failed("no joinable packet within 4s"), 4e3);
3505
3513
  };
3506
3514
  const enqueue = (task) => {
3507
3515
  chain = chain.then(task).catch((error) => {
@@ -3513,25 +3521,14 @@ function superviseEncoder(options) {
3513
3521
  wanted = true;
3514
3522
  clearTimeout(restart);
3515
3523
  if (active || candidate) return;
3516
- active = await spawnGeneration();
3524
+ await spawnCandidate();
3517
3525
  });
3518
3526
  const replace = () => enqueue(async () => {
3519
3527
  if (!wanted) return;
3520
- if (!active) {
3521
- active = await spawnGeneration();
3522
- return;
3523
- }
3528
+ clearTimeout(restart);
3524
3529
  clearTimeout(candidateDeadline);
3525
3530
  retire(candidate);
3526
- const started = await spawnGeneration();
3527
- candidate = started;
3528
- candidateDeadline = setTimeout(() => {
3529
- if (candidate !== started) return;
3530
- candidate = void 0;
3531
- retire(started);
3532
- log("replacement produced no keyframe; keeping the current encoder");
3533
- if (!active && wanted) restart = setTimeout(() => void start(), 500);
3534
- }, 4e3);
3531
+ await spawnCandidate();
3535
3532
  });
3536
3533
  const stop = () => enqueue(async () => {
3537
3534
  wanted = false;
@@ -3550,210 +3547,16 @@ function superviseEncoder(options) {
3550
3547
  replacing: () => Boolean(candidate),
3551
3548
  generation: () => active?.id ?? 0,
3552
3549
  pid: () => active?.child.pid ?? 0,
3550
+ command: () => active?.child.spawnfile,
3553
3551
  activeForMs: () => active ? Date.now() - active.startedAt : Number.POSITIVE_INFINITY
3554
3552
  };
3555
3553
  }
3556
3554
 
3557
- // src/input-bridge.ts
3558
- import { spawn as spawn4 } from "child_process";
3559
- var scrollPixelsPerStep = 80;
3560
- var namedKeys = {
3561
- AltLeft: "Alt_L",
3562
- AltRight: "Alt_R",
3563
- ArrowDown: "Down",
3564
- ArrowLeft: "Left",
3565
- ArrowRight: "Right",
3566
- ArrowUp: "Up",
3567
- Backquote: "grave",
3568
- Backslash: "backslash",
3569
- Backspace: "BackSpace",
3570
- BracketLeft: "bracketleft",
3571
- BracketRight: "bracketright",
3572
- Comma: "comma",
3573
- ContextMenu: "Menu",
3574
- ControlLeft: "Control_L",
3575
- ControlRight: "Control_R",
3576
- Delete: "Delete",
3577
- End: "End",
3578
- Enter: "Return",
3579
- Equal: "equal",
3580
- Escape: "Escape",
3581
- Home: "Home",
3582
- Insert: "Insert",
3583
- MetaLeft: "Super_L",
3584
- MetaRight: "Super_R",
3585
- Minus: "minus",
3586
- PageDown: "Page_Down",
3587
- PageUp: "Page_Up",
3588
- Period: "period",
3589
- Quote: "apostrophe",
3590
- Semicolon: "semicolon",
3591
- ShiftLeft: "Shift_L",
3592
- ShiftRight: "Shift_R",
3593
- Slash: "slash",
3594
- Space: "space",
3595
- Tab: "Tab"
3596
- };
3597
- function xKey(code) {
3598
- if (/^Key[A-Z]$/.test(code)) return code.slice(-1).toLowerCase();
3599
- if (/^Digit[0-9]$/.test(code)) return code.slice(-1);
3600
- if (/^F(?:[1-9]|1[0-9]|2[0-4])$/.test(code)) return code;
3601
- if (/^Numpad[0-9]$/.test(code)) return `KP_${code.slice(-1)}`;
3602
- return namedKeys[code];
3603
- }
3604
- var InputBridge = class {
3605
- constructor(display2, width2, height2) {
3606
- this.display = display2;
3607
- this.width = width2;
3608
- this.height = height2;
3609
- }
3610
- display;
3611
- width;
3612
- height;
3613
- process;
3614
- pointer;
3615
- closed = false;
3616
- pointerFlush;
3617
- positionSequence = -1;
3618
- actionId = 0;
3619
- scrollRemainder = 0;
3620
- scrollButton = 0;
3621
- latestPosition;
3622
- pressedButtons = /* @__PURE__ */ new Set();
3623
- pressedKeys = /* @__PURE__ */ new Set();
3624
- close() {
3625
- if (this.closed) return;
3626
- this.closed = true;
3627
- this.release();
3628
- const child = this.process;
3629
- this.process = void 0;
3630
- if (!child) return;
3631
- child.stdin.end();
3632
- const timeout = setTimeout(() => child.kill("SIGKILL"), 1e3);
3633
- timeout.unref();
3634
- child.once("exit", () => clearTimeout(timeout));
3635
- }
3636
- handle(message) {
3637
- if (this.closed) return;
3638
- if ("actionId" in message && message.actionId !== void 0) {
3639
- if (message.actionId !== this.actionId + 1) return;
3640
- this.actionId = message.actionId;
3641
- }
3642
- const sequence = "sequence" in message ? message.sequence : void 0;
3643
- const stale = sequence !== void 0 && sequence <= this.positionSequence;
3644
- if (stale && message.type === "pointer") return;
3645
- if (sequence !== void 0 && !stale) {
3646
- this.positionSequence = sequence;
3647
- if ("x" in message) this.latestPosition = this.coordinates(message.x, message.y);
3648
- }
3649
- this.apply(message);
3650
- if (stale && (message.type === "button" || message.type === "wheel" || message.type === "release") && this.latestPosition) {
3651
- this.pointer = this.latestPosition;
3652
- this.flushPointer(true);
3653
- }
3654
- }
3655
- apply(message) {
3656
- if (message.type === "pointer") {
3657
- this.pointer = this.coordinates(message.x, message.y);
3658
- this.pointerFlush ??= setImmediate(() => this.flushPointer());
3659
- return;
3660
- }
3661
- if (message.type === "button") {
3662
- this.pointer = this.coordinates(message.x, message.y);
3663
- this.flushPointer(true);
3664
- const button = message.button === 0 ? 1 : message.button === 1 ? 2 : message.button === 2 ? 3 : 0;
3665
- if (!button) return;
3666
- if (message.down) {
3667
- if (this.pressedButtons.has(button)) return;
3668
- this.pressedButtons.add(button);
3669
- this.write(`mousedown ${button}`);
3670
- } else {
3671
- this.pressedButtons.delete(button);
3672
- this.write(`mouseup ${button}`);
3673
- }
3674
- return;
3675
- }
3676
- if (message.type === "wheel") {
3677
- this.pointer = this.coordinates(message.x, message.y);
3678
- this.flushPointer(true);
3679
- const vertical = Math.abs(message.deltaY) >= Math.abs(message.deltaX);
3680
- const delta = vertical ? message.deltaY : message.deltaX;
3681
- if (!delta) return;
3682
- const button = vertical ? delta < 0 ? 4 : 5 : delta < 0 ? 6 : 7;
3683
- if (button !== this.scrollButton) this.scrollRemainder = 0;
3684
- this.scrollButton = button;
3685
- const distance = Math.abs(delta);
3686
- let steps;
3687
- if (distance >= scrollPixelsPerStep) {
3688
- steps = Math.min(4, Math.round(distance / scrollPixelsPerStep));
3689
- this.scrollRemainder = 0;
3690
- } else {
3691
- this.scrollRemainder += distance;
3692
- steps = Math.floor(this.scrollRemainder / scrollPixelsPerStep);
3693
- this.scrollRemainder %= scrollPixelsPerStep;
3694
- }
3695
- for (let index = 0; index < steps; index += 1) this.write(`mouseclick ${button}`);
3696
- return;
3697
- }
3698
- if (message.type === "key") {
3699
- const key = xKey(message.code);
3700
- if (!key || message.repeat) return;
3701
- if (message.down) {
3702
- if (this.pressedKeys.has(key)) return;
3703
- this.pressedKeys.add(key);
3704
- this.write(`keydown ${key}`);
3705
- } else {
3706
- this.pressedKeys.delete(key);
3707
- this.write(`keyup ${key}`);
3708
- }
3709
- return;
3710
- }
3711
- if (message.type === "release") this.release();
3712
- }
3713
- release() {
3714
- if (this.pointerFlush) clearImmediate(this.pointerFlush);
3715
- this.pointerFlush = void 0;
3716
- this.pointer = void 0;
3717
- this.scrollRemainder = 0;
3718
- this.scrollButton = 0;
3719
- for (const button of this.pressedButtons) this.write(`mouseup ${button}`);
3720
- for (const key of this.pressedKeys) this.write(`keyup ${key}`);
3721
- this.pressedButtons.clear();
3722
- this.pressedKeys.clear();
3723
- }
3724
- coordinates(x, y) {
3725
- return {
3726
- x: Math.round(Math.min(1, Math.max(0, x)) * (this.width - 1)),
3727
- y: Math.round(Math.min(1, Math.max(0, y)) * (this.height - 1))
3728
- };
3729
- }
3730
- flushPointer(force = false) {
3731
- if (this.pointerFlush) clearImmediate(this.pointerFlush);
3732
- this.pointerFlush = void 0;
3733
- if (!this.pointer || !force && this.process?.stdin.writableNeedDrain) return;
3734
- this.write(`mousemove ${this.pointer.x} ${this.pointer.y}`);
3735
- this.pointer = void 0;
3736
- }
3737
- write(command) {
3738
- if (!this.process || this.process.exitCode !== null || this.process.stdin.destroyed) {
3739
- const child = spawn4("xte", [], {
3740
- env: { ...process.env, DISPLAY: this.display },
3741
- stdio: ["pipe", "pipe", "pipe"]
3742
- });
3743
- child.stdout.resume();
3744
- child.stderr.resume();
3745
- child.stdin.on("error", () => {
3746
- });
3747
- child.stdin.on("drain", () => this.flushPointer());
3748
- child.once("exit", () => {
3749
- if (this.process === child) this.process = void 0;
3750
- });
3751
- this.process = child;
3752
- }
3753
- this.process.stdin.write(`${command}
3754
- `);
3755
- }
3756
- };
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";
3757
3560
 
3758
3561
  // ../shared/src/desktop-stream.ts
3759
3562
  var DEFAULT_STREAM_REQUEST = {
@@ -3828,7 +3631,7 @@ function validInput(value) {
3828
3631
  case "pong":
3829
3632
  return finite(value.sentAt);
3830
3633
  case "stats":
3831
- return finite(value.videoFps) && finite(value.audioPacketsPerSecond) && finite(value.decodeQueue) && finite(value.rttMs) && optionalFinite(value.lossPercent) && optionalFinite(value.nacks) && optionalFinite(value.freezes) && optionalFinite(value.videoPacketsPerSecond) && optionalFinite(value.jitterBufferMs) && optionalFinite(value.availableKbps) && optionalFinite(value.playbackDroppedPercent);
3634
+ return finite(value.videoFps) && finite(value.audioPacketsPerSecond) && finite(value.decodeQueue) && finite(value.rttMs) && optionalFinite(value.lossPercent) && optionalFinite(value.nacks) && optionalFinite(value.freezes) && optionalFinite(value.decodeMs) && optionalFinite(value.receiverProcessingMs) && optionalFinite(value.videoPacketsPerSecond) && optionalFinite(value.jitterBufferMs) && optionalFinite(value.availableKbps) && optionalFinite(value.playbackDroppedPercent);
3832
3635
  case "key":
3833
3636
  return typeof value.code === "string" && value.code.length < 40 && typeof value.down === "boolean";
3834
3637
  case "answer":
@@ -4227,45 +4030,419 @@ var WebRtcViewer = class {
4227
4030
  close() {
4228
4031
  if (this.closed) return;
4229
4032
  this.closed = true;
4230
- this.connected = false;
4231
- this.pacer.close();
4232
- for (const resource of [...this.channels, this.video, this.audio, this.peer]) {
4233
- try {
4234
- resource.close();
4235
- } catch {
4236
- }
4033
+ this.connected = false;
4034
+ this.pacer.close();
4035
+ for (const resource of [...this.channels, this.video, this.audio, this.peer]) {
4036
+ try {
4037
+ resource.close();
4038
+ } catch {
4039
+ }
4040
+ }
4041
+ }
4042
+ onFeedback(raw) {
4043
+ const feedback = parseRtcpFeedback(raw, VIDEO_SSRC);
4044
+ if (!feedback) return;
4045
+ if (feedback.pli) this.handlers.keyframe("pli");
4046
+ if (feedback.rembBps !== void 0) {
4047
+ this.remb = feedback.rembBps;
4048
+ this.handlers.remb(feedback.rembBps);
4049
+ }
4050
+ if (feedback.nacks.length > 0) this.pacer.repair(feedback.nacks, this.rttMs ?? 100);
4051
+ }
4052
+ sendFailures = 0;
4053
+ // Sends run from pacer timers, where a thrown error would take the whole
4054
+ // stream server down. Native closure racing the isOpen check is expected;
4055
+ // anything else is logged a few times and the packet is dropped.
4056
+ transmit(track, packet) {
4057
+ try {
4058
+ track.sendMessageBinary(packet);
4059
+ } catch (error) {
4060
+ const message = error instanceof Error ? error.message : String(error);
4061
+ if (/closed|not open/i.test(message) || this.sendFailures++ >= 5) return;
4062
+ process.stderr.write(`[desktop-stream] webrtc send failed: ${message}
4063
+ `);
4064
+ }
4065
+ }
4066
+ };
4067
+
4068
+ // src/video-encoder.ts
4069
+ var captureBinary = resolve2(dirname6(fileURLToPath(import.meta.url)), "..", "dist", "replicas-capture");
4070
+ function superviseVideoEncoder(options) {
4071
+ const { display: display2, width: width2, height: height2, fps: fps2, colorMode: colorMode2 } = options;
4072
+ const env = { ...process.env, DISPLAY: display2 };
4073
+ let nativeCapture = process.env.REPLICAS_DESKTOP_STREAM_CAPTURE !== "ffmpeg" && existsSync6(captureBinary);
4074
+ let captureMetrics;
4075
+ const encoder = superviseEncoder({
4076
+ label: "video",
4077
+ command: () => nativeCapture ? captureBinary : "ffmpeg",
4078
+ onFailure: (command) => {
4079
+ if (command !== captureBinary) return;
4080
+ nativeCapture = false;
4081
+ options.record("capture fallback", "native capture failed; using ffmpeg");
4082
+ },
4083
+ onOutput: (line) => {
4084
+ if (encoder.command() !== captureBinary) return;
4085
+ try {
4086
+ const metrics = JSON.parse(line);
4087
+ const { elapsedMs, busyMs, captureMs, scaleMs, encodeMs, frames, changedFrames } = metrics;
4088
+ if (![elapsedMs, busyMs, captureMs, scaleMs, encodeMs, frames, changedFrames].every((value) => typeof value === "number" && Number.isFinite(value) && value >= 0) || elapsedMs === 0) return;
4089
+ captureMetrics = { at: Date.now(), generation: encoder.generation(), elapsedMs, busyMs, captureMs, scaleMs, encodeMs, frames, changedFrames };
4090
+ } catch {
4091
+ }
4092
+ },
4093
+ log: (message) => options.record("video encoder", message),
4094
+ // Initial encoder work can leave its first image older than the outgoing stream.
4095
+ warmupFrames: 2,
4096
+ env,
4097
+ onPacket: options.onPacket,
4098
+ joinable: (packet) => isKeyframeStart(rtpPayload(packet)),
4099
+ args: (udpPort, warmupFrames) => {
4100
+ const { quality, keyframeMs, profile: videoProfile, intraRefresh, pixelFormat, chromaQpOffset, preset } = options.params();
4101
+ const keyframeInterval = Math.max(1, Math.round(fps2 * keyframeMs / 1e3));
4102
+ if (nativeCapture) return [
4103
+ "--width",
4104
+ String(width2),
4105
+ "--height",
4106
+ String(height2),
4107
+ "--fps",
4108
+ String(fps2),
4109
+ "--threads",
4110
+ String(Math.min(4, availableParallelism())),
4111
+ "--preset",
4112
+ preset,
4113
+ "--crf",
4114
+ String(quality.crf),
4115
+ "--kbps",
4116
+ String(quality.kbps),
4117
+ "--profile",
4118
+ videoProfile,
4119
+ "--keyframe-ms",
4120
+ String(keyframeMs),
4121
+ "--intra-refresh",
4122
+ String(Number(intraRefresh)),
4123
+ "--chroma-qp",
4124
+ String(chromaQpOffset),
4125
+ "--warmup",
4126
+ String(warmupFrames),
4127
+ "--bt709",
4128
+ String(Number(colorMode2 === "bt709")),
4129
+ "--payload",
4130
+ String(H264_PROFILES[videoProfile].payloadType),
4131
+ "--ssrc",
4132
+ String(VIDEO_SSRC),
4133
+ "--url",
4134
+ `rtp://127.0.0.1:${udpPort}?pkt_size=1200&connect=1&rtcpport=${udpPort}`
4135
+ ];
4136
+ const filters = [
4137
+ // Skip rapid catch-up captures after a stall; reset after backward clock adjustments.
4138
+ `select='isnan(prev_selected_t)+lt(t,prev_selected_t)+gte(t-prev_selected_t,${0.5 / fps2})'`,
4139
+ `scale=flags=lanczos${colorMode2 === "bt709" ? ":out_color_matrix=bt709:out_range=tv" : ""},format=${pixelFormat}`
4140
+ ];
4141
+ const x264 = [
4142
+ `fps=${fps2}/1`,
4143
+ "repeat-headers=1",
4144
+ "sliced-threads=1",
4145
+ "slices=4",
4146
+ `keyint=${keyframeInterval}`,
4147
+ `min-keyint=${keyframeInterval}`,
4148
+ "scenecut=0",
4149
+ ...preset === "superfast" ? ["subme=0"] : [],
4150
+ `chroma-qp-offset=${chromaQpOffset}`,
4151
+ ...intraRefresh ? ["intra-refresh=1"] : []
4152
+ ];
4153
+ return [
4154
+ "-hide_banner",
4155
+ "-loglevel",
4156
+ "warning",
4157
+ "-fflags",
4158
+ "nobuffer",
4159
+ "-flags",
4160
+ "low_delay",
4161
+ "-probesize",
4162
+ "32",
4163
+ "-analyzeduration",
4164
+ "0",
4165
+ // Raw 1080p frames are 8MB each; a shallow capture queue bounds memory
4166
+ // and prevents stalled encoding from retaining seconds of stale frames.
4167
+ "-thread_queue_size",
4168
+ "1",
4169
+ "-f",
4170
+ "x11grab",
4171
+ "-draw_mouse",
4172
+ "0",
4173
+ "-framerate",
4174
+ String(fps2),
4175
+ "-video_size",
4176
+ `${width2}x${height2}`,
4177
+ "-use_wallclock_as_timestamps",
4178
+ "1",
4179
+ "-i",
4180
+ `${display2}.0`,
4181
+ "-filter_threads",
4182
+ "1",
4183
+ "-vf",
4184
+ filters.join(","),
4185
+ ...warmupFrames ? ["-force_key_frames", `expr:eq(n,${warmupFrames})`, "-forced-idr", "1"] : [],
4186
+ "-an",
4187
+ "-c:v",
4188
+ "libx264",
4189
+ "-preset",
4190
+ preset,
4191
+ "-tune",
4192
+ "zerolatency",
4193
+ "-threads",
4194
+ String(Math.min(4, availableParallelism())),
4195
+ "-profile:v",
4196
+ videoProfile,
4197
+ "-level:v",
4198
+ "4.2",
4199
+ "-pix_fmt",
4200
+ pixelFormat,
4201
+ ...colorMode2 === "bt709" ? ["-color_primaries", "bt709", "-color_trc", "bt709", "-colorspace", "bt709", "-color_range", "tv"] : [],
4202
+ "-bf",
4203
+ "0",
4204
+ "-g",
4205
+ String(keyframeInterval),
4206
+ "-keyint_min",
4207
+ String(keyframeInterval),
4208
+ "-sc_threshold",
4209
+ "0",
4210
+ "-crf",
4211
+ String(quality.crf),
4212
+ "-maxrate",
4213
+ `${quality.kbps}k`,
4214
+ "-bufsize",
4215
+ `${Math.round(quality.kbps * Math.min(keyframeMs, 100) / 1e3)}k`,
4216
+ "-x264-params",
4217
+ x264.join(":"),
4218
+ "-fps_mode",
4219
+ "passthrough",
4220
+ "-flush_packets",
4221
+ "1",
4222
+ "-f",
4223
+ "rtp",
4224
+ "-payload_type",
4225
+ String(H264_PROFILES[videoProfile].payloadType),
4226
+ "-ssrc",
4227
+ String(VIDEO_SSRC),
4228
+ `rtp://127.0.0.1:${udpPort}?pkt_size=1200&connect=1&rtcpport=${udpPort}`
4229
+ ];
4230
+ }
4231
+ });
4232
+ return {
4233
+ ...encoder,
4234
+ capture: () => ({
4235
+ captureBackend: (encoder.command() ?? (nativeCapture ? captureBinary : "ffmpeg")) === captureBinary ? "native" : "ffmpeg",
4236
+ captureMetrics: captureMetrics?.generation === encoder.generation() && Date.now() - captureMetrics.at <= 2500 ? captureMetrics : void 0
4237
+ })
4238
+ };
4239
+ }
4240
+
4241
+ // src/input-bridge.ts
4242
+ import { spawn as spawn4 } from "child_process";
4243
+ var scrollPixelsPerStep = 80;
4244
+ var namedKeys = {
4245
+ AltLeft: "Alt_L",
4246
+ AltRight: "Alt_R",
4247
+ ArrowDown: "Down",
4248
+ ArrowLeft: "Left",
4249
+ ArrowRight: "Right",
4250
+ ArrowUp: "Up",
4251
+ Backquote: "grave",
4252
+ Backslash: "backslash",
4253
+ Backspace: "BackSpace",
4254
+ BracketLeft: "bracketleft",
4255
+ BracketRight: "bracketright",
4256
+ Comma: "comma",
4257
+ ContextMenu: "Menu",
4258
+ ControlLeft: "Control_L",
4259
+ ControlRight: "Control_R",
4260
+ Delete: "Delete",
4261
+ End: "End",
4262
+ Enter: "Return",
4263
+ Equal: "equal",
4264
+ Escape: "Escape",
4265
+ Home: "Home",
4266
+ Insert: "Insert",
4267
+ MetaLeft: "Super_L",
4268
+ MetaRight: "Super_R",
4269
+ Minus: "minus",
4270
+ PageDown: "Page_Down",
4271
+ PageUp: "Page_Up",
4272
+ Period: "period",
4273
+ Quote: "apostrophe",
4274
+ Semicolon: "semicolon",
4275
+ ShiftLeft: "Shift_L",
4276
+ ShiftRight: "Shift_R",
4277
+ Slash: "slash",
4278
+ Space: "space",
4279
+ Tab: "Tab"
4280
+ };
4281
+ function xKey(code) {
4282
+ if (/^Key[A-Z]$/.test(code)) return code.slice(-1).toLowerCase();
4283
+ if (/^Digit[0-9]$/.test(code)) return code.slice(-1);
4284
+ if (/^F(?:[1-9]|1[0-9]|2[0-4])$/.test(code)) return code;
4285
+ if (/^Numpad[0-9]$/.test(code)) return `KP_${code.slice(-1)}`;
4286
+ return namedKeys[code];
4287
+ }
4288
+ var InputBridge = class {
4289
+ constructor(display2, width2, height2) {
4290
+ this.display = display2;
4291
+ this.width = width2;
4292
+ this.height = height2;
4293
+ }
4294
+ display;
4295
+ width;
4296
+ height;
4297
+ process;
4298
+ pointer;
4299
+ closed = false;
4300
+ pointerFlush;
4301
+ positionSequence = -1;
4302
+ actionId = 0;
4303
+ scrollRemainder = 0;
4304
+ scrollButton = 0;
4305
+ latestPosition;
4306
+ pressedButtons = /* @__PURE__ */ new Set();
4307
+ pressedKeys = /* @__PURE__ */ new Set();
4308
+ close() {
4309
+ if (this.closed) return;
4310
+ this.closed = true;
4311
+ this.release();
4312
+ const child = this.process;
4313
+ this.process = void 0;
4314
+ if (!child) return;
4315
+ child.stdin.end();
4316
+ const timeout = setTimeout(() => child.kill("SIGKILL"), 1e3);
4317
+ timeout.unref();
4318
+ child.once("exit", () => clearTimeout(timeout));
4319
+ }
4320
+ handle(message) {
4321
+ if (this.closed) return;
4322
+ if ("actionId" in message && message.actionId !== void 0) {
4323
+ if (message.actionId !== this.actionId + 1) return;
4324
+ this.actionId = message.actionId;
4325
+ }
4326
+ const sequence = "sequence" in message ? message.sequence : void 0;
4327
+ const stale = sequence !== void 0 && sequence <= this.positionSequence;
4328
+ if (stale && message.type === "pointer") return;
4329
+ if (sequence !== void 0 && !stale) {
4330
+ this.positionSequence = sequence;
4331
+ if ("x" in message) this.latestPosition = this.coordinates(message.x, message.y);
4332
+ }
4333
+ this.apply(message);
4334
+ if (stale && (message.type === "button" || message.type === "wheel" || message.type === "release") && this.latestPosition) {
4335
+ this.pointer = this.latestPosition;
4336
+ this.flushPointer(true);
4237
4337
  }
4238
4338
  }
4239
- onFeedback(raw) {
4240
- const feedback = parseRtcpFeedback(raw, VIDEO_SSRC);
4241
- if (!feedback) return;
4242
- if (feedback.pli) this.handlers.keyframe("pli");
4243
- if (feedback.rembBps !== void 0) {
4244
- this.remb = feedback.rembBps;
4245
- this.handlers.remb(feedback.rembBps);
4339
+ apply(message) {
4340
+ if (message.type === "pointer") {
4341
+ this.pointer = this.coordinates(message.x, message.y);
4342
+ this.pointerFlush ??= setImmediate(() => this.flushPointer());
4343
+ return;
4246
4344
  }
4247
- if (feedback.nacks.length > 0) this.pacer.repair(feedback.nacks, this.rttMs ?? 100);
4345
+ if (message.type === "button") {
4346
+ this.pointer = this.coordinates(message.x, message.y);
4347
+ this.flushPointer(true);
4348
+ const button = message.button === 0 ? 1 : message.button === 1 ? 2 : message.button === 2 ? 3 : 0;
4349
+ if (!button) return;
4350
+ if (message.down) {
4351
+ if (this.pressedButtons.has(button)) return;
4352
+ this.pressedButtons.add(button);
4353
+ this.write(`mousedown ${button}`);
4354
+ } else {
4355
+ this.pressedButtons.delete(button);
4356
+ this.write(`mouseup ${button}`);
4357
+ }
4358
+ return;
4359
+ }
4360
+ if (message.type === "wheel") {
4361
+ this.pointer = this.coordinates(message.x, message.y);
4362
+ this.flushPointer(true);
4363
+ const vertical = Math.abs(message.deltaY) >= Math.abs(message.deltaX);
4364
+ const delta = vertical ? message.deltaY : message.deltaX;
4365
+ if (!delta) return;
4366
+ const button = vertical ? delta < 0 ? 4 : 5 : delta < 0 ? 6 : 7;
4367
+ if (button !== this.scrollButton) this.scrollRemainder = 0;
4368
+ this.scrollButton = button;
4369
+ const distance = Math.abs(delta);
4370
+ let steps;
4371
+ if (distance >= scrollPixelsPerStep) {
4372
+ steps = Math.min(4, Math.round(distance / scrollPixelsPerStep));
4373
+ this.scrollRemainder = 0;
4374
+ } else {
4375
+ this.scrollRemainder += distance;
4376
+ steps = Math.floor(this.scrollRemainder / scrollPixelsPerStep);
4377
+ this.scrollRemainder %= scrollPixelsPerStep;
4378
+ }
4379
+ for (let index = 0; index < steps; index += 1) this.write(`mouseclick ${button}`);
4380
+ return;
4381
+ }
4382
+ if (message.type === "key") {
4383
+ const key = xKey(message.code);
4384
+ if (!key || message.repeat) return;
4385
+ if (message.down) {
4386
+ if (this.pressedKeys.has(key)) return;
4387
+ this.pressedKeys.add(key);
4388
+ this.write(`keydown ${key}`);
4389
+ } else {
4390
+ this.pressedKeys.delete(key);
4391
+ this.write(`keyup ${key}`);
4392
+ }
4393
+ return;
4394
+ }
4395
+ if (message.type === "release") this.release();
4248
4396
  }
4249
- sendFailures = 0;
4250
- // Sends run from pacer timers, where a thrown error would take the whole
4251
- // stream server down. Native closure racing the isOpen check is expected;
4252
- // anything else is logged a few times and the packet is dropped.
4253
- transmit(track, packet) {
4254
- try {
4255
- track.sendMessageBinary(packet);
4256
- } catch (error) {
4257
- const message = error instanceof Error ? error.message : String(error);
4258
- if (/closed|not open/i.test(message) || this.sendFailures++ >= 5) return;
4259
- process.stderr.write(`[desktop-stream] webrtc send failed: ${message}
4260
- `);
4397
+ release() {
4398
+ if (this.pointerFlush) clearImmediate(this.pointerFlush);
4399
+ this.pointerFlush = void 0;
4400
+ this.pointer = void 0;
4401
+ this.scrollRemainder = 0;
4402
+ this.scrollButton = 0;
4403
+ for (const button of this.pressedButtons) this.write(`mouseup ${button}`);
4404
+ for (const key of this.pressedKeys) this.write(`keyup ${key}`);
4405
+ this.pressedButtons.clear();
4406
+ this.pressedKeys.clear();
4407
+ }
4408
+ coordinates(x, y) {
4409
+ return {
4410
+ x: Math.round(Math.min(1, Math.max(0, x)) * (this.width - 1)),
4411
+ y: Math.round(Math.min(1, Math.max(0, y)) * (this.height - 1))
4412
+ };
4413
+ }
4414
+ flushPointer(force = false) {
4415
+ if (this.pointerFlush) clearImmediate(this.pointerFlush);
4416
+ this.pointerFlush = void 0;
4417
+ if (!this.pointer || !force && this.process?.stdin.writableNeedDrain) return;
4418
+ this.write(`mousemove ${this.pointer.x} ${this.pointer.y}`);
4419
+ this.pointer = void 0;
4420
+ }
4421
+ write(command) {
4422
+ if (!this.process || this.process.exitCode !== null || this.process.stdin.destroyed) {
4423
+ const child = spawn4("xte", [], {
4424
+ env: { ...process.env, DISPLAY: this.display },
4425
+ stdio: ["pipe", "pipe", "pipe"]
4426
+ });
4427
+ child.stdout.resume();
4428
+ child.stderr.resume();
4429
+ child.stdin.on("error", () => {
4430
+ });
4431
+ child.stdin.on("drain", () => this.flushPointer());
4432
+ child.once("exit", () => {
4433
+ if (this.process === child) this.process = void 0;
4434
+ });
4435
+ this.process = child;
4261
4436
  }
4437
+ this.process.stdin.write(`${command}
4438
+ `);
4262
4439
  }
4263
4440
  };
4264
4441
 
4265
4442
  // src/media-server.ts
4266
4443
  var width = Number.parseInt(process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH), 10);
4267
4444
  var height = Number.parseInt(process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT), 10);
4268
- var basePreset = availableParallelism() >= 4 ? "superfast" : "ultrafast";
4445
+ var basePreset = availableParallelism2() >= 4 ? "superfast" : "ultrafast";
4269
4446
  var fps = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_FPS || "", 10) || 60;
4270
4447
  var port = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_PORT || String(DESKTOP_STREAM_PORT), 10);
4271
4448
  var display = process.env.REPLICAS_DESKTOP_DISPLAY?.replace(/^([^:])/, ":$1") || ":99";
@@ -4297,7 +4474,6 @@ var qualityLadder = (profile) => rungCaps.map((kbps, index) => ({ kbps, crf: pro
4297
4474
  var fullChromaMinKbps = 8e3;
4298
4475
  var fallbackKeyframeMs = 250;
4299
4476
  var webrtcStartupRung = 2;
4300
- var vbvMs = 100;
4301
4477
  var targetBacklogMs = 120;
4302
4478
  var congestedBacklogMs = 250;
4303
4479
  var congestedDecodeQueue = 3;
@@ -4310,8 +4486,8 @@ var sampleHistory = 300;
4310
4486
  var telemetryLog = process.env.REPLICAS_DESKTOP_STREAM_TELEMETRY === "1";
4311
4487
  var budgetFactor = 1.25;
4312
4488
  var protectMs = 1e4;
4313
- var packageRoot = resolve2(dirname6(fileURLToPath(import.meta.url)), "..");
4314
- var webroot = process.env.REPLICAS_DESKTOP_STREAM_WEBROOT || resolve2(packageRoot, "assets", "desktop");
4489
+ var packageRoot = resolve3(dirname7(fileURLToPath2(import.meta.url)), "..");
4490
+ var webroot = process.env.REPLICAS_DESKTOP_STREAM_WEBROOT || resolve3(packageRoot, "assets", "desktop");
4315
4491
  var contentTypes = {
4316
4492
  ".css": "text/css; charset=utf-8",
4317
4493
  ".html": "text/html; charset=utf-8",
@@ -4367,7 +4543,7 @@ function processCpuMs(pid) {
4367
4543
  }
4368
4544
  }
4369
4545
  async function runMediaServer() {
4370
- if (![width, height, fps, port].every((value) => Number.isInteger(value) && value > 0)) throw new Error("Invalid desktop stream dimensions, frame rate, or port");
4546
+ if (![width, height, fps, port].every((value) => Number.isInteger(value) && value > 0) || width % 2 || height % 2) throw new Error("Invalid desktop stream dimensions, frame rate, or port");
4371
4547
  const webrtc = webrtcEnabled && await loadWebRtc();
4372
4548
  const viewers = /* @__PURE__ */ new Map();
4373
4549
  const videoAssembler = new H264RtpAssembler();
@@ -4518,112 +4694,17 @@ async function runMediaServer() {
4518
4694
  if (parsed) broadcast(mediaPacket(3 /* Audio */, audioClock.microseconds(parsed.timestamp), parsed.payload));
4519
4695
  };
4520
4696
  const env = { ...process.env, DISPLAY: display };
4521
- const videoEncoder = superviseEncoder({
4522
- label: "video",
4523
- log: (message) => record("video encoder", message),
4524
- // Initial encoder work can leave its first image older than the outgoing stream.
4525
- warmupFrames: 2,
4526
- env,
4697
+ const videoEncoder = superviseVideoEncoder({
4698
+ display,
4699
+ width,
4700
+ height,
4701
+ fps,
4702
+ colorMode,
4703
+ params: encoderParams,
4527
4704
  onPacket: onVideoPacket,
4528
- joinable: (packet) => isKeyframeStart(rtpPayload(packet)),
4529
- args: (udpPort, warmupFrames) => {
4530
- const { quality, keyframeMs, profile: videoProfile, intraRefresh, pixelFormat, chromaQpOffset } = encoderParams();
4531
- const keyframeInterval = Math.max(1, Math.round(fps * keyframeMs / 1e3));
4532
- const filters = [
4533
- // Skip rapid catch-up captures after a stall; reset after backward clock adjustments.
4534
- `select='isnan(prev_selected_t)+lt(t,prev_selected_t)+gte(t-prev_selected_t,${0.5 / fps})'`,
4535
- colorMode === "bt709" ? `scale=out_color_matrix=bt709:out_range=tv,format=${pixelFormat}` : `format=${pixelFormat}`
4536
- ];
4537
- const x264 = [
4538
- `fps=${fps}/1`,
4539
- "repeat-headers=1",
4540
- "sliced-threads=1",
4541
- "slices=4",
4542
- `keyint=${keyframeInterval}`,
4543
- `min-keyint=${keyframeInterval}`,
4544
- "scenecut=0",
4545
- `chroma-qp-offset=${chromaQpOffset}`,
4546
- ...intraRefresh ? ["intra-refresh=1"] : []
4547
- ];
4548
- return [
4549
- "-hide_banner",
4550
- "-loglevel",
4551
- "warning",
4552
- "-fflags",
4553
- "nobuffer",
4554
- "-flags",
4555
- "low_delay",
4556
- "-probesize",
4557
- "32",
4558
- "-analyzeduration",
4559
- "0",
4560
- // Raw 1080p frames are 8MB each; a shallow capture queue bounds memory
4561
- // and prevents stalled encoding from retaining seconds of stale frames.
4562
- "-thread_queue_size",
4563
- "1",
4564
- "-f",
4565
- "x11grab",
4566
- "-draw_mouse",
4567
- "0",
4568
- "-framerate",
4569
- String(fps),
4570
- "-video_size",
4571
- `${width}x${height}`,
4572
- "-use_wallclock_as_timestamps",
4573
- "1",
4574
- "-i",
4575
- `${display}.0`,
4576
- "-filter_threads",
4577
- "1",
4578
- "-vf",
4579
- filters.join(","),
4580
- ...warmupFrames ? ["-force_key_frames", `expr:eq(n,${warmupFrames})`, "-forced-idr", "1"] : [],
4581
- "-an",
4582
- "-c:v",
4583
- "libx264",
4584
- "-preset",
4585
- preset,
4586
- "-tune",
4587
- "zerolatency",
4588
- "-threads",
4589
- String(Math.min(4, availableParallelism())),
4590
- "-profile:v",
4591
- videoProfile,
4592
- "-level:v",
4593
- "4.2",
4594
- "-pix_fmt",
4595
- pixelFormat,
4596
- ...colorMode === "bt709" ? ["-color_primaries", "bt709", "-color_trc", "bt709", "-colorspace", "bt709", "-color_range", "tv"] : [],
4597
- "-bf",
4598
- "0",
4599
- "-g",
4600
- String(keyframeInterval),
4601
- "-keyint_min",
4602
- String(keyframeInterval),
4603
- "-sc_threshold",
4604
- "0",
4605
- "-crf",
4606
- String(quality.crf),
4607
- "-maxrate",
4608
- `${quality.kbps}k`,
4609
- "-bufsize",
4610
- `${Math.round(quality.kbps * Math.min(keyframeMs, vbvMs) / 1e3)}k`,
4611
- "-x264-params",
4612
- x264.join(":"),
4613
- "-fps_mode",
4614
- "passthrough",
4615
- "-flush_packets",
4616
- "1",
4617
- "-f",
4618
- "rtp",
4619
- "-payload_type",
4620
- String(H264_PROFILES[videoProfile].payloadType),
4621
- "-ssrc",
4622
- String(VIDEO_SSRC),
4623
- `rtp://127.0.0.1:${udpPort}?pkt_size=1200&connect=1&rtcpport=${udpPort}`
4624
- ];
4625
- }
4705
+ record
4626
4706
  });
4707
+ const expectedFps = () => videoEncoder.capture().captureBackend === "native" ? Math.max(1, Math.min(fps, encoderFps)) : fps;
4627
4708
  const audioEncoder = superviseEncoder({
4628
4709
  label: "audio",
4629
4710
  log: (message) => record("audio encoder", message),
@@ -4760,13 +4841,13 @@ async function runMediaServer() {
4760
4841
  const stableSince = stableRttWindows.get(viewer)?.since;
4761
4842
  return [
4762
4843
  ...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`] : [],
4763
- ...stats.videoFps < fps * 0.6 && viewer.lowFpsReports >= 2 ? [`decoded ${stats.videoFps.toFixed(0)} fps`] : [],
4844
+ ...stats.videoFps < expectedFps() * 0.6 && viewer.lowFpsReports >= 2 ? [`decoded ${stats.videoFps.toFixed(0)} fps`] : [],
4764
4845
  ...(stats.lossPercent ?? 0) > 3 ? [`loss ${(stats.lossPercent ?? 0).toFixed(1)}%`] : [],
4765
4846
  ...(stats.jitterBufferMs ?? 0) > 80 ? [`jitter buffer ${(stats.jitterBufferMs ?? 0).toFixed(0)} ms`] : []
4766
4847
  ];
4767
4848
  };
4768
4849
  const lowFpsObservationAt = /* @__PURE__ */ new WeakMap();
4769
- const severe = (viewer, stats) => !viewer.fallback && ((stats.lossPercent ?? 0) > 10 || stats.videoFps < fps * 0.25 && viewer.lowFpsReports >= 2);
4850
+ const severe = (viewer, stats) => !viewer.fallback && ((stats.lossPercent ?? 0) > 10 || stats.videoFps < expectedFps() * 0.25 && viewer.lowFpsReports >= 2);
4770
4851
  let lastSevereDescentAt = 0;
4771
4852
  const descendSevere = (now, trigger) => {
4772
4853
  comfortableSeconds = 0;
@@ -4792,7 +4873,9 @@ async function runMediaServer() {
4792
4873
  bytesAtTick = videoBytes;
4793
4874
  if (recentVideoKbps.push(videoKbps) > 3) recentVideoKbps.shift();
4794
4875
  if (viewers.size === 0) return;
4795
- if (videoEncoder.running() && encoderFps < fps * 0.9) {
4876
+ const { captureBackend, captureMetrics } = videoEncoder.capture();
4877
+ const captureBusy = captureBackend !== "native" || !captureMetrics || captureMetrics.busyMs / captureMetrics.elapsedMs > 0.8;
4878
+ if (videoEncoder.running() && encoderFps < fps * 0.9 && captureBusy) {
4796
4879
  cleanTransportSeconds = 0;
4797
4880
  congestedSeconds = 0;
4798
4881
  comfortableSeconds = 0;
@@ -4820,7 +4903,7 @@ async function runMediaServer() {
4820
4903
  }
4821
4904
  const now = Date.now();
4822
4905
  const reporting = [...viewers.values()].filter((viewer) => viewer.stats !== void 0 && viewer.stats.videoPacketsPerSecond !== 0 && now - viewer.stats.at < 3e3 && now - viewer.connectedAt > 2500);
4823
- cleanTransportSeconds = reporting.length === viewers.size && reporting.length > 0 && reporting.every((viewer) => !viewer.fallback && viewer.rttFloorMs <= viewer.rtcBaselineMs + 20 && (viewer.stats.lossPercent ?? 0) === 0 && (viewer.stats.nacks ?? 0) === 0 && (viewer.stats.jitterBufferMs ?? 0) <= 20 && viewer.stats.videoFps >= fps * 0.95) ? cleanTransportSeconds + 1 : 0;
4906
+ cleanTransportSeconds = reporting.length === viewers.size && reporting.length > 0 && reporting.every((viewer) => !viewer.fallback && viewer.rttFloorMs <= viewer.rtcBaselineMs + 20 && (viewer.stats.lossPercent ?? 0) === 0 && (viewer.stats.nacks ?? 0) === 0 && (viewer.stats.jitterBufferMs ?? 0) <= 20 && viewer.stats.videoFps >= expectedFps() * 0.95) ? cleanTransportSeconds + 1 : 0;
4824
4907
  if (reporting.length > 0 && reporting.every((viewer) => severe(viewer, viewer.stats))) {
4825
4908
  descendSevere(now, reporting.map((viewer) => `${(viewer.stats.lossPercent ?? 0).toFixed(1)}% loss, ${viewer.stats.videoFps.toFixed(0)} fps decoded`).join(" | "));
4826
4909
  return;
@@ -4890,7 +4973,7 @@ async function runMediaServer() {
4890
4973
  kbps: videoKbps,
4891
4974
  adaptive
4892
4975
  };
4893
- const cpuState = { ...cpu, load: Math.round(loadavg()[0] * 100) / 100, cores: availableParallelism() };
4976
+ const cpuState = { ...cpu, load: Math.round(loadavg()[0] * 100) / 100, cores: availableParallelism2() };
4894
4977
  const sample = {
4895
4978
  at: now,
4896
4979
  encoder: { rung, crf: quality.crf, kbpsCap: quality.kbps, keyframeMs, preset, fps: encoderFps, kbps: videoKbps },
@@ -4989,11 +5072,13 @@ async function runMediaServer() {
4989
5072
  nacks: optional(message.nacks, 1e5),
4990
5073
  freezes: optional(message.freezes, 1e3),
4991
5074
  jitterBufferMs: optional(message.jitterBufferMs, 6e4),
5075
+ decodeMs: optional(message.decodeMs, 6e4),
5076
+ receiverProcessingMs: optional(message.receiverProcessingMs, 6e4),
4992
5077
  availableKbps: optional(message.availableKbps, 1e6),
4993
5078
  playbackDroppedPercent: optional(message.playbackDroppedPercent, 100)
4994
5079
  };
4995
5080
  const now = viewer.stats.at;
4996
- if (viewer.stats.videoFps >= fps * 0.6 || viewer.stats.videoPacketsPerSecond === 0) {
5081
+ if (viewer.stats.videoFps >= expectedFps() * 0.6 || viewer.stats.videoPacketsPerSecond === 0) {
4997
5082
  viewer.lowFpsReports = 0;
4998
5083
  lowFpsObservationAt.delete(viewer);
4999
5084
  } else if (now - (lowFpsObservationAt.get(viewer) ?? 0) >= 1e3) {
@@ -5006,7 +5091,7 @@ async function runMediaServer() {
5006
5091
  const rtt = viewer.rttFloorMs;
5007
5092
  let window = stableRttWindows.get(viewer);
5008
5093
  const stableNetwork = !viewer.fallback && rtt > viewer.rtcBaselineMs + 20 && (viewer.stats.videoPacketsPerSecond ?? 0) > 0 && (viewer.stats.lossPercent ?? 0) <= 1 && (viewer.stats.jitterBufferMs ?? 0) <= 80;
5009
- if (stableNetwork && viewer.stats.videoFps >= fps * 0.9) {
5094
+ if (stableNetwork && viewer.stats.videoFps >= expectedFps() * 0.9) {
5010
5095
  if (!window || now - window.at > 3e3 || Math.max(window.max, rtt) - Math.min(window.min, rtt) > 20) {
5011
5096
  window = { since: now, at: now, min: rtt, max: rtt };
5012
5097
  }
@@ -5018,7 +5103,7 @@ async function runMediaServer() {
5018
5103
  viewer.rtcBaselineMs = window.min;
5019
5104
  stableRttWindows.delete(viewer);
5020
5105
  }
5021
- } else if (!stableNetwork || !window || now - window.at > 750 || viewer.stats.videoFps < fps * 0.6 || Math.max(window.max, rtt) - Math.min(window.min, rtt) > 20) {
5106
+ } else if (!stableNetwork || !window || now - window.at > 750 || viewer.stats.videoFps < expectedFps() * 0.6 || Math.max(window.max, rtt) - Math.min(window.min, rtt) > 20) {
5022
5107
  stableRttWindows.delete(viewer);
5023
5108
  } else {
5024
5109
  window.min = Math.min(window.min, rtt);
@@ -5037,7 +5122,7 @@ async function runMediaServer() {
5037
5122
  }
5038
5123
  }
5039
5124
  if (viewer.stats.videoFps > 0) viewer.hasDecodedFrame = true;
5040
- if (viewer.stats.videoFps >= fps * 0.8) viewer.everHealthy = true;
5125
+ if (viewer.stats.videoFps >= expectedFps() * 0.8) viewer.everHealthy = true;
5041
5126
  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");
5042
5127
  return;
5043
5128
  }
@@ -5099,6 +5184,7 @@ async function runMediaServer() {
5099
5184
  audioBytes,
5100
5185
  videoKbps,
5101
5186
  encoderFps,
5187
+ ...videoEncoder.capture(),
5102
5188
  cpu,
5103
5189
  clientStats: all.flatMap((viewer) => viewer.stats ? [viewer.stats] : []),
5104
5190
  clientPacer: all.map((viewer) => viewer.rtc?.snapshot() ?? null),
@@ -5125,8 +5211,8 @@ async function runMediaServer() {
5125
5211
  return;
5126
5212
  }
5127
5213
  const requested = url.pathname === "/" ? "stream-index.html" : url.pathname.slice(1);
5128
- const path = resolve2(webroot, requested);
5129
- if (!path.startsWith(resolve2(webroot) + "/") || !existsSync6(path) || !statSync2(path).isFile()) {
5214
+ const path = resolve3(webroot, requested);
5215
+ if (!path.startsWith(resolve3(webroot) + "/") || !existsSync7(path) || !statSync2(path).isFile()) {
5130
5216
  response.statusCode = 404;
5131
5217
  response.end("Not found");
5132
5218
  return;
@@ -5137,7 +5223,7 @@ async function runMediaServer() {
5137
5223
  if (viewers.size === 0) stopEncodersSoon();
5138
5224
  response.setHeader("cache-control", "no-store");
5139
5225
  const digest = createHash3("sha1");
5140
- for (const name of readdirSync(webroot).filter((file) => /\.(js|css)$/.test(file)).sort()) digest.update(readFileSync6(resolve2(webroot, name)));
5226
+ for (const name of readdirSync(webroot).filter((file) => /\.(js|css)$/.test(file)).sort()) digest.update(readFileSync6(resolve3(webroot, name)));
5141
5227
  response.end(readFileSync6(path, "utf8").replaceAll("__VERSION__", `${COMPUTER_USE_VERSION}-${digest.digest("hex").slice(0, 8)}`));
5142
5228
  return;
5143
5229
  }
@@ -5279,9 +5365,9 @@ async function runMediaServer() {
5279
5365
 
5280
5366
  // src/setup.ts
5281
5367
  import { spawnSync as spawnSync6 } from "child_process";
5282
- import { accessSync, chmodSync as chmodSync2, constants, cpSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync5 } from "fs";
5283
- import { dirname as dirname7, resolve as resolve3 } from "path";
5284
- import { fileURLToPath as fileURLToPath2 } from "url";
5368
+ import { accessSync, chmodSync as chmodSync2, constants, cpSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync5 } from "fs";
5369
+ import { dirname as dirname8, resolve as resolve4 } from "path";
5370
+ import { fileURLToPath as fileURLToPath3 } from "url";
5285
5371
  var dependencies = [
5286
5372
  ["bash", "bash"],
5287
5373
  ["Node.js", "node"],
@@ -5341,7 +5427,7 @@ function commandVersion(command) {
5341
5427
  function hasCommand(command) {
5342
5428
  return (process.env.PATH ?? "").split(":").some((directory) => {
5343
5429
  try {
5344
- accessSync(resolve3(directory, command), constants.X_OK);
5430
+ accessSync(resolve4(directory, command), constants.X_OK);
5345
5431
  return true;
5346
5432
  } catch {
5347
5433
  return false;
@@ -5377,10 +5463,10 @@ async function setupComputer(options = {}) {
5377
5463
  missing.push("WebRTC native module (node-datachannel)");
5378
5464
  console.error("\u2717 WebRTC (node-datachannel) failed to load");
5379
5465
  }
5380
- const noVncPackageRoot = dirname7(dirname7(fileURLToPath2(import.meta.resolve("@novnc/novnc"))));
5381
- const noVncCore = resolve3(noVncPackageRoot, "core");
5382
- const noVncVendor = resolve3(noVncPackageRoot, "vendor");
5383
- if (!existsSync7(noVncCore) || !existsSync7(noVncVendor)) {
5466
+ const noVncPackageRoot = dirname8(dirname8(fileURLToPath3(import.meta.resolve("@novnc/novnc"))));
5467
+ const noVncCore = resolve4(noVncPackageRoot, "core");
5468
+ const noVncVendor = resolve4(noVncPackageRoot, "vendor");
5469
+ if (!existsSync8(noVncCore) || !existsSync8(noVncVendor)) {
5384
5470
  missing.push("bundled noVNC client");
5385
5471
  console.error("\u2717 bundled noVNC client");
5386
5472
  } else {
@@ -5395,38 +5481,38 @@ async function setupComputer(options = {}) {
5395
5481
  if (missing.length > 0) throw new Error(`Missing ${missing.length} computer-use requirement${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}`);
5396
5482
  if (options.checkOnly) return;
5397
5483
  if (process.getuid?.() !== 0) throw new Error("replicas-computer setup must run as root to install runtime files");
5398
- const packageRoot2 = resolve3(dirname7(fileURLToPath2(import.meta.url)), "..");
5399
- const assets = resolve3(packageRoot2, "assets", "desktop");
5400
- if (!existsSync7(assets)) throw new Error(`Computer-use assets are missing from ${assets}`);
5484
+ const packageRoot2 = resolve4(dirname8(fileURLToPath3(import.meta.url)), "..");
5485
+ const assets = resolve4(packageRoot2, "assets", "desktop");
5486
+ if (!existsSync8(assets)) throw new Error(`Computer-use assets are missing from ${assets}`);
5401
5487
  mkdirSync6("/usr/local/share/replicas/desktop", { recursive: true });
5402
5488
  cpSync(assets, "/usr/local/share/replicas/desktop", { recursive: true, force: true });
5403
5489
  const fontRoot = "/usr/local/share/fonts/replicas";
5404
5490
  mkdirSync6(fontRoot, { recursive: true });
5405
- for (const file of readdirSync2(assets).filter((name) => name.endsWith(".ttf"))) cpSync(resolve3(assets, file), resolve3(fontRoot, file), { force: true });
5491
+ for (const file of readdirSync2(assets).filter((name) => name.endsWith(".ttf"))) cpSync(resolve4(assets, file), resolve4(fontRoot, file), { force: true });
5406
5492
  spawnSync6("fc-cache", ["-f", fontRoot], { stdio: "ignore" });
5407
5493
  const viewerRoot = "/usr/local/share/replicas/desktop/novnc";
5408
5494
  mkdirSync6(viewerRoot, { recursive: true });
5409
- cpSync(noVncCore, resolve3(viewerRoot, "core"), { recursive: true, force: true });
5410
- cpSync(noVncVendor, resolve3(viewerRoot, "vendor"), { recursive: true, force: true });
5495
+ cpSync(noVncCore, resolve4(viewerRoot, "core"), { recursive: true, force: true });
5496
+ cpSync(noVncVendor, resolve4(viewerRoot, "vendor"), { recursive: true, force: true });
5411
5497
  for (const file of ["novnc-index.html", "viewer.css", "viewer-theme.css", "viewer-theme.js", "viewer-icon.svg", "geist-mono-400.ttf", "geist-mono-600.ttf", "geist-mono-700.ttf", "general-sans-300.ttf"]) {
5412
- cpSync(resolve3(assets, file), resolve3(viewerRoot, file === "novnc-index.html" ? "index.html" : file), { force: true });
5498
+ cpSync(resolve4(assets, file), resolve4(viewerRoot, file === "novnc-index.html" ? "index.html" : file), { force: true });
5413
5499
  }
5414
- cpSync(resolve3(packageRoot2, "dist", "viewer.js"), resolve3(viewerRoot, "viewer.js"), { force: true });
5500
+ cpSync(resolve4(packageRoot2, "dist", "viewer.js"), resolve4(viewerRoot, "viewer.js"), { force: true });
5415
5501
  const streamRoot = "/usr/local/share/replicas/desktop/stream";
5416
5502
  mkdirSync6(streamRoot, { recursive: true });
5417
5503
  for (const file of ["stream-index.html", "stream-viewer.css", "viewer-theme.css", "viewer-theme.js", "viewer-icon.svg", "geist-mono-400.ttf", "geist-mono-600.ttf", "geist-mono-700.ttf", "general-sans-300.ttf"]) {
5418
- cpSync(resolve3(assets, file), resolve3(streamRoot, file), { force: true });
5504
+ cpSync(resolve4(assets, file), resolve4(streamRoot, file), { force: true });
5419
5505
  }
5420
- cpSync(resolve3(packageRoot2, "dist", "stream-viewer.js"), resolve3(streamRoot, "stream-viewer.js"), { force: true });
5421
- cpSync(resolve3(assets, "start-services.sh"), "/usr/local/bin/replicas-start-desktop-services", { force: true });
5422
- cpSync(resolve3(assets, "replicas-chrome"), "/usr/local/bin/replicas-chrome", { force: true });
5423
- cpSync(resolve3(assets, "replicas-terminal"), "/usr/local/bin/replicas-terminal", { force: true });
5424
- cpSync(resolve3(assets, "replicas-chrome.desktop"), "/usr/share/applications/replicas-chrome.desktop", { force: true });
5506
+ cpSync(resolve4(packageRoot2, "dist", "stream-viewer.js"), resolve4(streamRoot, "stream-viewer.js"), { force: true });
5507
+ cpSync(resolve4(assets, "start-services.sh"), "/usr/local/bin/replicas-start-desktop-services", { force: true });
5508
+ cpSync(resolve4(assets, "replicas-chrome"), "/usr/local/bin/replicas-chrome", { force: true });
5509
+ cpSync(resolve4(assets, "replicas-terminal"), "/usr/local/bin/replicas-terminal", { force: true });
5510
+ cpSync(resolve4(assets, "replicas-chrome.desktop"), "/usr/share/applications/replicas-chrome.desktop", { force: true });
5425
5511
  chmodSync2("/usr/local/bin/replicas-start-desktop-services", 493);
5426
5512
  chmodSync2("/usr/local/bin/replicas-chrome", 493);
5427
5513
  chmodSync2("/usr/local/bin/replicas-terminal", 493);
5428
5514
  writeFileSync5("/etc/systemd/system/replicas-computer.service", serviceUnit, { mode: 420 });
5429
- if (existsSync7("/run/systemd/system")) {
5515
+ if (existsSync8("/run/systemd/system")) {
5430
5516
  const reload = spawnSync6("systemctl", ["daemon-reload"], { stdio: "pipe" });
5431
5517
  if (reload.status !== 0) throw new Error(`Failed to reload systemd: ${reload.stderr?.toString().trim() || `exit ${reload.status}`}`);
5432
5518
  }