replicas-computer 0.3.15 → 0.3.17
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 +1 -1
- package/dist/index.js +163 -101
- package/dist/stream-viewer.js +104 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ The server records every decision that changes the picture (viewer connects and
|
|
|
44
44
|
| `REPLICAS_DESKTOP_STREAM_TRANSPORT` | `webrtc` | Transport for viewers whose URL does not choose one |
|
|
45
45
|
| `REPLICAS_DESKTOP_STREAM_PROFILE` | `balanced` | Encoder profile for viewers whose URL does not choose one |
|
|
46
46
|
| `REPLICAS_DESKTOP_STREAM_ADAPTIVE` | `1` | `0` pins the top quality rung by default |
|
|
47
|
-
| `REPLICAS_DESKTOP_STREAM_ICE_SERVERS` | `stun:stun.l.google.com:19302` | Comma-separated STUN/TURN servers; `turn:user:secret@host:port?transport=tcp` adds a relay. Percent-encode the username and password; the browser receives their decoded values. Re-read from the workspace runtime env on every viewer, so the monolith's `DESKTOP_STREAM_ICE_SERVERS` reaches running workspaces |
|
|
47
|
+
| `REPLICAS_DESKTOP_STREAM_ICE_SERVERS` | `stun:stun.l.google.com:19302` | Comma-separated STUN/TURN servers; `turn:user:secret@host:port?transport=tcp` adds a relay. Percent-encode the username and password; the browser receives their decoded values. Re-read from the workspace runtime env on every viewer, so the monolith's `DESKTOP_STREAM_ICE_SERVERS` reaches running workspaces. Short-lived credentials for the [TURN relay](../services/turn/README.md) are fetched from the monolith and appended; the server itself only uses UDP relays |
|
|
48
48
|
| `REPLICAS_DESKTOP_STREAM_ICE_PORTS` | ephemeral | `begin-end` UDP port range for the server's ICE candidates |
|
|
49
49
|
| `REPLICAS_DESKTOP_STREAM_WEBRTC` | `1` | `0` serves the WebSocket transport only |
|
|
50
50
|
| `REPLICAS_DESKTOP_STREAM_COLOR` | `bt709` | `legacy` disables the explicit BT.709 conversion and signalling |
|
package/dist/index.js
CHANGED
|
@@ -2937,7 +2937,7 @@ async function computerStatusCommand() {
|
|
|
2937
2937
|
}
|
|
2938
2938
|
|
|
2939
2939
|
// src/version.ts
|
|
2940
|
-
var COMPUTER_USE_VERSION = "0.3.
|
|
2940
|
+
var COMPUTER_USE_VERSION = "0.3.17";
|
|
2941
2941
|
var COMPUTER_USE_PROTOCOL_VERSION = 1;
|
|
2942
2942
|
|
|
2943
2943
|
// src/program.ts
|
|
@@ -3179,69 +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 existsSync7, readdirSync, readFileSync as
|
|
3182
|
+
import { createReadStream, existsSync as existsSync7, readdirSync, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
3183
3183
|
import { createServer as createServer2 } from "http";
|
|
3184
3184
|
import { availableParallelism as availableParallelism2, loadavg } from "os";
|
|
3185
3185
|
import { dirname as dirname7, extname, resolve as resolve3 } from "path";
|
|
3186
3186
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3187
|
-
|
|
3188
|
-
// ../shared/src/runtime-env.ts
|
|
3189
|
-
function parsePosixEnvFile(content) {
|
|
3190
|
-
const result = {};
|
|
3191
|
-
let pos = 0;
|
|
3192
|
-
while (pos < content.length) {
|
|
3193
|
-
let lineStart;
|
|
3194
|
-
if (pos === 0 || content[pos - 1] === "\n") {
|
|
3195
|
-
lineStart = pos;
|
|
3196
|
-
} else {
|
|
3197
|
-
const nl2 = content.indexOf("\n", pos);
|
|
3198
|
-
if (nl2 === -1) break;
|
|
3199
|
-
lineStart = nl2 + 1;
|
|
3200
|
-
}
|
|
3201
|
-
if (!content.startsWith("export ", lineStart)) {
|
|
3202
|
-
const nl2 = content.indexOf("\n", lineStart);
|
|
3203
|
-
if (nl2 === -1) break;
|
|
3204
|
-
pos = nl2 + 1;
|
|
3205
|
-
continue;
|
|
3206
|
-
}
|
|
3207
|
-
const nameStart = lineStart + "export ".length;
|
|
3208
|
-
const eqIdx = content.indexOf("=", nameStart);
|
|
3209
|
-
if (eqIdx === -1) break;
|
|
3210
|
-
const name = content.slice(nameStart, eqIdx).trim();
|
|
3211
|
-
let cursor = eqIdx + 1;
|
|
3212
|
-
if (content[cursor] !== "'") {
|
|
3213
|
-
const nl2 = content.indexOf("\n", cursor);
|
|
3214
|
-
pos = nl2 === -1 ? content.length : nl2 + 1;
|
|
3215
|
-
continue;
|
|
3216
|
-
}
|
|
3217
|
-
cursor += 1;
|
|
3218
|
-
let value = "";
|
|
3219
|
-
let closed = false;
|
|
3220
|
-
while (cursor < content.length) {
|
|
3221
|
-
const ch = content[cursor];
|
|
3222
|
-
if (ch === "'") {
|
|
3223
|
-
if (content.startsWith("\\''", cursor + 1)) {
|
|
3224
|
-
value += "'";
|
|
3225
|
-
cursor += 4;
|
|
3226
|
-
continue;
|
|
3227
|
-
}
|
|
3228
|
-
cursor += 1;
|
|
3229
|
-
closed = true;
|
|
3230
|
-
break;
|
|
3231
|
-
}
|
|
3232
|
-
value += ch;
|
|
3233
|
-
cursor += 1;
|
|
3234
|
-
}
|
|
3235
|
-
if (closed && name) {
|
|
3236
|
-
result[name] = value;
|
|
3237
|
-
}
|
|
3238
|
-
const nl = content.indexOf("\n", cursor);
|
|
3239
|
-
pos = nl === -1 ? content.length : nl + 1;
|
|
3240
|
-
}
|
|
3241
|
-
return result;
|
|
3242
|
-
}
|
|
3243
|
-
|
|
3244
|
-
// src/media-server.ts
|
|
3245
3187
|
import WebSocket2, { WebSocketServer } from "ws";
|
|
3246
3188
|
|
|
3247
3189
|
// src/encoder.ts
|
|
@@ -3573,13 +3515,17 @@ function superviseEncoder(options) {
|
|
|
3573
3515
|
};
|
|
3574
3516
|
}
|
|
3575
3517
|
|
|
3576
|
-
// src/
|
|
3577
|
-
import {
|
|
3578
|
-
import { availableParallelism } from "os";
|
|
3579
|
-
import { dirname as dirname6, resolve as resolve2 } from "path";
|
|
3580
|
-
import { fileURLToPath } from "url";
|
|
3518
|
+
// src/ice-servers.ts
|
|
3519
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
3581
3520
|
|
|
3582
3521
|
// ../shared/src/desktop-stream.ts
|
|
3522
|
+
function isStreamIceServer(value) {
|
|
3523
|
+
return isRecord(value) && typeof value.urls === "string" && (value.username === void 0 || typeof value.username === "string") && (value.credential === void 0 || typeof value.credential === "string");
|
|
3524
|
+
}
|
|
3525
|
+
var DESKTOP_ICE_SERVERS_PATH = "/v1/engine/desktop/ice-servers";
|
|
3526
|
+
function isDesktopIceServers(value) {
|
|
3527
|
+
return isRecord(value) && typeof value.expiresAt === "number" && Array.isArray(value.iceServers) && value.iceServers.every(isStreamIceServer);
|
|
3528
|
+
}
|
|
3583
3529
|
var DEFAULT_STREAM_REQUEST = {
|
|
3584
3530
|
transport: "webrtc",
|
|
3585
3531
|
profile: "balanced",
|
|
@@ -3636,6 +3582,144 @@ function isDesktopWebRtcFailure(value) {
|
|
|
3636
3582
|
].every((key) => typeof value[key] === "string" && value[key].length <= 200) && ["elapsedMs", "candidateErrors", "framesDecoded", "packetsReceived", "packetsLost", "bytesReceived", "lastFrameAgeMs", "encoderFps"].every((key) => typeof value[key] === "number" && Number.isFinite(value[key]));
|
|
3637
3583
|
}
|
|
3638
3584
|
|
|
3585
|
+
// ../shared/src/runtime-env.ts
|
|
3586
|
+
var REPLICAS_RUNTIME_ENV_ALIASES = {
|
|
3587
|
+
monolithUrl: ["REPLICAS_MONOLITH_URL", "MONOLITH_URL"],
|
|
3588
|
+
workspaceId: ["REPLICAS_WORKSPACE_ID", "WORKSPACE_ID"],
|
|
3589
|
+
linearSessionId: ["REPLICAS_LINEAR_SESSION_ID", "LINEAR_SESSION_ID"],
|
|
3590
|
+
linearAccessToken: ["REPLICAS_LINEAR_ACCESS_TOKEN", "LINEAR_ACCESS_TOKEN"],
|
|
3591
|
+
slackBotToken: ["REPLICAS_SLACK_BOT_TOKEN", "SLACK_BOT_TOKEN"],
|
|
3592
|
+
slackChannelId: ["REPLICAS_SLACK_CHANNEL_ID", "SLACK_CHANNEL_ID"],
|
|
3593
|
+
slackThreadTs: ["REPLICAS_SLACK_THREAD_TS", "SLACK_THREAD_TS"]
|
|
3594
|
+
};
|
|
3595
|
+
function readReplicasRuntimeEnv(readEnv, [namespacedKey, legacyKey]) {
|
|
3596
|
+
return readEnv(namespacedKey) ?? readEnv(legacyKey);
|
|
3597
|
+
}
|
|
3598
|
+
function parsePosixEnvFile(content) {
|
|
3599
|
+
const result = {};
|
|
3600
|
+
let pos = 0;
|
|
3601
|
+
while (pos < content.length) {
|
|
3602
|
+
let lineStart;
|
|
3603
|
+
if (pos === 0 || content[pos - 1] === "\n") {
|
|
3604
|
+
lineStart = pos;
|
|
3605
|
+
} else {
|
|
3606
|
+
const nl2 = content.indexOf("\n", pos);
|
|
3607
|
+
if (nl2 === -1) break;
|
|
3608
|
+
lineStart = nl2 + 1;
|
|
3609
|
+
}
|
|
3610
|
+
if (!content.startsWith("export ", lineStart)) {
|
|
3611
|
+
const nl2 = content.indexOf("\n", lineStart);
|
|
3612
|
+
if (nl2 === -1) break;
|
|
3613
|
+
pos = nl2 + 1;
|
|
3614
|
+
continue;
|
|
3615
|
+
}
|
|
3616
|
+
const nameStart = lineStart + "export ".length;
|
|
3617
|
+
const eqIdx = content.indexOf("=", nameStart);
|
|
3618
|
+
if (eqIdx === -1) break;
|
|
3619
|
+
const name = content.slice(nameStart, eqIdx).trim();
|
|
3620
|
+
let cursor = eqIdx + 1;
|
|
3621
|
+
if (content[cursor] !== "'") {
|
|
3622
|
+
const nl2 = content.indexOf("\n", cursor);
|
|
3623
|
+
pos = nl2 === -1 ? content.length : nl2 + 1;
|
|
3624
|
+
continue;
|
|
3625
|
+
}
|
|
3626
|
+
cursor += 1;
|
|
3627
|
+
let value = "";
|
|
3628
|
+
let closed = false;
|
|
3629
|
+
while (cursor < content.length) {
|
|
3630
|
+
const ch = content[cursor];
|
|
3631
|
+
if (ch === "'") {
|
|
3632
|
+
if (content.startsWith("\\''", cursor + 1)) {
|
|
3633
|
+
value += "'";
|
|
3634
|
+
cursor += 4;
|
|
3635
|
+
continue;
|
|
3636
|
+
}
|
|
3637
|
+
cursor += 1;
|
|
3638
|
+
closed = true;
|
|
3639
|
+
break;
|
|
3640
|
+
}
|
|
3641
|
+
value += ch;
|
|
3642
|
+
cursor += 1;
|
|
3643
|
+
}
|
|
3644
|
+
if (closed && name) {
|
|
3645
|
+
result[name] = value;
|
|
3646
|
+
}
|
|
3647
|
+
const nl = content.indexOf("\n", cursor);
|
|
3648
|
+
pos = nl === -1 ? content.length : nl + 1;
|
|
3649
|
+
}
|
|
3650
|
+
return result;
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
// src/ice-servers.ts
|
|
3654
|
+
var runtimeEnvFile = process.env.REPLICAS_RUNTIME_ENV_FILE || "/home/user/.replicas/runtime-env.sh";
|
|
3655
|
+
var refreshMs = 60 * 6e4;
|
|
3656
|
+
var retryMs = 6e4;
|
|
3657
|
+
var relays = [];
|
|
3658
|
+
var expiresAt = 0;
|
|
3659
|
+
var refreshAt = 0;
|
|
3660
|
+
var refreshing;
|
|
3661
|
+
async function currentIceServers() {
|
|
3662
|
+
let env = process.env;
|
|
3663
|
+
try {
|
|
3664
|
+
env = { ...env, ...parsePosixEnvFile(readFileSync6(runtimeEnvFile, "utf8")) };
|
|
3665
|
+
} catch {
|
|
3666
|
+
}
|
|
3667
|
+
const monolithUrl = readReplicasRuntimeEnv((name) => env[name], REPLICAS_RUNTIME_ENV_ALIASES.monolithUrl);
|
|
3668
|
+
const workspaceId = readReplicasRuntimeEnv((name) => env[name], REPLICAS_RUNTIME_ENV_ALIASES.workspaceId);
|
|
3669
|
+
const engineSecret = env.REPLICAS_ENGINE_SECRET;
|
|
3670
|
+
if (monolithUrl && workspaceId && engineSecret && Date.now() >= refreshAt) {
|
|
3671
|
+
const refreshed = refreshing ??= refreshRelays(monolithUrl, workspaceId, engineSecret).finally(() => {
|
|
3672
|
+
refreshing = void 0;
|
|
3673
|
+
});
|
|
3674
|
+
if (expiresAt <= Date.now()) await refreshed;
|
|
3675
|
+
}
|
|
3676
|
+
return toIceServers(env.REPLICAS_DESKTOP_STREAM_ICE_SERVERS, expiresAt > Date.now() ? relays : []);
|
|
3677
|
+
}
|
|
3678
|
+
async function refreshRelays(monolithUrl, workspaceId, engineSecret) {
|
|
3679
|
+
try {
|
|
3680
|
+
const response = await fetch(new URL(DESKTOP_ICE_SERVERS_PATH, monolithUrl), {
|
|
3681
|
+
headers: { authorization: `Bearer ${engineSecret}`, "x-workspace-id": workspaceId },
|
|
3682
|
+
redirect: "error",
|
|
3683
|
+
signal: AbortSignal.timeout(2e3)
|
|
3684
|
+
});
|
|
3685
|
+
if (response.status === 404) {
|
|
3686
|
+
relays = [];
|
|
3687
|
+
expiresAt = 0;
|
|
3688
|
+
} else {
|
|
3689
|
+
const body = response.ok ? await response.json() : void 0;
|
|
3690
|
+
if (!isDesktopIceServers(body)) throw new Error(`HTTP ${response.status}`);
|
|
3691
|
+
({ iceServers: relays, expiresAt } = body);
|
|
3692
|
+
}
|
|
3693
|
+
refreshAt = Date.now() + refreshMs;
|
|
3694
|
+
} catch (error) {
|
|
3695
|
+
process.stderr.write(`[desktop-stream] TURN credentials unavailable: ${error instanceof Error ? error.message : String(error)}
|
|
3696
|
+
`);
|
|
3697
|
+
refreshAt = Date.now() + retryMs;
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
function toIceServers(spec = "stun:stun.l.google.com:19302", relayServers) {
|
|
3701
|
+
const browser = spec.split(",").map((url) => url.trim()).filter(Boolean).map((url) => {
|
|
3702
|
+
const match = url.match(/^(turns?):([^:@]+):(.+)@([^@]+)$/);
|
|
3703
|
+
if (!match) return { urls: url };
|
|
3704
|
+
const [username, credential] = match.slice(2, 4).map((value) => {
|
|
3705
|
+
try {
|
|
3706
|
+
return decodeURIComponent(value);
|
|
3707
|
+
} catch {
|
|
3708
|
+
return value;
|
|
3709
|
+
}
|
|
3710
|
+
});
|
|
3711
|
+
return { urls: `${match[1]}:${match[4]}`, username, credential };
|
|
3712
|
+
}).concat(relayServers);
|
|
3713
|
+
const native = browser.filter(({ urls }) => !/^turns:|[?&]transport=tcp/i.test(urls)).map(({ urls, username, credential }) => username === void 0 || credential === void 0 ? urls : urls.replace(/^turn:/i, `turn:${encodeURIComponent(username)}:${encodeURIComponent(credential)}@`));
|
|
3714
|
+
return { native, browser };
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
// src/video-encoder.ts
|
|
3718
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3719
|
+
import { availableParallelism } from "os";
|
|
3720
|
+
import { dirname as dirname6, resolve as resolve2 } from "path";
|
|
3721
|
+
import { fileURLToPath } from "url";
|
|
3722
|
+
|
|
3639
3723
|
// src/input-validation.ts
|
|
3640
3724
|
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
3641
3725
|
var optionalFinite = (value) => value === void 0 || finite(value);
|
|
@@ -3951,22 +4035,6 @@ async function loadWebRtc() {
|
|
|
3951
4035
|
}
|
|
3952
4036
|
}
|
|
3953
4037
|
var cleanupWebRtc = () => ndc?.cleanup();
|
|
3954
|
-
function parseIceServers(spec) {
|
|
3955
|
-
const native = (spec ?? "stun:stun.l.google.com:19302").split(",").map((server) => server.trim()).filter(Boolean);
|
|
3956
|
-
const browser = native.map((url) => {
|
|
3957
|
-
const match = url.match(/^(turns?):([^:@]+):(.+)@([^@]+)$/);
|
|
3958
|
-
if (!match) return { urls: url };
|
|
3959
|
-
const [username, credential] = match.slice(2, 4).map((value) => {
|
|
3960
|
-
try {
|
|
3961
|
-
return decodeURIComponent(value);
|
|
3962
|
-
} catch {
|
|
3963
|
-
return value;
|
|
3964
|
-
}
|
|
3965
|
-
});
|
|
3966
|
-
return { urls: `${match[1]}:${match[4]}`, username, credential };
|
|
3967
|
-
});
|
|
3968
|
-
return { native, browser };
|
|
3969
|
-
}
|
|
3970
4038
|
var WebRtcViewer = class {
|
|
3971
4039
|
constructor(options, handlers) {
|
|
3972
4040
|
this.handlers = handlers;
|
|
@@ -4478,7 +4546,6 @@ var standbyRung = 5;
|
|
|
4478
4546
|
var port = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_PORT || String(DESKTOP_STREAM_PORT), 10);
|
|
4479
4547
|
var display = process.env.REPLICAS_DESKTOP_DISPLAY?.replace(/^([^:])/, ":$1") || ":99";
|
|
4480
4548
|
var webrtcEnabled = process.env.REPLICAS_DESKTOP_STREAM_WEBRTC !== "0";
|
|
4481
|
-
var runtimeEnvFile = process.env.REPLICAS_RUNTIME_ENV_FILE || "/home/user/.replicas/runtime-env.sh";
|
|
4482
4549
|
var colorMode = process.env.REPLICAS_DESKTOP_STREAM_COLOR === "legacy" ? "legacy" : "bt709";
|
|
4483
4550
|
var defaultRequest = parseStreamRequest(new URLSearchParams({
|
|
4484
4551
|
transport: process.env.REPLICAS_DESKTOP_STREAM_TRANSPORT ?? "",
|
|
@@ -4534,14 +4601,6 @@ function mediaPacket(kind, timestamp, payload) {
|
|
|
4534
4601
|
header.writeBigUInt64BE(BigInt(Math.max(0, timestamp)), 4);
|
|
4535
4602
|
return Buffer.concat([header, payload]);
|
|
4536
4603
|
}
|
|
4537
|
-
function currentIceServers() {
|
|
4538
|
-
let spec = process.env.REPLICAS_DESKTOP_STREAM_ICE_SERVERS;
|
|
4539
|
-
try {
|
|
4540
|
-
spec = parsePosixEnvFile(readFileSync6(runtimeEnvFile, "utf8")).REPLICAS_DESKTOP_STREAM_ICE_SERVERS ?? spec;
|
|
4541
|
-
} catch {
|
|
4542
|
-
}
|
|
4543
|
-
return parseIceServers(spec);
|
|
4544
|
-
}
|
|
4545
4604
|
var appliedTheme;
|
|
4546
4605
|
var pendingTheme;
|
|
4547
4606
|
var applyingTheme = false;
|
|
@@ -4567,7 +4626,7 @@ function drainDesktopTheme() {
|
|
|
4567
4626
|
function processCpuMs(pid) {
|
|
4568
4627
|
if (!pid) return 0;
|
|
4569
4628
|
try {
|
|
4570
|
-
const stat =
|
|
4629
|
+
const stat = readFileSync7(`/proc/${pid}/stat`, "utf8");
|
|
4571
4630
|
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
4572
4631
|
return (Number(fields[11]) + Number(fields[12])) * 10;
|
|
4573
4632
|
} catch {
|
|
@@ -5252,10 +5311,13 @@ async function runMediaServer() {
|
|
|
5252
5311
|
// A socket viewer whose WebRTC attempt failed tries again in the background;
|
|
5253
5312
|
// the socket keeps carrying its media until the new peer shows video.
|
|
5254
5313
|
case "retry-webrtc": {
|
|
5255
|
-
const
|
|
5256
|
-
if (!
|
|
5257
|
-
|
|
5258
|
-
|
|
5314
|
+
const retryable = () => webrtc && viewer.fallback && !viewer.rtc && viewer.request.transport === "webrtc" && viewer.socket.readyState === WebSocket2.OPEN;
|
|
5315
|
+
if (!retryable()) return;
|
|
5316
|
+
void currentIceServers().then((ice) => {
|
|
5317
|
+
if (!retryable()) return;
|
|
5318
|
+
record("retrying webrtc", "socket media continues meanwhile");
|
|
5319
|
+
openWebRtc(viewer, reply, ice.native);
|
|
5320
|
+
});
|
|
5259
5321
|
return;
|
|
5260
5322
|
}
|
|
5261
5323
|
case "webrtc-ready":
|
|
@@ -5359,10 +5421,11 @@ async function runMediaServer() {
|
|
|
5359
5421
|
if (extname(path) === ".html") {
|
|
5360
5422
|
ensureEncoders();
|
|
5361
5423
|
if (viewers.size === 0) stopEncodersSoon();
|
|
5424
|
+
if (webrtc) void currentIceServers();
|
|
5362
5425
|
response.setHeader("cache-control", "no-store");
|
|
5363
5426
|
const digest = createHash3("sha1");
|
|
5364
|
-
for (const name of readdirSync(webroot).filter((file) => /\.(js|css)$/.test(file)).sort()) digest.update(
|
|
5365
|
-
response.end(
|
|
5427
|
+
for (const name of readdirSync(webroot).filter((file) => /\.(js|css)$/.test(file)).sort()) digest.update(readFileSync7(resolve3(webroot, name)));
|
|
5428
|
+
response.end(readFileSync7(path, "utf8").replaceAll("__VERSION__", `${COMPUTER_USE_VERSION}-${digest.digest("hex").slice(0, 8)}`));
|
|
5366
5429
|
return;
|
|
5367
5430
|
}
|
|
5368
5431
|
response.setHeader("cache-control", "private, max-age=31536000, immutable");
|
|
@@ -5375,9 +5438,10 @@ async function runMediaServer() {
|
|
|
5375
5438
|
socket.destroy();
|
|
5376
5439
|
return;
|
|
5377
5440
|
}
|
|
5378
|
-
|
|
5441
|
+
const streamRequest = parseStreamRequest(url.searchParams, defaultRequest);
|
|
5442
|
+
void (webrtc && streamRequest.transport === "webrtc" ? currentIceServers() : Promise.resolve(void 0)).then((ice) => webSocketServer.handleUpgrade(request, socket, head, (client) => webSocketServer.emit("connection", client, streamRequest, url.searchParams.get("standby") === "1", ice)));
|
|
5379
5443
|
});
|
|
5380
|
-
webSocketServer.on("connection", (socket, request, standby) => {
|
|
5444
|
+
webSocketServer.on("connection", (socket, request, standby, ice) => {
|
|
5381
5445
|
const viewer = {
|
|
5382
5446
|
socket,
|
|
5383
5447
|
input: new InputBridge(display, width, height),
|
|
@@ -5411,8 +5475,6 @@ async function runMediaServer() {
|
|
|
5411
5475
|
const reply = (message) => {
|
|
5412
5476
|
if (socket.readyState === WebSocket2.OPEN) socket.send(JSON.stringify(message));
|
|
5413
5477
|
};
|
|
5414
|
-
const offerWebRtc = webrtc && request.transport === "webrtc";
|
|
5415
|
-
const ice = offerWebRtc ? currentIceServers() : void 0;
|
|
5416
5478
|
const fallbackProfile = profile() === "high444" ? "main" : profile();
|
|
5417
5479
|
const configMessage = { ...config, videoCodec: H264_PROFILES[fallbackProfile].codec, iceServers: ice?.browser, request };
|
|
5418
5480
|
reply(configMessage);
|
package/dist/stream-viewer.js
CHANGED
|
@@ -4,6 +4,9 @@ function isRecord(value) {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
// ../shared/src/desktop-stream.ts
|
|
7
|
+
function isStreamIceServer(value) {
|
|
8
|
+
return isRecord(value) && typeof value.urls === "string" && (value.username === void 0 || typeof value.username === "string") && (value.credential === void 0 || typeof value.credential === "string");
|
|
9
|
+
}
|
|
7
10
|
var DEFAULT_STREAM_REQUEST = {
|
|
8
11
|
transport: "webrtc",
|
|
9
12
|
profile: "balanced",
|
|
@@ -46,13 +49,10 @@ var DESKTOP_WEBRTC_FAILURE_REASONS = {
|
|
|
46
49
|
full_chroma_undecodable: "WebRTC could not decode H.264 High 4:4:4"
|
|
47
50
|
};
|
|
48
51
|
var DESKTOP_QUALITY_MESSAGE = "replicas-desktop-quality";
|
|
52
|
+
var DESKTOP_CONNECTION_MESSAGE = "replicas-desktop-connection";
|
|
49
53
|
|
|
50
54
|
// src/input-validation.ts
|
|
51
55
|
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
52
|
-
var iceServer = (value) => {
|
|
53
|
-
if (!isRecord(value)) return false;
|
|
54
|
-
return typeof value.urls === "string" && (value.username === void 0 || typeof value.username === "string") && (value.credential === void 0 || typeof value.credential === "string");
|
|
55
|
-
};
|
|
56
56
|
function validServerMessage(value) {
|
|
57
57
|
if (!isRecord(value) || typeof value.type !== "string") return false;
|
|
58
58
|
switch (value.type) {
|
|
@@ -66,7 +66,7 @@ function validServerMessage(value) {
|
|
|
66
66
|
case "candidate":
|
|
67
67
|
return typeof value.candidate === "string" && value.candidate.length < 1024 && typeof value.mid === "string" && value.mid.length < 16;
|
|
68
68
|
case "config":
|
|
69
|
-
return finite(value.protocolVersion) && (value.serverVersion === void 0 || typeof value.serverVersion === "string") && (value.webrtcUnavailableReason === void 0 || typeof value.webrtcUnavailableReason === "string") && (value.inputRace === void 0 || typeof value.inputRace === "boolean") && finite(value.width) && finite(value.height) && finite(value.fps) && typeof value.videoCodec === "string" && typeof value.audioCodec === "string" && finite(value.audioSampleRate) && finite(value.audioChannels) && isRecord(value.request) && (value.iceServers === void 0 || Array.isArray(value.iceServers) && value.iceServers.every(
|
|
69
|
+
return finite(value.protocolVersion) && (value.serverVersion === void 0 || typeof value.serverVersion === "string") && (value.webrtcUnavailableReason === void 0 || typeof value.webrtcUnavailableReason === "string") && (value.inputRace === void 0 || typeof value.inputRace === "boolean") && finite(value.width) && finite(value.height) && finite(value.fps) && typeof value.videoCodec === "string" && typeof value.audioCodec === "string" && finite(value.audioSampleRate) && finite(value.audioChannels) && isRecord(value.request) && (value.iceServers === void 0 || Array.isArray(value.iceServers) && value.iceServers.every(isStreamIceServer));
|
|
70
70
|
case "telemetry":
|
|
71
71
|
return isRecord(value.encoder) && isRecord(value.cpu) && isRecord(value.viewer) && Array.isArray(value.events);
|
|
72
72
|
default:
|
|
@@ -103,6 +103,7 @@ var webrtcFirstFrameMs = 3e3;
|
|
|
103
103
|
var webrtcRetryFirstMs = 15e3;
|
|
104
104
|
var webrtcRetryMaxMs = 3e5;
|
|
105
105
|
var webrtcRetryTimeoutMs = 1e4;
|
|
106
|
+
var hiddenDisconnectMs = 6e4;
|
|
106
107
|
var webrtcSkipsAfterFailure = 3;
|
|
107
108
|
var pointerIntervalMs = 4;
|
|
108
109
|
var qualityWindowMs = 3e4;
|
|
@@ -114,8 +115,10 @@ try {
|
|
|
114
115
|
} catch {
|
|
115
116
|
}
|
|
116
117
|
var socket;
|
|
117
|
-
var
|
|
118
|
+
var intersecting = innerWidth > 0 && innerHeight > 0;
|
|
119
|
+
var standby = !intersecting;
|
|
118
120
|
var standbyTimer;
|
|
121
|
+
var hiddenTimer;
|
|
119
122
|
var config;
|
|
120
123
|
var transport;
|
|
121
124
|
var transportReason = "";
|
|
@@ -172,6 +175,24 @@ var restartQuality = () => {
|
|
|
172
175
|
qualitySequence = 0;
|
|
173
176
|
presentedAt = 0;
|
|
174
177
|
};
|
|
178
|
+
var newConnectionTotals = () => ({
|
|
179
|
+
sampledAt: 0,
|
|
180
|
+
presented: 0,
|
|
181
|
+
shownMs: 0,
|
|
182
|
+
frames: 0,
|
|
183
|
+
samples: 0,
|
|
184
|
+
rung: 0,
|
|
185
|
+
worstRung: 0,
|
|
186
|
+
cpu: 0,
|
|
187
|
+
rtt: 0,
|
|
188
|
+
rttSamples: 0,
|
|
189
|
+
worstRtt: 0,
|
|
190
|
+
loss: 0,
|
|
191
|
+
lossSamples: 0,
|
|
192
|
+
longestGapMs: 0,
|
|
193
|
+
reportedAt: 0
|
|
194
|
+
});
|
|
195
|
+
var connectionTotals = newConnectionTotals();
|
|
175
196
|
var state = {};
|
|
176
197
|
globalThis.__replicasDesktop = state;
|
|
177
198
|
var viewerEvents = [];
|
|
@@ -258,6 +279,31 @@ var announceTransport = () => {
|
|
|
258
279
|
if (window.parent === window) return;
|
|
259
280
|
window.parent.postMessage({ type: DESKTOP_TRANSPORT_MESSAGE, transport: transport ?? "connecting", reason: transportReason, path: route.path, codec: route.codec, failure, recovery }, "*");
|
|
260
281
|
};
|
|
282
|
+
var average = (sum, count) => count ? Math.round(sum * 10 / count) / 10 : void 0;
|
|
283
|
+
var reportConnection = (ended) => {
|
|
284
|
+
if (window.parent === window || !config) return;
|
|
285
|
+
const totals = connectionTotals;
|
|
286
|
+
totals.reportedAt = performance.now();
|
|
287
|
+
window.parent.postMessage({
|
|
288
|
+
type: DESKTOP_CONNECTION_MESSAGE,
|
|
289
|
+
serverVersion: config.serverVersion ?? "unknown",
|
|
290
|
+
firstFrameMs: totals.firstFrameAt === void 0 ? void 0 : Math.round(totals.firstFrameAt - connectionStartedAt),
|
|
291
|
+
shownMs: Math.round(totals.shownMs),
|
|
292
|
+
displayedFps: totals.shownMs >= 1e3 ? Math.round(totals.frames * 1e4 / totals.shownMs) / 10 : void 0,
|
|
293
|
+
inputRttMs: average(totals.rtt, totals.rttSamples),
|
|
294
|
+
worstInputRttMs: totals.rttSamples ? Math.round(totals.worstRtt) : void 0,
|
|
295
|
+
qualityRung: average(totals.rung, totals.samples),
|
|
296
|
+
worstQualityRung: totals.samples ? totals.worstRung : void 0,
|
|
297
|
+
longestFrameGapMs: totals.samples ? Math.round(totals.longestGapMs) : void 0,
|
|
298
|
+
lossPercent: average(totals.loss, totals.lossSamples),
|
|
299
|
+
cpuLoadPercent: average(totals.cpu, totals.samples),
|
|
300
|
+
ended
|
|
301
|
+
}, "*");
|
|
302
|
+
};
|
|
303
|
+
var endConnection = (ended) => {
|
|
304
|
+
if (ended || connectionTotals.sampledAt) reportConnection(ended);
|
|
305
|
+
connectionTotals = newConnectionTotals();
|
|
306
|
+
};
|
|
261
307
|
var closePeer = (preserveDeadline = false) => {
|
|
262
308
|
cancelRetry();
|
|
263
309
|
if (!preserveDeadline) {
|
|
@@ -311,9 +357,11 @@ var configureVideo = async (next, preferHardware = true) => {
|
|
|
311
357
|
if (configuration !== videoConfiguration) return;
|
|
312
358
|
const selectedConfig = support.supported && support.config ? support.config : decoderConfig(next);
|
|
313
359
|
activeVideoConfig = selectedConfig;
|
|
360
|
+
const totals = connectionTotals;
|
|
314
361
|
const decoder = new VideoDecoder({
|
|
315
362
|
output: (frame) => {
|
|
316
363
|
socketDecodedFrames += 1;
|
|
364
|
+
totals.firstFrameAt ??= performance.now();
|
|
317
365
|
const writer = videoWriter;
|
|
318
366
|
if (!writer || (writer.desiredSize ?? 1) <= 0) {
|
|
319
367
|
frame.close();
|
|
@@ -777,6 +825,7 @@ var handleServerText = (data, viaSocket) => {
|
|
|
777
825
|
};
|
|
778
826
|
var connect = () => {
|
|
779
827
|
if (document.visibilityState !== "visible") return;
|
|
828
|
+
endConnection();
|
|
780
829
|
clearTimeout(reconnectTimer);
|
|
781
830
|
closePeer();
|
|
782
831
|
transport = void 0;
|
|
@@ -819,9 +868,12 @@ var connect = () => {
|
|
|
819
868
|
});
|
|
820
869
|
nextSocket.addEventListener("close", () => {
|
|
821
870
|
if (socket !== nextSocket) return;
|
|
871
|
+
endConnection();
|
|
822
872
|
closePeer();
|
|
823
|
-
transport
|
|
824
|
-
|
|
873
|
+
if (transport) {
|
|
874
|
+
transport = void 0;
|
|
875
|
+
announceTransport();
|
|
876
|
+
}
|
|
825
877
|
reconnectAttempt += 1;
|
|
826
878
|
const delay = Math.min(8e3, 250 * 2 ** Math.min(reconnectAttempt, 5)) * (0.85 + Math.random() * 0.3);
|
|
827
879
|
note("socket closed", `reconnect in ${Math.round(delay)} ms`);
|
|
@@ -856,6 +908,8 @@ var observeFrame = () => {
|
|
|
856
908
|
lastPresentedCount = metadata.presentedFrames;
|
|
857
909
|
lastFrameAt = performance.now();
|
|
858
910
|
if (presentedAt && qualityWindow) qualityWindow.longestGapMs = Math.max(qualityWindow.longestGapMs, lastFrameAt - Math.max(presentedAt, qualityWindow.at));
|
|
911
|
+
const { shownSince } = connectionTotals;
|
|
912
|
+
if (presentedAt && shownSince !== void 0) connectionTotals.longestGapMs = Math.max(connectionTotals.longestGapMs, lastFrameAt - Math.max(presentedAt, shownSince));
|
|
859
913
|
presentedAt = lastFrameAt;
|
|
860
914
|
status.classList.remove("visible");
|
|
861
915
|
observeFrame();
|
|
@@ -910,22 +964,32 @@ var setStandby = (next) => {
|
|
|
910
964
|
sendSocket({ type: "standby", standby });
|
|
911
965
|
if (!standby && !audioPrompt.hidden) unlockAudio();
|
|
912
966
|
};
|
|
913
|
-
|
|
914
|
-
if (
|
|
967
|
+
var syncStandby = () => {
|
|
968
|
+
if (intersecting && document.visibilityState === "visible") setStandby(false);
|
|
915
969
|
else if (!standby) {
|
|
916
970
|
clearTimeout(standbyTimer);
|
|
917
971
|
standbyTimer = setTimeout(() => setStandby(true), 1e3);
|
|
918
972
|
}
|
|
973
|
+
};
|
|
974
|
+
new IntersectionObserver(([entry]) => {
|
|
975
|
+
intersecting = entry.isIntersecting;
|
|
976
|
+
syncStandby();
|
|
919
977
|
}).observe(document.documentElement);
|
|
920
978
|
window.addEventListener("online", connect);
|
|
921
979
|
document.addEventListener("visibilitychange", () => {
|
|
980
|
+
clearTimeout(hiddenTimer);
|
|
981
|
+
syncStandby();
|
|
922
982
|
if (document.visibilityState === "hidden") {
|
|
923
|
-
|
|
983
|
+
hiddenTimer = setTimeout(() => {
|
|
984
|
+
endConnection("hidden");
|
|
985
|
+
disconnect();
|
|
986
|
+
}, hiddenDisconnectMs);
|
|
924
987
|
return;
|
|
925
988
|
}
|
|
926
|
-
connect();
|
|
989
|
+
if (!socket || socket.readyState >= WebSocket.CLOSING) connect();
|
|
927
990
|
});
|
|
928
991
|
window.addEventListener("pagehide", () => {
|
|
992
|
+
endConnection();
|
|
929
993
|
disconnect();
|
|
930
994
|
screen.cancelVideoFrameCallback(frameCallback);
|
|
931
995
|
videoDecoder?.close();
|
|
@@ -964,6 +1028,32 @@ var updateDiagnostics = (stats) => {
|
|
|
964
1028
|
viewerEvents.length ? `viewer events: ${describeEvents(viewerEvents.slice(-5))}` : ""
|
|
965
1029
|
].filter(Boolean).join("\n");
|
|
966
1030
|
};
|
|
1031
|
+
var sampleConnection = (stats, current, now) => {
|
|
1032
|
+
const totals = connectionTotals;
|
|
1033
|
+
const shown = !standby && document.visibilityState === "visible" && qualityUnseen.samples < 4;
|
|
1034
|
+
if (shown && totals.sampledAt) {
|
|
1035
|
+
totals.shownSince ??= totals.sampledAt;
|
|
1036
|
+
totals.shownMs += now - totals.sampledAt;
|
|
1037
|
+
totals.frames += presentedFrames - totals.presented;
|
|
1038
|
+
totals.samples += 1;
|
|
1039
|
+
totals.rung += current.encoder.rung;
|
|
1040
|
+
totals.worstRung = Math.max(totals.worstRung, current.encoder.rung);
|
|
1041
|
+
totals.cpu += current.cpu.load * 100 / current.cpu.cores;
|
|
1042
|
+
if (stats.rttMs > 0) {
|
|
1043
|
+
totals.rtt += stats.rttMs;
|
|
1044
|
+
totals.rttSamples += 1;
|
|
1045
|
+
totals.worstRtt = Math.max(totals.worstRtt, stats.rttMs);
|
|
1046
|
+
}
|
|
1047
|
+
if (stats.lossPercent !== void 0) {
|
|
1048
|
+
totals.loss += stats.lossPercent;
|
|
1049
|
+
totals.lossSamples += 1;
|
|
1050
|
+
}
|
|
1051
|
+
totals.longestGapMs = Math.max(totals.longestGapMs, now - Math.max(presentedAt, totals.shownSince));
|
|
1052
|
+
} else if (!shown) totals.shownSince = void 0;
|
|
1053
|
+
totals.sampledAt = now;
|
|
1054
|
+
totals.presented = presentedFrames;
|
|
1055
|
+
if (now - totals.reportedAt >= 2e3) reportConnection();
|
|
1056
|
+
};
|
|
967
1057
|
var reportQuality = (stats) => {
|
|
968
1058
|
if (window.parent === window || !transport || !config || !telemetry) return;
|
|
969
1059
|
const now = performance.now();
|
|
@@ -971,6 +1061,7 @@ var reportQuality = (stats) => {
|
|
|
971
1061
|
const decoded = transport === "webrtc" ? inbound?.framesDecoded ?? 0 : socketDecodedFrames;
|
|
972
1062
|
qualityUnseen = { decoded, presented: presentedFrames, samples: decoded > qualityUnseen.decoded && presentedFrames === qualityUnseen.presented ? qualityUnseen.samples + 1 : 0 };
|
|
973
1063
|
if (qualityUnseen.samples >= 4) current.unseen = true;
|
|
1064
|
+
sampleConnection(stats, telemetry, now);
|
|
974
1065
|
qualitySamples.push({ stats, telemetry });
|
|
975
1066
|
const windowMs = now - current.at;
|
|
976
1067
|
if (windowMs < qualityWindowMs) return;
|
|
@@ -1049,6 +1140,7 @@ var reportWebRtcStats = async (pc, stats) => {
|
|
|
1049
1140
|
};
|
|
1050
1141
|
if (counters.framesDecoded > (inbound?.framesDecoded ?? 0)) {
|
|
1051
1142
|
lastFrameAt = now;
|
|
1143
|
+
connectionTotals.firstFrameAt ??= now;
|
|
1052
1144
|
clearTimeout(webrtcDeadline);
|
|
1053
1145
|
webrtcDeadline = void 0;
|
|
1054
1146
|
if (fullChromaFailure) {
|