replicas-computer 0.3.14 → 0.3.16
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 +6 -6
- package/assets/desktop/start-services.sh +14 -0
- package/dist/index.js +317 -162
- package/dist/stream-viewer.js +214 -63
- package/native/internal/capture/native.go +40 -11
- package/native/main.go +69 -4
- package/native/options.go +1 -1
- package/package.json +1 -1
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.16";
|
|
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
|
|
@@ -3251,13 +3193,17 @@ import { createInterface } from "readline";
|
|
|
3251
3193
|
|
|
3252
3194
|
// src/rtp.ts
|
|
3253
3195
|
var RTP_HEADER_BYTES = 12;
|
|
3196
|
+
var NAL_SLICE = 1;
|
|
3254
3197
|
var NAL_IDR = 5;
|
|
3198
|
+
var NAL_SEI = 6;
|
|
3255
3199
|
var NAL_SPS = 7;
|
|
3200
|
+
var SEI_RECOVERY_POINT = 6;
|
|
3256
3201
|
var NAL_STAP_A = 24;
|
|
3257
3202
|
var NAL_FU_A = 28;
|
|
3258
3203
|
var nalType = (byte) => byte & 31;
|
|
3259
3204
|
var headerBytesOf = (packet) => RTP_HEADER_BYTES + (packet[0] & 15) * 4;
|
|
3260
3205
|
var joinable = (nal) => nal === NAL_IDR || nal === NAL_SPS;
|
|
3206
|
+
var refreshFrame = (nal) => nalType(nal[0]) === NAL_SLICE || nalType(nal[0]) === NAL_SEI && nal[1] === SEI_RECOVERY_POINT;
|
|
3261
3207
|
var isRtp = (data) => data.length >= RTP_HEADER_BYTES && data[0] >> 6 === 2 && (data[1] < 192 || data[1] > 223);
|
|
3262
3208
|
var rtpPayload = (packet) => packet.subarray(headerBytesOf(packet));
|
|
3263
3209
|
function aggregatedNals(payload) {
|
|
@@ -3286,7 +3232,10 @@ function isKeyframeStart(payload) {
|
|
|
3286
3232
|
const fragment = fragmentHeader(payload);
|
|
3287
3233
|
return fragment.start && joinable(fragment.type);
|
|
3288
3234
|
}
|
|
3289
|
-
if (type === NAL_STAP_A)
|
|
3235
|
+
if (type === NAL_STAP_A) {
|
|
3236
|
+
const { nals } = aggregatedNals(payload);
|
|
3237
|
+
return nals.some((nal) => joinable(nalType(nal[0]))) && !nals.some(refreshFrame);
|
|
3238
|
+
}
|
|
3290
3239
|
return joinable(type);
|
|
3291
3240
|
}
|
|
3292
3241
|
var RtpSplicer = class {
|
|
@@ -3363,6 +3312,8 @@ var RtpClock = class {
|
|
|
3363
3312
|
};
|
|
3364
3313
|
var startCode = new Uint8Array([0, 0, 0, 1]);
|
|
3365
3314
|
var H264RtpAssembler = class {
|
|
3315
|
+
/** Frames abandoned for a missing or out-of-order packet; every delta after one is undecodable until a keyframe. */
|
|
3316
|
+
discarded = 0;
|
|
3366
3317
|
clock = new RtpClock(9e4);
|
|
3367
3318
|
chunks = [];
|
|
3368
3319
|
key = false;
|
|
@@ -3380,6 +3331,7 @@ var H264RtpAssembler = class {
|
|
|
3380
3331
|
}
|
|
3381
3332
|
push(packet) {
|
|
3382
3333
|
if (this.timestamp !== void 0 && packet.timestamp !== this.timestamp) {
|
|
3334
|
+
if (this.chunks.length > 0 || this.damaged) this.discarded += 1;
|
|
3383
3335
|
this.clearFrame();
|
|
3384
3336
|
}
|
|
3385
3337
|
this.timestamp ??= packet.timestamp;
|
|
@@ -3412,6 +3364,7 @@ var H264RtpAssembler = class {
|
|
|
3412
3364
|
} else this.damaged = true;
|
|
3413
3365
|
if (!packet.marker) return null;
|
|
3414
3366
|
if (this.damaged || this.fragmentType !== void 0 || this.chunks.length === 0) {
|
|
3367
|
+
this.discarded += 1;
|
|
3415
3368
|
this.clearFrame();
|
|
3416
3369
|
return null;
|
|
3417
3370
|
}
|
|
@@ -3469,7 +3422,9 @@ function superviseEncoder(options) {
|
|
|
3469
3422
|
const warmupFrames = active ? options.warmupFrames ?? 0 : 0;
|
|
3470
3423
|
const command = options.command?.() ?? "ffmpeg";
|
|
3471
3424
|
const args = options.args(socket.address().port, warmupFrames);
|
|
3472
|
-
const child = spawn3(command, args, { env: options.env, stdio: ["ignore", options.onOutput ? "pipe" : "ignore", "pipe"] });
|
|
3425
|
+
const child = spawn3(command, args, { env: options.env, stdio: [options.controllable?.(command) ? "pipe" : "ignore", options.onOutput ? "pipe" : "ignore", "pipe"] });
|
|
3426
|
+
child.stdin?.on("error", () => {
|
|
3427
|
+
});
|
|
3473
3428
|
child.stderr?.on("data", (chunk) => process.stderr.write(`[desktop-stream] ${chunk}`));
|
|
3474
3429
|
const generation = { id: ++nextId, child, socket, live: true, startedAt: Date.now(), warmupFrames };
|
|
3475
3430
|
candidate = generation;
|
|
@@ -3539,10 +3494,18 @@ function superviseEncoder(options) {
|
|
|
3539
3494
|
candidate = void 0;
|
|
3540
3495
|
active = void 0;
|
|
3541
3496
|
});
|
|
3497
|
+
const control = (line) => {
|
|
3498
|
+
for (const generation of [candidate, active]) {
|
|
3499
|
+
if (generation?.live && generation.child.stdin?.writable) generation.child.stdin.write(`${line}
|
|
3500
|
+
`);
|
|
3501
|
+
}
|
|
3502
|
+
return Boolean(active?.live && active.child.stdin?.writable);
|
|
3503
|
+
};
|
|
3542
3504
|
return {
|
|
3543
3505
|
start,
|
|
3544
3506
|
replace,
|
|
3545
3507
|
stop,
|
|
3508
|
+
control,
|
|
3546
3509
|
running: () => Boolean(active && alive(active.child)),
|
|
3547
3510
|
replacing: () => Boolean(candidate),
|
|
3548
3511
|
generation: () => active?.id ?? 0,
|
|
@@ -3552,13 +3515,17 @@ function superviseEncoder(options) {
|
|
|
3552
3515
|
};
|
|
3553
3516
|
}
|
|
3554
3517
|
|
|
3555
|
-
// src/
|
|
3556
|
-
import {
|
|
3557
|
-
import { availableParallelism } from "os";
|
|
3558
|
-
import { dirname as dirname6, resolve as resolve2 } from "path";
|
|
3559
|
-
import { fileURLToPath } from "url";
|
|
3518
|
+
// src/ice-servers.ts
|
|
3519
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
3560
3520
|
|
|
3561
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
|
+
}
|
|
3562
3529
|
var DEFAULT_STREAM_REQUEST = {
|
|
3563
3530
|
transport: "webrtc",
|
|
3564
3531
|
profile: "balanced",
|
|
@@ -3615,6 +3582,144 @@ function isDesktopWebRtcFailure(value) {
|
|
|
3615
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]));
|
|
3616
3583
|
}
|
|
3617
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
|
+
|
|
3618
3723
|
// src/input-validation.ts
|
|
3619
3724
|
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
3620
3725
|
var optionalFinite = (value) => value === void 0 || finite(value);
|
|
@@ -3625,6 +3730,8 @@ function validInput(value) {
|
|
|
3625
3730
|
switch (value.type) {
|
|
3626
3731
|
case "release":
|
|
3627
3732
|
case "keyframe":
|
|
3733
|
+
case "retry-webrtc":
|
|
3734
|
+
case "webrtc-ready":
|
|
3628
3735
|
return true;
|
|
3629
3736
|
case "theme":
|
|
3630
3737
|
return value.theme === "light" || value.theme === "dark";
|
|
@@ -3928,22 +4035,6 @@ async function loadWebRtc() {
|
|
|
3928
4035
|
}
|
|
3929
4036
|
}
|
|
3930
4037
|
var cleanupWebRtc = () => ndc?.cleanup();
|
|
3931
|
-
function parseIceServers(spec) {
|
|
3932
|
-
const native = (spec ?? "stun:stun.l.google.com:19302").split(",").map((server) => server.trim()).filter(Boolean);
|
|
3933
|
-
const browser = native.map((url) => {
|
|
3934
|
-
const match = url.match(/^(turns?):([^:@]+):(.+)@([^@]+)$/);
|
|
3935
|
-
if (!match) return { urls: url };
|
|
3936
|
-
const [username, credential] = match.slice(2, 4).map((value) => {
|
|
3937
|
-
try {
|
|
3938
|
-
return decodeURIComponent(value);
|
|
3939
|
-
} catch {
|
|
3940
|
-
return value;
|
|
3941
|
-
}
|
|
3942
|
-
});
|
|
3943
|
-
return { urls: `${match[1]}:${match[4]}`, username, credential };
|
|
3944
|
-
});
|
|
3945
|
-
return { native, browser };
|
|
3946
|
-
}
|
|
3947
4038
|
var WebRtcViewer = class {
|
|
3948
4039
|
constructor(options, handlers) {
|
|
3949
4040
|
this.handlers = handlers;
|
|
@@ -4078,6 +4169,7 @@ function superviseVideoEncoder(options) {
|
|
|
4078
4169
|
const encoder = superviseEncoder({
|
|
4079
4170
|
label: "video",
|
|
4080
4171
|
command: () => nativeCapture ? captureBinary : "ffmpeg",
|
|
4172
|
+
controllable: (command) => command === captureBinary,
|
|
4081
4173
|
onFailure: (command) => {
|
|
4082
4174
|
if (command !== captureBinary) return;
|
|
4083
4175
|
nativeCapture = false;
|
|
@@ -4234,6 +4326,8 @@ function superviseVideoEncoder(options) {
|
|
|
4234
4326
|
});
|
|
4235
4327
|
return {
|
|
4236
4328
|
...encoder,
|
|
4329
|
+
/** Native capture takes keyframe, quality and frame-rate changes over stdin; ffmpeg needs a replacement for each. */
|
|
4330
|
+
controllable: () => nativeCapture,
|
|
4237
4331
|
capture: () => ({
|
|
4238
4332
|
captureBackend: (encoder.command() ?? (nativeCapture ? captureBinary : "ffmpeg")) === captureBinary ? "native" : "ffmpeg",
|
|
4239
4333
|
captureMetrics: captureMetrics?.generation === encoder.generation() && Date.now() - captureMetrics.at <= 2500 ? captureMetrics : void 0
|
|
@@ -4452,7 +4546,6 @@ var standbyRung = 5;
|
|
|
4452
4546
|
var port = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_PORT || String(DESKTOP_STREAM_PORT), 10);
|
|
4453
4547
|
var display = process.env.REPLICAS_DESKTOP_DISPLAY?.replace(/^([^:])/, ":$1") || ":99";
|
|
4454
4548
|
var webrtcEnabled = process.env.REPLICAS_DESKTOP_STREAM_WEBRTC !== "0";
|
|
4455
|
-
var runtimeEnvFile = process.env.REPLICAS_RUNTIME_ENV_FILE || "/home/user/.replicas/runtime-env.sh";
|
|
4456
4549
|
var colorMode = process.env.REPLICAS_DESKTOP_STREAM_COLOR === "legacy" ? "legacy" : "bt709";
|
|
4457
4550
|
var defaultRequest = parseStreamRequest(new URLSearchParams({
|
|
4458
4551
|
transport: process.env.REPLICAS_DESKTOP_STREAM_TRANSPORT ?? "",
|
|
@@ -4479,6 +4572,7 @@ var qualityLadder = (profile) => rungCaps.map((kbps, index) => ({ kbps, crf: pro
|
|
|
4479
4572
|
var fullChromaMinKbps = 8e3;
|
|
4480
4573
|
var fallbackKeyframeMs = 250;
|
|
4481
4574
|
var webrtcStartupRung = 2;
|
|
4575
|
+
var descentMemoryMs = 18e4;
|
|
4482
4576
|
var targetBacklogMs = 120;
|
|
4483
4577
|
var congestedBacklogMs = 250;
|
|
4484
4578
|
var congestedDecodeQueue = 3;
|
|
@@ -4507,14 +4601,6 @@ function mediaPacket(kind, timestamp, payload) {
|
|
|
4507
4601
|
header.writeBigUInt64BE(BigInt(Math.max(0, timestamp)), 4);
|
|
4508
4602
|
return Buffer.concat([header, payload]);
|
|
4509
4603
|
}
|
|
4510
|
-
function currentIceServers() {
|
|
4511
|
-
let spec = process.env.REPLICAS_DESKTOP_STREAM_ICE_SERVERS;
|
|
4512
|
-
try {
|
|
4513
|
-
spec = parsePosixEnvFile(readFileSync6(runtimeEnvFile, "utf8")).REPLICAS_DESKTOP_STREAM_ICE_SERVERS ?? spec;
|
|
4514
|
-
} catch {
|
|
4515
|
-
}
|
|
4516
|
-
return parseIceServers(spec);
|
|
4517
|
-
}
|
|
4518
4604
|
var appliedTheme;
|
|
4519
4605
|
var pendingTheme;
|
|
4520
4606
|
var applyingTheme = false;
|
|
@@ -4540,7 +4626,7 @@ function drainDesktopTheme() {
|
|
|
4540
4626
|
function processCpuMs(pid) {
|
|
4541
4627
|
if (!pid) return 0;
|
|
4542
4628
|
try {
|
|
4543
|
-
const stat =
|
|
4629
|
+
const stat = readFileSync7(`/proc/${pid}/stat`, "utf8");
|
|
4544
4630
|
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
4545
4631
|
return (Number(fields[11]) + Number(fields[12])) * 10;
|
|
4546
4632
|
} catch {
|
|
@@ -4558,7 +4644,8 @@ async function runMediaServer() {
|
|
|
4558
4644
|
let activeProfile = "balanced";
|
|
4559
4645
|
let adaptive = true;
|
|
4560
4646
|
let rung = 0;
|
|
4561
|
-
let descents =
|
|
4647
|
+
let descents = [];
|
|
4648
|
+
let failedRung = -1;
|
|
4562
4649
|
let congestedSeconds = 0;
|
|
4563
4650
|
let comfortableSeconds = 0;
|
|
4564
4651
|
let cleanTransportSeconds = 0;
|
|
@@ -4577,6 +4664,8 @@ async function runMediaServer() {
|
|
|
4577
4664
|
let encoderFps = 0;
|
|
4578
4665
|
let videoWanted = false;
|
|
4579
4666
|
let encoderSignature = "";
|
|
4667
|
+
let encoderDescription = "";
|
|
4668
|
+
let encoderLive = { crf: 0, kbps: 0, fps: 0 };
|
|
4580
4669
|
let encoderStop;
|
|
4581
4670
|
let joinKeyframe;
|
|
4582
4671
|
let lastVideoAt = 0;
|
|
@@ -4611,10 +4700,13 @@ async function runMediaServer() {
|
|
|
4611
4700
|
const quality = ladder()[allStandby() ? Math.max(rung, standbyRung) : rung];
|
|
4612
4701
|
const encodeFps = streamFps();
|
|
4613
4702
|
const sockets = socketViewers().length > 0;
|
|
4614
|
-
const
|
|
4703
|
+
const onDemand = videoEncoder.controllable();
|
|
4704
|
+
const keyframeMs = onDemand ? 0 : sockets ? Math.min(quality.keyframeMs, fallbackKeyframeMs) : quality.keyframeMs;
|
|
4615
4705
|
const videoProfile = profile();
|
|
4616
|
-
const intraRefresh = !sockets;
|
|
4706
|
+
const intraRefresh = !onDemand && !sockets;
|
|
4617
4707
|
const pixelFormat = videoProfile === "high444" ? "yuv444p" : "yuv420p";
|
|
4708
|
+
const live = `rung ${rung} \xB7 crf ${quality.crf} \xB7 ${encodeFps} fps`;
|
|
4709
|
+
const restart = `${activeProfile} \xB7 ${preset} \xB7 ${videoProfile} \xB7 keyframe ${keyframeMs ? `${keyframeMs}ms${intraRefresh ? " refresh" : ""}` : "on demand"}`;
|
|
4618
4710
|
return {
|
|
4619
4711
|
quality,
|
|
4620
4712
|
preset,
|
|
@@ -4624,7 +4716,8 @@ async function runMediaServer() {
|
|
|
4624
4716
|
pixelFormat,
|
|
4625
4717
|
fps: encodeFps,
|
|
4626
4718
|
chromaQpOffset: profiles[activeProfile].chromaQpOffset,
|
|
4627
|
-
signature:
|
|
4719
|
+
signature: onDemand ? restart : `${live} \xB7 ${restart}`,
|
|
4720
|
+
description: `${live} \xB7 ${restart}`
|
|
4628
4721
|
};
|
|
4629
4722
|
};
|
|
4630
4723
|
const budgetBps = () => ladder()[rung].kbps * 1e3 * budgetFactor;
|
|
@@ -4645,6 +4738,7 @@ async function runMediaServer() {
|
|
|
4645
4738
|
if (!keyframe2 && (viewer.waitingForKeyframe || queueMs(viewer) > targetBacklogMs || socket.bufferedAmount > fallbackBufferedBytes)) {
|
|
4646
4739
|
viewer.waitingForKeyframe = true;
|
|
4647
4740
|
viewer.withheldFrames += 1;
|
|
4741
|
+
if (videoEncoder.controllable() && queueMs(viewer) <= targetBacklogMs && socket.bufferedAmount <= fallbackBufferedBytes) requestKeyframe("socket viewer caught up", void 0, viewer);
|
|
4648
4742
|
continue;
|
|
4649
4743
|
}
|
|
4650
4744
|
if (keyframe2) viewer.waitingForKeyframe = false;
|
|
@@ -4676,7 +4770,7 @@ async function runMediaServer() {
|
|
|
4676
4770
|
let sockets = false;
|
|
4677
4771
|
for (const viewer of viewers.values()) {
|
|
4678
4772
|
if (viewer.fallback) sockets = true;
|
|
4679
|
-
|
|
4773
|
+
viewer.rtc?.enqueueVideo(rtp ??= withPlayoutDelay(packet, PLAYOUT_DELAY_ID), frameId, keyframe, last);
|
|
4680
4774
|
}
|
|
4681
4775
|
frameStart = last;
|
|
4682
4776
|
if (last) {
|
|
@@ -4685,7 +4779,12 @@ async function runMediaServer() {
|
|
|
4685
4779
|
}
|
|
4686
4780
|
if (!sockets) return;
|
|
4687
4781
|
const parsed = parseRtpPacket(packet);
|
|
4782
|
+
const discarded = videoAssembler.discarded;
|
|
4688
4783
|
const frame = parsed && videoAssembler.push(parsed);
|
|
4784
|
+
if (videoAssembler.discarded !== discarded) {
|
|
4785
|
+
for (const viewer of socketViewers()) viewer.waitingForKeyframe = true;
|
|
4786
|
+
if (videoEncoder.controllable()) requestKeyframe("socket frame lost");
|
|
4787
|
+
}
|
|
4689
4788
|
if (frame) broadcast(mediaPacket(frame.key ? 1 /* VideoKey */ : 2 /* VideoDelta */, frame.timestamp, frame.data), frame.key);
|
|
4690
4789
|
};
|
|
4691
4790
|
const onAudioPacket = (packet, generation) => {
|
|
@@ -4696,7 +4795,7 @@ async function runMediaServer() {
|
|
|
4696
4795
|
let sockets = false;
|
|
4697
4796
|
for (const viewer of viewers.values()) {
|
|
4698
4797
|
if (viewer.fallback) sockets = true;
|
|
4699
|
-
|
|
4798
|
+
viewer.rtc?.sendAudio(packet);
|
|
4700
4799
|
}
|
|
4701
4800
|
if (!sockets) return;
|
|
4702
4801
|
const parsed = parseRtpPacket(packet);
|
|
@@ -4770,20 +4869,30 @@ async function runMediaServer() {
|
|
|
4770
4869
|
const ensureEncoders = () => {
|
|
4771
4870
|
clearTimeout(encoderStop);
|
|
4772
4871
|
void audioEncoder.start();
|
|
4773
|
-
const { signature } = encoderParams();
|
|
4872
|
+
const { signature, description, quality, fps: encodeFps } = encoderParams();
|
|
4873
|
+
const live = { crf: quality.crf, kbps: quality.kbps, fps: encodeFps };
|
|
4774
4874
|
if (!videoWanted) {
|
|
4775
4875
|
videoWanted = true;
|
|
4776
4876
|
encoderSignature = signature;
|
|
4777
|
-
|
|
4877
|
+
encoderDescription = description;
|
|
4878
|
+
encoderLive = live;
|
|
4879
|
+
record("encoder", description);
|
|
4778
4880
|
void videoEncoder.start();
|
|
4779
4881
|
return false;
|
|
4780
4882
|
}
|
|
4781
|
-
if (
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4883
|
+
if (description !== encoderDescription) record("encoder", description);
|
|
4884
|
+
encoderDescription = description;
|
|
4885
|
+
if (signature !== encoderSignature) {
|
|
4886
|
+
encoderSignature = signature;
|
|
4887
|
+
encoderLive = live;
|
|
4888
|
+
lastSwapAt = Date.now();
|
|
4889
|
+
void videoEncoder.replace();
|
|
4890
|
+
return true;
|
|
4891
|
+
}
|
|
4892
|
+
if (live.crf !== encoderLive.crf || live.kbps !== encoderLive.kbps) videoEncoder.control(`quality ${live.crf} ${live.kbps}`);
|
|
4893
|
+
if (live.fps !== encoderLive.fps) videoEncoder.control(`fps ${live.fps}`);
|
|
4894
|
+
encoderLive = live;
|
|
4895
|
+
return false;
|
|
4787
4896
|
};
|
|
4788
4897
|
const syncEncoder = () => videoWanted && viewers.size > 0 && ensureEncoders();
|
|
4789
4898
|
const stopEncodersSoon = () => {
|
|
@@ -4801,8 +4910,9 @@ async function runMediaServer() {
|
|
|
4801
4910
|
const quality = ladder()[next];
|
|
4802
4911
|
record(next > rung ? "quality down" : "quality up", `rung ${rung} \u2192 ${next} (${quality.kbps} kbps cap, crf ${quality.crf}): ${reason}`);
|
|
4803
4912
|
if (next > rung) {
|
|
4804
|
-
if (now - lastDescentAt > 1e4) descents
|
|
4913
|
+
if (now - lastDescentAt > 1e4) descents.push(now);
|
|
4805
4914
|
lastDescentAt = now;
|
|
4915
|
+
failedRung = rung;
|
|
4806
4916
|
if (now - lastAscentAt < 15e3) {
|
|
4807
4917
|
blockedRung = rung;
|
|
4808
4918
|
blockedUntil = now + 12e4;
|
|
@@ -4818,7 +4928,8 @@ async function runMediaServer() {
|
|
|
4818
4928
|
const resetLadder = (request) => {
|
|
4819
4929
|
rung = request.transport === "webrtc" && request.adaptive ? webrtcStartupRung : 0;
|
|
4820
4930
|
cleanTransportSeconds = 0;
|
|
4821
|
-
descents =
|
|
4931
|
+
descents = [];
|
|
4932
|
+
failedRung = -1;
|
|
4822
4933
|
lastDescentAt = 0;
|
|
4823
4934
|
lastAscentAt = 0;
|
|
4824
4935
|
congestedSeconds = 0;
|
|
@@ -4838,6 +4949,7 @@ async function runMediaServer() {
|
|
|
4838
4949
|
const sent = Math.min(...recentVideoKbps);
|
|
4839
4950
|
return remb !== void 0 && earlier !== void 0 && remb <= earlier.kbps && sent > 500 && remb < sent * 0.9;
|
|
4840
4951
|
};
|
|
4952
|
+
const loaded = () => Math.min(...recentVideoKbps) > 500;
|
|
4841
4953
|
const signals = (viewer, stats) => {
|
|
4842
4954
|
const now = Date.now();
|
|
4843
4955
|
if (viewer.fallback) {
|
|
@@ -4848,22 +4960,24 @@ async function runMediaServer() {
|
|
|
4848
4960
|
}
|
|
4849
4961
|
const stableSince = stableRttWindows.get(viewer)?.since;
|
|
4850
4962
|
return [
|
|
4851
|
-
...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`] : [],
|
|
4963
|
+
...viewer.rttFloorMs > viewer.rtcBaselineMs + 40 && (stableSince === void 0 || now - stableSince < 2e3) && loaded() ? [`rtt floor ${Math.round(viewer.rttFloorMs)} ms over ${Math.round(viewer.rtcBaselineMs)} ms baseline`] : [],
|
|
4852
4964
|
...stats.videoFps < expectedFps() * 0.6 && viewer.lowFpsReports >= 2 ? [`decoded ${stats.videoFps.toFixed(0)} fps`] : [],
|
|
4853
|
-
...(stats.lossPercent ?? 0) > 3 ? [`loss ${(stats.lossPercent ?? 0).toFixed(1)}%`] : [],
|
|
4854
|
-
...(stats.jitterBufferMs ?? 0) > 80 ? [`jitter buffer ${(stats.jitterBufferMs ?? 0).toFixed(0)} ms`] : []
|
|
4965
|
+
...(stats.lossPercent ?? 0) > 3 && loaded() ? [`loss ${(stats.lossPercent ?? 0).toFixed(1)}%`] : [],
|
|
4966
|
+
...(stats.jitterBufferMs ?? 0) > 80 && loaded() ? [`jitter buffer ${(stats.jitterBufferMs ?? 0).toFixed(0)} ms`] : []
|
|
4855
4967
|
];
|
|
4856
4968
|
};
|
|
4857
4969
|
const lowFpsObservationAt = /* @__PURE__ */ new WeakMap();
|
|
4858
|
-
const severe = (viewer, stats) => !viewer.fallback && ((stats.lossPercent ?? 0) > 10 || stats.videoFps < expectedFps() * 0.25 && viewer.lowFpsReports >= 2);
|
|
4970
|
+
const severe = (viewer, stats) => !viewer.fallback && ((stats.lossPercent ?? 0) > 10 && loaded() || stats.videoFps < expectedFps() * 0.25 && viewer.lowFpsReports >= 2);
|
|
4859
4971
|
let lastSevereDescentAt = 0;
|
|
4860
4972
|
const descendSevere = (now, trigger) => {
|
|
4861
4973
|
comfortableSeconds = 0;
|
|
4862
4974
|
congestedSeconds = 0;
|
|
4863
4975
|
if (!adaptive || now - lastSevereDescentAt < 1e3) return;
|
|
4864
4976
|
lastSevereDescentAt = now;
|
|
4977
|
+
const sent = Math.min(...recentVideoKbps);
|
|
4865
4978
|
const estimates = [...viewers.values()].flatMap((viewer) => {
|
|
4866
|
-
const
|
|
4979
|
+
const available = viewer.stats && now - viewer.stats.at < 750 ? viewer.stats.availableKbps ?? 0 : 0;
|
|
4980
|
+
const reported = sent > 500 && available < sent * 0.9 ? available : 0;
|
|
4867
4981
|
const remb = rembBelowSent(viewer, now) ? freshRemb(viewer, now) ?? 0 : 0;
|
|
4868
4982
|
return [...reported > 0 ? [reported] : [], ...remb > 0 ? [remb] : []];
|
|
4869
4983
|
});
|
|
@@ -4880,9 +4994,12 @@ async function runMediaServer() {
|
|
|
4880
4994
|
videoKbps = Math.round((videoBytes - bytesAtTick) * 8 / 1e3);
|
|
4881
4995
|
bytesAtTick = videoBytes;
|
|
4882
4996
|
if (recentVideoKbps.push(videoKbps) > 3) recentVideoKbps.shift();
|
|
4883
|
-
if (viewers.size === 0)
|
|
4997
|
+
if (viewers.size === 0) {
|
|
4998
|
+
starvedSeconds = 0;
|
|
4999
|
+
return;
|
|
5000
|
+
}
|
|
4884
5001
|
const { captureBackend, captureMetrics } = videoEncoder.capture();
|
|
4885
|
-
const captureBusy = captureBackend !== "native" ||
|
|
5002
|
+
const captureBusy = captureBackend !== "native" || (captureMetrics ? captureMetrics.busyMs / captureMetrics.elapsedMs > 0.8 : videoEncoder.activeForMs() > 2500);
|
|
4886
5003
|
if (videoEncoder.running() && encoderFps < streamFps() * 0.9 && captureBusy) {
|
|
4887
5004
|
cleanTransportSeconds = 0;
|
|
4888
5005
|
congestedSeconds = 0;
|
|
@@ -4931,15 +5048,16 @@ async function runMediaServer() {
|
|
|
4931
5048
|
return;
|
|
4932
5049
|
}
|
|
4933
5050
|
congestedSeconds = Math.max(0, congestedSeconds - 0.5);
|
|
4934
|
-
|
|
5051
|
+
descents = descents.filter((at) => now - at < descentMemoryMs);
|
|
5052
|
+
if (descents.length === 0 && reporting.some((viewer) => !viewer.fallback && (viewer.rttFloorMs > viewer.rtcBaselineMs + 20 && loaded() || (viewer.stats.playbackDroppedPercent ?? 0) > 5 && cleanTransportSeconds < 15))) {
|
|
4935
5053
|
comfortableSeconds = 0;
|
|
4936
5054
|
return;
|
|
4937
5055
|
}
|
|
4938
5056
|
comfortableSeconds += 1;
|
|
4939
|
-
const startupSeconds = reporting.every((viewer) => !viewer.fallback) ? 2 : 3;
|
|
4940
|
-
const recoverySeconds = lastAscentAt > lastDescentAt && cleanTransportSeconds >= 15 ? 15 : Math.min(60, 15 * 2 ** Math.min(descents, 2));
|
|
4941
|
-
if (comfortableSeconds < (descents === 0 ? startupSeconds : recoverySeconds)) return;
|
|
4942
5057
|
const target = Math.max(0, rung - 1);
|
|
5058
|
+
const startupSeconds = reporting.every((viewer) => !viewer.fallback) ? 2 : 3;
|
|
5059
|
+
const retrySeconds = lastAscentAt > lastDescentAt && cleanTransportSeconds >= 15 ? 15 : Math.min(60, 15 * 2 ** Math.min(descents.length - 1, 2));
|
|
5060
|
+
if (comfortableSeconds < (descents.length > 0 && target <= failedRung ? retrySeconds : startupSeconds)) return;
|
|
4943
5061
|
if (target === blockedRung && now < blockedUntil) return;
|
|
4944
5062
|
setRung(target, `${comfortableSeconds}s comfortable, ${cleanTransportSeconds}s clean transport`);
|
|
4945
5063
|
}, 1e3);
|
|
@@ -5019,9 +5137,18 @@ async function runMediaServer() {
|
|
|
5019
5137
|
`);
|
|
5020
5138
|
}, telemetryIntervalMs);
|
|
5021
5139
|
telemetry.unref();
|
|
5140
|
+
let lastKeyframeRequestAt = 0;
|
|
5022
5141
|
const requestKeyframe = (reason, joinedAt, requestingViewer) => {
|
|
5023
5142
|
const now = Date.now();
|
|
5024
5143
|
if (!videoWanted || viewers.size === 0 || videoEncoder.replacing() || lastKeyframeAt < now - videoEncoder.activeForMs() || joinedAt !== void 0 && lastKeyframeAt > joinedAt) return;
|
|
5144
|
+
if (videoEncoder.controllable()) {
|
|
5145
|
+
const rttMs = requestingViewer?.rtc?.rttMs ?? requestingViewer?.stats?.rttMs ?? 0;
|
|
5146
|
+
if (joinedAt === void 0 && now - Math.max(lastKeyframeAt, lastKeyframeRequestAt) < Math.max(300, rttMs + 150)) return;
|
|
5147
|
+
if (!videoEncoder.control("keyframe")) return;
|
|
5148
|
+
lastKeyframeRequestAt = now;
|
|
5149
|
+
record("keyframe", reason);
|
|
5150
|
+
return;
|
|
5151
|
+
}
|
|
5025
5152
|
const startup = requestingViewer && !requestingViewer.hasDecodedFrame && now - requestingViewer.connectedAt < 1e4;
|
|
5026
5153
|
const wait = (joinedAt === void 0 ? startup ? 500 : 2e3 : 350) - (now - lastSwapAt);
|
|
5027
5154
|
if (wait > 0) {
|
|
@@ -5031,7 +5158,9 @@ async function runMediaServer() {
|
|
|
5031
5158
|
}
|
|
5032
5159
|
return;
|
|
5033
5160
|
}
|
|
5034
|
-
|
|
5161
|
+
const { keyframeMs, intraRefresh } = encoderParams();
|
|
5162
|
+
const untilPeriodic = keyframeMs - (now - lastKeyframeAt);
|
|
5163
|
+
if (!startup && !intraRefresh && untilPeriodic >= 0 && untilPeriodic <= 350) return;
|
|
5035
5164
|
lastSwapAt = now;
|
|
5036
5165
|
record("keyframe", reason);
|
|
5037
5166
|
void videoEncoder.replace();
|
|
@@ -5044,16 +5173,51 @@ async function runMediaServer() {
|
|
|
5044
5173
|
process.stderr.write(`[desktop-stream] webrtc fallback diagnostics: ${JSON.stringify(entry)}
|
|
5045
5174
|
`);
|
|
5046
5175
|
}
|
|
5047
|
-
|
|
5176
|
+
const retried = viewer.rtc;
|
|
5177
|
+
viewer.rtc?.close();
|
|
5178
|
+
viewer.rtc = void 0;
|
|
5179
|
+
if (viewer.fallback) {
|
|
5180
|
+
if (retried) record("webrtc retry failed", reason);
|
|
5181
|
+
return;
|
|
5182
|
+
}
|
|
5048
5183
|
viewer.fallback = true;
|
|
5049
5184
|
viewer.waitingForKeyframe = true;
|
|
5050
5185
|
viewer.input.release();
|
|
5051
|
-
viewer.rtc?.close();
|
|
5052
|
-
viewer.rtc = void 0;
|
|
5053
5186
|
record("fallback to socket", reason);
|
|
5054
5187
|
if (!syncEncoder()) requestKeyframe("socket fallback");
|
|
5055
5188
|
};
|
|
5056
5189
|
const optional = (value, max) => value === void 0 ? void 0 : clamp(value, 0, max);
|
|
5190
|
+
const openWebRtc = (viewer, reply, iceServers) => {
|
|
5191
|
+
try {
|
|
5192
|
+
viewer.rtc = new WebRtcViewer({ iceServers, iceTransportPolicy: "all", portRange: icePorts, budgetBps: budgetBps() }, {
|
|
5193
|
+
offer: (sdp) => reply({ type: "offer", sdp }),
|
|
5194
|
+
candidate: (candidate, mid) => reply({ type: "candidate", candidate, mid }),
|
|
5195
|
+
message: (message, answer) => handleMessage(viewer, message, answer),
|
|
5196
|
+
keyframe: (reason) => {
|
|
5197
|
+
if (reason === "stale" && Date.now() - lastDescentAt > 1e3) descendSevere(Date.now(), "send queue went stale and was cut back to a keyframe");
|
|
5198
|
+
requestKeyframe(reason === "pli" ? "receiver pli" : "stale send queue", void 0, viewer);
|
|
5199
|
+
},
|
|
5200
|
+
remb: (bitsPerSecond) => {
|
|
5201
|
+
if (viewer.rembs.push({ kbps: bitsPerSecond / 1e3, at: Date.now() }) > 16) viewer.rembs.shift();
|
|
5202
|
+
},
|
|
5203
|
+
connected: () => {
|
|
5204
|
+
record("webrtc connected", viewer.rtc?.path ?? void 0);
|
|
5205
|
+
requestKeyframe("viewer joined", Date.now());
|
|
5206
|
+
},
|
|
5207
|
+
// A viewer that loses its peer cannot send the key-ups for whatever it
|
|
5208
|
+
// was holding, and its socket may stay open for a long time yet. A socket
|
|
5209
|
+
// viewer trying WebRTC again still sends its input over the socket.
|
|
5210
|
+
disconnected: (state) => {
|
|
5211
|
+
record(`webrtc ${state}`);
|
|
5212
|
+
if (!viewer.fallback) viewer.input.release();
|
|
5213
|
+
if (state === "failed") reply({ type: "webrtc-failed" });
|
|
5214
|
+
}
|
|
5215
|
+
});
|
|
5216
|
+
} catch (error) {
|
|
5217
|
+
record("webrtc error", `could not create a peer connection: ${error instanceof Error ? error.message : String(error)}`);
|
|
5218
|
+
reply({ type: "webrtc-failed" });
|
|
5219
|
+
}
|
|
5220
|
+
};
|
|
5057
5221
|
const handleMessage = (viewer, message, reply) => {
|
|
5058
5222
|
switch (message.type) {
|
|
5059
5223
|
case "ping":
|
|
@@ -5131,7 +5295,7 @@ async function runMediaServer() {
|
|
|
5131
5295
|
}
|
|
5132
5296
|
if (viewer.stats.videoFps > 0) viewer.hasDecodedFrame = true;
|
|
5133
5297
|
if (viewer.stats.videoFps >= expectedFps() * 0.8) viewer.everHealthy = true;
|
|
5134
|
-
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");
|
|
5298
|
+
if (encoderHealthyTicks >= 3 && videoEncoder.running() && loaded() && [...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");
|
|
5135
5299
|
return;
|
|
5136
5300
|
}
|
|
5137
5301
|
case "answer":
|
|
@@ -5144,8 +5308,28 @@ async function runMediaServer() {
|
|
|
5144
5308
|
case "fallback":
|
|
5145
5309
|
enterFallback(viewer, message.reason, message.failure);
|
|
5146
5310
|
return;
|
|
5311
|
+
// A socket viewer whose WebRTC attempt failed tries again in the background;
|
|
5312
|
+
// the socket keeps carrying its media until the new peer shows video.
|
|
5313
|
+
case "retry-webrtc": {
|
|
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
|
+
});
|
|
5321
|
+
return;
|
|
5322
|
+
}
|
|
5323
|
+
case "webrtc-ready":
|
|
5324
|
+
if (!viewer.fallback || !viewer.rtc?.connected) return;
|
|
5325
|
+
viewer.fallback = false;
|
|
5326
|
+
viewer.withheldFrames = 0;
|
|
5327
|
+
record("back on webrtc", viewer.rtc.path ?? void 0);
|
|
5328
|
+
syncEncoder();
|
|
5329
|
+
return;
|
|
5147
5330
|
case "keyframe":
|
|
5148
|
-
if (
|
|
5331
|
+
if (viewer.fallback) viewer.waitingForKeyframe = true;
|
|
5332
|
+
if (!viewer.fallback || videoEncoder.controllable()) requestKeyframe(viewer.fallback ? "socket viewer decoder reset" : "viewer watchdog", void 0, viewer);
|
|
5149
5333
|
return;
|
|
5150
5334
|
case "theme":
|
|
5151
5335
|
applyDesktopTheme(message.theme);
|
|
@@ -5237,10 +5421,11 @@ async function runMediaServer() {
|
|
|
5237
5421
|
if (extname(path) === ".html") {
|
|
5238
5422
|
ensureEncoders();
|
|
5239
5423
|
if (viewers.size === 0) stopEncodersSoon();
|
|
5424
|
+
if (webrtc) void currentIceServers();
|
|
5240
5425
|
response.setHeader("cache-control", "no-store");
|
|
5241
5426
|
const digest = createHash3("sha1");
|
|
5242
|
-
for (const name of readdirSync(webroot).filter((file) => /\.(js|css)$/.test(file)).sort()) digest.update(
|
|
5243
|
-
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)}`));
|
|
5244
5429
|
return;
|
|
5245
5430
|
}
|
|
5246
5431
|
response.setHeader("cache-control", "private, max-age=31536000, immutable");
|
|
@@ -5253,9 +5438,10 @@ async function runMediaServer() {
|
|
|
5253
5438
|
socket.destroy();
|
|
5254
5439
|
return;
|
|
5255
5440
|
}
|
|
5256
|
-
|
|
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)));
|
|
5257
5443
|
});
|
|
5258
|
-
webSocketServer.on("connection", (socket, request, standby) => {
|
|
5444
|
+
webSocketServer.on("connection", (socket, request, standby, ice) => {
|
|
5259
5445
|
const viewer = {
|
|
5260
5446
|
socket,
|
|
5261
5447
|
input: new InputBridge(display, width, height),
|
|
@@ -5289,41 +5475,10 @@ async function runMediaServer() {
|
|
|
5289
5475
|
const reply = (message) => {
|
|
5290
5476
|
if (socket.readyState === WebSocket2.OPEN) socket.send(JSON.stringify(message));
|
|
5291
5477
|
};
|
|
5292
|
-
const offerWebRtc = webrtc && request.transport === "webrtc";
|
|
5293
|
-
const ice = offerWebRtc ? currentIceServers() : void 0;
|
|
5294
5478
|
const fallbackProfile = profile() === "high444" ? "main" : profile();
|
|
5295
5479
|
const configMessage = { ...config, videoCodec: H264_PROFILES[fallbackProfile].codec, iceServers: ice?.browser, request };
|
|
5296
5480
|
reply(configMessage);
|
|
5297
|
-
if (ice)
|
|
5298
|
-
try {
|
|
5299
|
-
viewer.rtc = new WebRtcViewer({ iceServers: ice.native, iceTransportPolicy: "all", portRange: icePorts, budgetBps: budgetBps() }, {
|
|
5300
|
-
offer: (sdp) => reply({ type: "offer", sdp }),
|
|
5301
|
-
candidate: (candidate, mid) => reply({ type: "candidate", candidate, mid }),
|
|
5302
|
-
message: (message, answer) => handleMessage(viewer, message, answer),
|
|
5303
|
-
keyframe: (reason) => {
|
|
5304
|
-
if (reason === "stale") descendSevere(Date.now(), "send queue went stale and was cut back to a keyframe");
|
|
5305
|
-
requestKeyframe(reason === "pli" ? "receiver pli" : "stale send queue", void 0, viewer);
|
|
5306
|
-
},
|
|
5307
|
-
remb: (bitsPerSecond) => {
|
|
5308
|
-
if (viewer.rembs.push({ kbps: bitsPerSecond / 1e3, at: Date.now() }) > 16) viewer.rembs.shift();
|
|
5309
|
-
},
|
|
5310
|
-
connected: () => {
|
|
5311
|
-
record("webrtc connected", viewer.rtc?.path ?? void 0);
|
|
5312
|
-
requestKeyframe("viewer joined", Date.now());
|
|
5313
|
-
},
|
|
5314
|
-
// A viewer that loses its peer cannot send the key-ups for whatever it
|
|
5315
|
-
// was holding, and its socket may stay open for a long time yet.
|
|
5316
|
-
disconnected: (state) => {
|
|
5317
|
-
record(`webrtc ${state}`);
|
|
5318
|
-
viewer.input.release();
|
|
5319
|
-
if (state === "failed") reply({ type: "webrtc-failed" });
|
|
5320
|
-
}
|
|
5321
|
-
});
|
|
5322
|
-
} catch (error) {
|
|
5323
|
-
record("webrtc error", `could not create a peer connection: ${error instanceof Error ? error.message : String(error)}`);
|
|
5324
|
-
reply({ type: "webrtc-failed" });
|
|
5325
|
-
}
|
|
5326
|
-
}
|
|
5481
|
+
if (ice) openWebRtc(viewer, reply, ice.native);
|
|
5327
5482
|
if (!viewer.rtc) enterFallback(viewer, request.transport === "webrtc" ? "webrtc unavailable" : "websocket requested");
|
|
5328
5483
|
ensureEncoders();
|
|
5329
5484
|
socket.on("message", (raw, binary) => {
|