replicas-computer 0.3.7 → 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/README.md +14 -4
- package/assets/desktop/replicas-chrome +7 -0
- package/dist/index.js +509 -423
- package/dist/stream-viewer.js +17 -2
- package/native/go.mod +3 -0
- package/native/internal/capture/fixture.go +130 -0
- package/native/internal/capture/native.go +462 -0
- package/native/internal/capture/native.h +31 -0
- package/native/internal/capture/native_test.go +22 -0
- package/native/internal/capture/options.go +7 -0
- package/native/internal/capture/yuv.go +55 -0
- package/native/internal/capture/yuv_fallback.go +23 -0
- package/native/main.go +105 -0
- package/native/options.go +52 -0
- package/package.json +10 -3
- package/patches/libyuv-chroma-rounding.patch +83 -0
- package/scripts/build-capture.sh +30 -0
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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:
|
|
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((
|
|
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.
|
|
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((
|
|
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
|
-
|
|
3034
|
+
resolve5(true);
|
|
3035
3035
|
});
|
|
3036
|
-
socket.once("error", () =>
|
|
3037
|
-
socket.once("close", () =>
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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(() =>
|
|
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((
|
|
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)
|
|
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((
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
3500
|
+
if (generation === active) active = void 0;
|
|
3498
3501
|
retire(generation);
|
|
3499
|
-
|
|
3502
|
+
options.onFailure?.(command);
|
|
3503
|
+
log(`encoder failed (${reason})${active ? "; keeping the current encoder" : ""}`);
|
|
3500
3504
|
if (!wanted) return;
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
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
|
-
|
|
3524
|
+
await spawnCandidate();
|
|
3517
3525
|
});
|
|
3518
3526
|
const replace = () => enqueue(async () => {
|
|
3519
3527
|
if (!wanted) return;
|
|
3520
|
-
|
|
3521
|
-
active = await spawnGeneration();
|
|
3522
|
-
return;
|
|
3523
|
-
}
|
|
3528
|
+
clearTimeout(restart);
|
|
3524
3529
|
clearTimeout(candidateDeadline);
|
|
3525
3530
|
retire(candidate);
|
|
3526
|
-
|
|
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/
|
|
3558
|
-
import {
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
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
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
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 (
|
|
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
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
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 =
|
|
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 =
|
|
4314
|
-
var webroot = process.env.REPLICAS_DESKTOP_STREAM_WEBROOT ||
|
|
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 =
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4697
|
+
const videoEncoder = superviseVideoEncoder({
|
|
4698
|
+
display,
|
|
4699
|
+
width,
|
|
4700
|
+
height,
|
|
4701
|
+
fps,
|
|
4702
|
+
colorMode,
|
|
4703
|
+
params: encoderParams,
|
|
4527
4704
|
onPacket: onVideoPacket,
|
|
4528
|
-
|
|
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 <
|
|
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 <
|
|
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
|
-
|
|
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 >=
|
|
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:
|
|
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 >=
|
|
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 >=
|
|
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 <
|
|
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 >=
|
|
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 =
|
|
5129
|
-
if (!path.startsWith(
|
|
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(
|
|
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
|
|
5283
|
-
import { dirname as
|
|
5284
|
-
import { fileURLToPath as
|
|
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(
|
|
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 =
|
|
5381
|
-
const noVncCore =
|
|
5382
|
-
const noVncVendor =
|
|
5383
|
-
if (!
|
|
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 =
|
|
5399
|
-
const assets =
|
|
5400
|
-
if (!
|
|
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(
|
|
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,
|
|
5410
|
-
cpSync(noVncVendor,
|
|
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(
|
|
5498
|
+
cpSync(resolve4(assets, file), resolve4(viewerRoot, file === "novnc-index.html" ? "index.html" : file), { force: true });
|
|
5413
5499
|
}
|
|
5414
|
-
cpSync(
|
|
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(
|
|
5504
|
+
cpSync(resolve4(assets, file), resolve4(streamRoot, file), { force: true });
|
|
5419
5505
|
}
|
|
5420
|
-
cpSync(
|
|
5421
|
-
cpSync(
|
|
5422
|
-
cpSync(
|
|
5423
|
-
cpSync(
|
|
5424
|
-
cpSync(
|
|
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 (
|
|
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
|
}
|