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/README.md +17 -7
- package/assets/desktop/replicas-chrome +7 -0
- package/dist/index.js +519 -422
- package/dist/stream-viewer.js +18 -3
- 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 +11 -3
- package/patches/libjuice-ice-role-presence.patch +96 -0
- package/patches/libyuv-chroma-rounding.patch +83 -0
- package/scripts/build-capture.sh +30 -0
- package/scripts/build-webrtc.sh +1 -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":
|
|
@@ -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
|
-
|
|
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
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
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 (
|
|
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
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
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 =
|
|
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 =
|
|
4306
|
-
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");
|
|
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 =
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4697
|
+
const videoEncoder = superviseVideoEncoder({
|
|
4698
|
+
display,
|
|
4699
|
+
width,
|
|
4700
|
+
height,
|
|
4701
|
+
fps,
|
|
4702
|
+
colorMode,
|
|
4703
|
+
params: encoderParams,
|
|
4519
4704
|
onPacket: onVideoPacket,
|
|
4520
|
-
|
|
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 <
|
|
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 <
|
|
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
|
-
|
|
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 >=
|
|
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:
|
|
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 >=
|
|
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 >=
|
|
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 <
|
|
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 >=
|
|
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 =
|
|
5118
|
-
if (!path.startsWith(
|
|
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(
|
|
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
|
|
5272
|
-
import { dirname as
|
|
5273
|
-
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";
|
|
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(
|
|
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 =
|
|
5370
|
-
const noVncCore =
|
|
5371
|
-
const noVncVendor =
|
|
5372
|
-
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)) {
|
|
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 =
|
|
5388
|
-
const assets =
|
|
5389
|
-
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}`);
|
|
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(
|
|
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,
|
|
5399
|
-
cpSync(noVncVendor,
|
|
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(
|
|
5498
|
+
cpSync(resolve4(assets, file), resolve4(viewerRoot, file === "novnc-index.html" ? "index.html" : file), { force: true });
|
|
5402
5499
|
}
|
|
5403
|
-
cpSync(
|
|
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(
|
|
5504
|
+
cpSync(resolve4(assets, file), resolve4(streamRoot, file), { force: true });
|
|
5408
5505
|
}
|
|
5409
|
-
cpSync(
|
|
5410
|
-
cpSync(
|
|
5411
|
-
cpSync(
|
|
5412
|
-
cpSync(
|
|
5413
|
-
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 });
|
|
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 (
|
|
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
|
}
|