replicas-computer 0.3.6 → 0.3.8

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.6";
2940
+ var COMPUTER_USE_VERSION = "0.3.8";
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":
@@ -4126,7 +3929,15 @@ function parseIceServers(spec) {
4126
3929
  const native = (spec ?? "stun:stun.l.google.com:19302").split(",").map((server) => server.trim()).filter(Boolean);
4127
3930
  const browser = native.map((url) => {
4128
3931
  const match = url.match(/^(turns?):([^:@]+):(.+)@([^@]+)$/);
4129
- return match ? { urls: `${match[1]}:${match[4]}`, username: match[2], credential: match[3] } : { urls: url };
3932
+ if (!match) return { urls: url };
3933
+ const [username, credential] = match.slice(2, 4).map((value) => {
3934
+ try {
3935
+ return decodeURIComponent(value);
3936
+ } catch {
3937
+ return value;
3938
+ }
3939
+ });
3940
+ return { urls: `${match[1]}:${match[4]}`, username, credential };
4130
3941
  });
4131
3942
  return { native, browser };
4132
3943
  }
@@ -4219,45 +4030,419 @@ var WebRtcViewer = class {
4219
4030
  close() {
4220
4031
  if (this.closed) return;
4221
4032
  this.closed = true;
4222
- this.connected = false;
4223
- this.pacer.close();
4224
- for (const resource of [...this.channels, this.video, this.audio, this.peer]) {
4225
- try {
4226
- resource.close();
4227
- } catch {
4228
- }
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);
4229
4337
  }
4230
4338
  }
4231
- onFeedback(raw) {
4232
- const feedback = parseRtcpFeedback(raw, VIDEO_SSRC);
4233
- if (!feedback) return;
4234
- if (feedback.pli) this.handlers.keyframe("pli");
4235
- if (feedback.rembBps !== void 0) {
4236
- this.remb = feedback.rembBps;
4237
- 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;
4238
4344
  }
4239
- 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();
4240
4396
  }
4241
- sendFailures = 0;
4242
- // Sends run from pacer timers, where a thrown error would take the whole
4243
- // stream server down. Native closure racing the isOpen check is expected;
4244
- // anything else is logged a few times and the packet is dropped.
4245
- transmit(track, packet) {
4246
- try {
4247
- track.sendMessageBinary(packet);
4248
- } catch (error) {
4249
- const message = error instanceof Error ? error.message : String(error);
4250
- if (/closed|not open/i.test(message) || this.sendFailures++ >= 5) return;
4251
- process.stderr.write(`[desktop-stream] webrtc send failed: ${message}
4252
- `);
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;
4253
4436
  }
4437
+ this.process.stdin.write(`${command}
4438
+ `);
4254
4439
  }
4255
4440
  };
4256
4441
 
4257
4442
  // src/media-server.ts
4258
4443
  var width = Number.parseInt(process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH), 10);
4259
4444
  var height = Number.parseInt(process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT), 10);
4260
- var basePreset = availableParallelism() >= 4 ? "superfast" : "ultrafast";
4445
+ var basePreset = availableParallelism2() >= 4 ? "superfast" : "ultrafast";
4261
4446
  var fps = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_FPS || "", 10) || 60;
4262
4447
  var port = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_PORT || String(DESKTOP_STREAM_PORT), 10);
4263
4448
  var display = process.env.REPLICAS_DESKTOP_DISPLAY?.replace(/^([^:])/, ":$1") || ":99";
@@ -4284,12 +4469,11 @@ var profiles = {
4284
4469
  balanced: { crf: 14, chromaQpOffset: 0 },
4285
4470
  text: { crf: 10, chromaQpOffset: -6 }
4286
4471
  };
4287
- var rungCaps = [configuredTopKbps, 12e3, 8e3, 5e3, 3e3, 1500].map((kbps) => Math.min(kbps, configuredTopKbps));
4472
+ var rungCaps = [configuredTopKbps, 12e3, 8e3, 5e3, 3e3, 1500, 750].map((kbps) => Math.min(kbps, configuredTopKbps));
4288
4473
  var qualityLadder = (profile) => rungCaps.map((kbps, index) => ({ kbps, crf: profiles[profile].crf + 2 * index, keyframeMs: index >= 4 ? 2e3 : 1e3 }));
4289
4474
  var fullChromaMinKbps = 8e3;
4290
4475
  var fallbackKeyframeMs = 250;
4291
4476
  var webrtcStartupRung = 2;
4292
- var vbvMs = 100;
4293
4477
  var targetBacklogMs = 120;
4294
4478
  var congestedBacklogMs = 250;
4295
4479
  var congestedDecodeQueue = 3;
@@ -4302,8 +4486,8 @@ var sampleHistory = 300;
4302
4486
  var telemetryLog = process.env.REPLICAS_DESKTOP_STREAM_TELEMETRY === "1";
4303
4487
  var budgetFactor = 1.25;
4304
4488
  var protectMs = 1e4;
4305
- var packageRoot = resolve2(dirname6(fileURLToPath(import.meta.url)), "..");
4306
- 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");
4307
4491
  var contentTypes = {
4308
4492
  ".css": "text/css; charset=utf-8",
4309
4493
  ".html": "text/html; charset=utf-8",
@@ -4359,7 +4543,7 @@ function processCpuMs(pid) {
4359
4543
  }
4360
4544
  }
4361
4545
  async function runMediaServer() {
4362
- 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");
4363
4547
  const webrtc = webrtcEnabled && await loadWebRtc();
4364
4548
  const viewers = /* @__PURE__ */ new Map();
4365
4549
  const videoAssembler = new H264RtpAssembler();
@@ -4510,109 +4694,17 @@ async function runMediaServer() {
4510
4694
  if (parsed) broadcast(mediaPacket(3 /* Audio */, audioClock.microseconds(parsed.timestamp), parsed.payload));
4511
4695
  };
4512
4696
  const env = { ...process.env, DISPLAY: display };
4513
- const videoEncoder = superviseEncoder({
4514
- label: "video",
4515
- log: (message) => record("video encoder", message),
4516
- // Initial encoder work can leave its first image older than the outgoing stream.
4517
- warmupFrames: 2,
4518
- env,
4697
+ const videoEncoder = superviseVideoEncoder({
4698
+ display,
4699
+ width,
4700
+ height,
4701
+ fps,
4702
+ colorMode,
4703
+ params: encoderParams,
4519
4704
  onPacket: onVideoPacket,
4520
- joinable: (packet) => isKeyframeStart(rtpPayload(packet)),
4521
- args: (udpPort, warmupFrames) => {
4522
- const { quality, keyframeMs, profile: videoProfile, intraRefresh, pixelFormat, chromaQpOffset } = encoderParams();
4523
- const keyframeInterval = Math.max(1, Math.round(fps * keyframeMs / 1e3));
4524
- const filters = [
4525
- // Skip rapid catch-up captures after a stall; reset after backward clock adjustments.
4526
- `select='isnan(prev_selected_t)+lt(t,prev_selected_t)+gte(t-prev_selected_t,${0.5 / fps})'`,
4527
- colorMode === "bt709" ? `scale=out_color_matrix=bt709:out_range=tv,format=${pixelFormat}` : `format=${pixelFormat}`
4528
- ];
4529
- const x264 = [
4530
- "repeat-headers=1",
4531
- "sliced-threads=1",
4532
- "slices=4",
4533
- `keyint=${keyframeInterval}`,
4534
- `min-keyint=${keyframeInterval}`,
4535
- "scenecut=0",
4536
- `chroma-qp-offset=${chromaQpOffset}`,
4537
- ...intraRefresh ? ["intra-refresh=1"] : []
4538
- ];
4539
- return [
4540
- "-hide_banner",
4541
- "-loglevel",
4542
- "warning",
4543
- "-fflags",
4544
- "nobuffer",
4545
- "-flags",
4546
- "low_delay",
4547
- "-probesize",
4548
- "32",
4549
- "-analyzeduration",
4550
- "0",
4551
- // Raw 1080p frames are 8MB each; a shallow capture queue bounds memory
4552
- // and prevents stalled encoding from retaining seconds of stale frames.
4553
- "-thread_queue_size",
4554
- "1",
4555
- "-f",
4556
- "x11grab",
4557
- "-draw_mouse",
4558
- "0",
4559
- "-framerate",
4560
- String(fps),
4561
- "-video_size",
4562
- `${width}x${height}`,
4563
- "-use_wallclock_as_timestamps",
4564
- "1",
4565
- "-i",
4566
- `${display}.0`,
4567
- "-vf",
4568
- filters.join(","),
4569
- ...warmupFrames ? ["-force_key_frames", `expr:eq(n,${warmupFrames})`, "-forced-idr", "1"] : [],
4570
- "-an",
4571
- "-c:v",
4572
- "libx264",
4573
- "-preset",
4574
- preset,
4575
- "-tune",
4576
- "zerolatency",
4577
- "-threads",
4578
- String(Math.min(4, availableParallelism())),
4579
- "-profile:v",
4580
- videoProfile,
4581
- "-level:v",
4582
- "4.2",
4583
- "-pix_fmt",
4584
- pixelFormat,
4585
- ...colorMode === "bt709" ? ["-color_primaries", "bt709", "-color_trc", "bt709", "-colorspace", "bt709", "-color_range", "tv"] : [],
4586
- "-bf",
4587
- "0",
4588
- "-g",
4589
- String(keyframeInterval),
4590
- "-keyint_min",
4591
- String(keyframeInterval),
4592
- "-sc_threshold",
4593
- "0",
4594
- "-crf",
4595
- String(quality.crf),
4596
- "-maxrate",
4597
- `${quality.kbps}k`,
4598
- "-bufsize",
4599
- `${Math.round(quality.kbps * Math.min(keyframeMs, vbvMs) / 1e3)}k`,
4600
- "-x264-params",
4601
- x264.join(":"),
4602
- "-fps_mode",
4603
- "passthrough",
4604
- "-flush_packets",
4605
- "1",
4606
- "-f",
4607
- "rtp",
4608
- "-payload_type",
4609
- String(H264_PROFILES[videoProfile].payloadType),
4610
- "-ssrc",
4611
- String(VIDEO_SSRC),
4612
- `rtp://127.0.0.1:${udpPort}?pkt_size=1200&connect=1&rtcpport=${udpPort}`
4613
- ];
4614
- }
4705
+ record
4615
4706
  });
4707
+ const expectedFps = () => videoEncoder.capture().captureBackend === "native" ? Math.max(1, Math.min(fps, encoderFps)) : fps;
4616
4708
  const audioEncoder = superviseEncoder({
4617
4709
  label: "audio",
4618
4710
  log: (message) => record("audio encoder", message),
@@ -4749,13 +4841,13 @@ async function runMediaServer() {
4749
4841
  const stableSince = stableRttWindows.get(viewer)?.since;
4750
4842
  return [
4751
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`] : [],
4752
- ...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`] : [],
4753
4845
  ...(stats.lossPercent ?? 0) > 3 ? [`loss ${(stats.lossPercent ?? 0).toFixed(1)}%`] : [],
4754
4846
  ...(stats.jitterBufferMs ?? 0) > 80 ? [`jitter buffer ${(stats.jitterBufferMs ?? 0).toFixed(0)} ms`] : []
4755
4847
  ];
4756
4848
  };
4757
4849
  const lowFpsObservationAt = /* @__PURE__ */ new WeakMap();
4758
- 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);
4759
4851
  let lastSevereDescentAt = 0;
4760
4852
  const descendSevere = (now, trigger) => {
4761
4853
  comfortableSeconds = 0;
@@ -4781,7 +4873,9 @@ async function runMediaServer() {
4781
4873
  bytesAtTick = videoBytes;
4782
4874
  if (recentVideoKbps.push(videoKbps) > 3) recentVideoKbps.shift();
4783
4875
  if (viewers.size === 0) return;
4784
- 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) {
4785
4879
  cleanTransportSeconds = 0;
4786
4880
  congestedSeconds = 0;
4787
4881
  comfortableSeconds = 0;
@@ -4809,7 +4903,7 @@ async function runMediaServer() {
4809
4903
  }
4810
4904
  const now = Date.now();
4811
4905
  const reporting = [...viewers.values()].filter((viewer) => viewer.stats !== void 0 && viewer.stats.videoPacketsPerSecond !== 0 && now - viewer.stats.at < 3e3 && now - viewer.connectedAt > 2500);
4812
- 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;
4813
4907
  if (reporting.length > 0 && reporting.every((viewer) => severe(viewer, viewer.stats))) {
4814
4908
  descendSevere(now, reporting.map((viewer) => `${(viewer.stats.lossPercent ?? 0).toFixed(1)}% loss, ${viewer.stats.videoFps.toFixed(0)} fps decoded`).join(" | "));
4815
4909
  return;
@@ -4879,7 +4973,7 @@ async function runMediaServer() {
4879
4973
  kbps: videoKbps,
4880
4974
  adaptive
4881
4975
  };
4882
- 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() };
4883
4977
  const sample = {
4884
4978
  at: now,
4885
4979
  encoder: { rung, crf: quality.crf, kbpsCap: quality.kbps, keyframeMs, preset, fps: encoderFps, kbps: videoKbps },
@@ -4978,11 +5072,13 @@ async function runMediaServer() {
4978
5072
  nacks: optional(message.nacks, 1e5),
4979
5073
  freezes: optional(message.freezes, 1e3),
4980
5074
  jitterBufferMs: optional(message.jitterBufferMs, 6e4),
5075
+ decodeMs: optional(message.decodeMs, 6e4),
5076
+ receiverProcessingMs: optional(message.receiverProcessingMs, 6e4),
4981
5077
  availableKbps: optional(message.availableKbps, 1e6),
4982
5078
  playbackDroppedPercent: optional(message.playbackDroppedPercent, 100)
4983
5079
  };
4984
5080
  const now = viewer.stats.at;
4985
- if (viewer.stats.videoFps >= fps * 0.6 || viewer.stats.videoPacketsPerSecond === 0) {
5081
+ if (viewer.stats.videoFps >= expectedFps() * 0.6 || viewer.stats.videoPacketsPerSecond === 0) {
4986
5082
  viewer.lowFpsReports = 0;
4987
5083
  lowFpsObservationAt.delete(viewer);
4988
5084
  } else if (now - (lowFpsObservationAt.get(viewer) ?? 0) >= 1e3) {
@@ -4995,7 +5091,7 @@ async function runMediaServer() {
4995
5091
  const rtt = viewer.rttFloorMs;
4996
5092
  let window = stableRttWindows.get(viewer);
4997
5093
  const stableNetwork = !viewer.fallback && rtt > viewer.rtcBaselineMs + 20 && (viewer.stats.videoPacketsPerSecond ?? 0) > 0 && (viewer.stats.lossPercent ?? 0) <= 1 && (viewer.stats.jitterBufferMs ?? 0) <= 80;
4998
- if (stableNetwork && viewer.stats.videoFps >= fps * 0.9) {
5094
+ if (stableNetwork && viewer.stats.videoFps >= expectedFps() * 0.9) {
4999
5095
  if (!window || now - window.at > 3e3 || Math.max(window.max, rtt) - Math.min(window.min, rtt) > 20) {
5000
5096
  window = { since: now, at: now, min: rtt, max: rtt };
5001
5097
  }
@@ -5007,7 +5103,7 @@ async function runMediaServer() {
5007
5103
  viewer.rtcBaselineMs = window.min;
5008
5104
  stableRttWindows.delete(viewer);
5009
5105
  }
5010
- } 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) {
5011
5107
  stableRttWindows.delete(viewer);
5012
5108
  } else {
5013
5109
  window.min = Math.min(window.min, rtt);
@@ -5026,7 +5122,7 @@ async function runMediaServer() {
5026
5122
  }
5027
5123
  }
5028
5124
  if (viewer.stats.videoFps > 0) viewer.hasDecodedFrame = true;
5029
- if (viewer.stats.videoFps >= fps * 0.8) viewer.everHealthy = true;
5125
+ if (viewer.stats.videoFps >= expectedFps() * 0.8) viewer.everHealthy = true;
5030
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");
5031
5127
  return;
5032
5128
  }
@@ -5088,6 +5184,7 @@ async function runMediaServer() {
5088
5184
  audioBytes,
5089
5185
  videoKbps,
5090
5186
  encoderFps,
5187
+ ...videoEncoder.capture(),
5091
5188
  cpu,
5092
5189
  clientStats: all.flatMap((viewer) => viewer.stats ? [viewer.stats] : []),
5093
5190
  clientPacer: all.map((viewer) => viewer.rtc?.snapshot() ?? null),
@@ -5114,8 +5211,8 @@ async function runMediaServer() {
5114
5211
  return;
5115
5212
  }
5116
5213
  const requested = url.pathname === "/" ? "stream-index.html" : url.pathname.slice(1);
5117
- const path = resolve2(webroot, requested);
5118
- 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()) {
5119
5216
  response.statusCode = 404;
5120
5217
  response.end("Not found");
5121
5218
  return;
@@ -5126,7 +5223,7 @@ async function runMediaServer() {
5126
5223
  if (viewers.size === 0) stopEncodersSoon();
5127
5224
  response.setHeader("cache-control", "no-store");
5128
5225
  const digest = createHash3("sha1");
5129
- 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)));
5130
5227
  response.end(readFileSync6(path, "utf8").replaceAll("__VERSION__", `${COMPUTER_USE_VERSION}-${digest.digest("hex").slice(0, 8)}`));
5131
5228
  return;
5132
5229
  }
@@ -5268,9 +5365,9 @@ async function runMediaServer() {
5268
5365
 
5269
5366
  // src/setup.ts
5270
5367
  import { spawnSync as spawnSync6 } from "child_process";
5271
- import { accessSync, chmodSync as chmodSync2, constants, cpSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync5 } from "fs";
5272
- import { dirname as dirname7, resolve as resolve3 } from "path";
5273
- 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";
5274
5371
  var dependencies = [
5275
5372
  ["bash", "bash"],
5276
5373
  ["Node.js", "node"],
@@ -5330,7 +5427,7 @@ function commandVersion(command) {
5330
5427
  function hasCommand(command) {
5331
5428
  return (process.env.PATH ?? "").split(":").some((directory) => {
5332
5429
  try {
5333
- accessSync(resolve3(directory, command), constants.X_OK);
5430
+ accessSync(resolve4(directory, command), constants.X_OK);
5334
5431
  return true;
5335
5432
  } catch {
5336
5433
  return false;
@@ -5366,10 +5463,10 @@ async function setupComputer(options = {}) {
5366
5463
  missing.push("WebRTC native module (node-datachannel)");
5367
5464
  console.error("\u2717 WebRTC (node-datachannel) failed to load");
5368
5465
  }
5369
- const noVncPackageRoot = dirname7(dirname7(fileURLToPath2(import.meta.resolve("@novnc/novnc"))));
5370
- const noVncCore = resolve3(noVncPackageRoot, "core");
5371
- const noVncVendor = resolve3(noVncPackageRoot, "vendor");
5372
- 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)) {
5373
5470
  missing.push("bundled noVNC client");
5374
5471
  console.error("\u2717 bundled noVNC client");
5375
5472
  } else {
@@ -5384,38 +5481,38 @@ async function setupComputer(options = {}) {
5384
5481
  if (missing.length > 0) throw new Error(`Missing ${missing.length} computer-use requirement${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}`);
5385
5482
  if (options.checkOnly) return;
5386
5483
  if (process.getuid?.() !== 0) throw new Error("replicas-computer setup must run as root to install runtime files");
5387
- const packageRoot2 = resolve3(dirname7(fileURLToPath2(import.meta.url)), "..");
5388
- const assets = resolve3(packageRoot2, "assets", "desktop");
5389
- 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}`);
5390
5487
  mkdirSync6("/usr/local/share/replicas/desktop", { recursive: true });
5391
5488
  cpSync(assets, "/usr/local/share/replicas/desktop", { recursive: true, force: true });
5392
5489
  const fontRoot = "/usr/local/share/fonts/replicas";
5393
5490
  mkdirSync6(fontRoot, { recursive: true });
5394
- 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 });
5395
5492
  spawnSync6("fc-cache", ["-f", fontRoot], { stdio: "ignore" });
5396
5493
  const viewerRoot = "/usr/local/share/replicas/desktop/novnc";
5397
5494
  mkdirSync6(viewerRoot, { recursive: true });
5398
- cpSync(noVncCore, resolve3(viewerRoot, "core"), { recursive: true, force: true });
5399
- 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 });
5400
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"]) {
5401
- 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 });
5402
5499
  }
5403
- 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 });
5404
5501
  const streamRoot = "/usr/local/share/replicas/desktop/stream";
5405
5502
  mkdirSync6(streamRoot, { recursive: true });
5406
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"]) {
5407
- cpSync(resolve3(assets, file), resolve3(streamRoot, file), { force: true });
5504
+ cpSync(resolve4(assets, file), resolve4(streamRoot, file), { force: true });
5408
5505
  }
5409
- cpSync(resolve3(packageRoot2, "dist", "stream-viewer.js"), resolve3(streamRoot, "stream-viewer.js"), { force: true });
5410
- cpSync(resolve3(assets, "start-services.sh"), "/usr/local/bin/replicas-start-desktop-services", { force: true });
5411
- cpSync(resolve3(assets, "replicas-chrome"), "/usr/local/bin/replicas-chrome", { force: true });
5412
- cpSync(resolve3(assets, "replicas-terminal"), "/usr/local/bin/replicas-terminal", { force: true });
5413
- 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 });
5414
5511
  chmodSync2("/usr/local/bin/replicas-start-desktop-services", 493);
5415
5512
  chmodSync2("/usr/local/bin/replicas-chrome", 493);
5416
5513
  chmodSync2("/usr/local/bin/replicas-terminal", 493);
5417
5514
  writeFileSync5("/etc/systemd/system/replicas-computer.service", serviceUnit, { mode: 420 });
5418
- if (existsSync7("/run/systemd/system")) {
5515
+ if (existsSync8("/run/systemd/system")) {
5419
5516
  const reload = spawnSync6("systemctl", ["daemon-reload"], { stdio: "pipe" });
5420
5517
  if (reload.status !== 0) throw new Error(`Failed to reload systemd: ${reload.stderr?.toString().trim() || `exit ${reload.status}`}`);
5421
5518
  }