replicas-computer 0.2.3 → 0.2.5
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/assets/desktop/replicas-chrome +6 -0
- package/assets/desktop/start-services.sh +10 -1
- package/dist/index.js +269 -88
- package/dist/stream-viewer.js +5 -1
- package/package.json +1 -1
|
@@ -39,6 +39,8 @@ JSON
|
|
|
39
39
|
touch "$MARKER"
|
|
40
40
|
fi
|
|
41
41
|
|
|
42
|
+
# password-store skips a keyring D-Bus probe that stalls first launch for seconds
|
|
43
|
+
# on a headless image; the network flags keep first-run downloads off the critical path.
|
|
42
44
|
exec /usr/bin/google-chrome-stable \
|
|
43
45
|
--user-data-dir="$PROFILE_DIR" \
|
|
44
46
|
--profile-directory=Default \
|
|
@@ -46,6 +48,10 @@ exec /usr/bin/google-chrome-stable \
|
|
|
46
48
|
--no-default-browser-check \
|
|
47
49
|
--disable-search-engine-choice-screen \
|
|
48
50
|
--disable-sync \
|
|
51
|
+
--password-store=basic \
|
|
52
|
+
--disable-component-update \
|
|
53
|
+
--disable-background-networking \
|
|
54
|
+
--no-pings \
|
|
49
55
|
--remote-debugging-address=127.0.0.1 \
|
|
50
56
|
--remote-debugging-port="$DEBUG_PORT" \
|
|
51
57
|
--start-maximized \
|
|
@@ -384,8 +384,17 @@ fi
|
|
|
384
384
|
|
|
385
385
|
kill_pids "$(legacy_stream_websockify_pids)"
|
|
386
386
|
clear_bridge_pid_if_unhealthy stream has_stream stream_listener_pids stream_pids
|
|
387
|
+
# Forwarded only when set, so the stream server keeps its own default.
|
|
387
388
|
start_bg stream \
|
|
388
389
|
"has_stream" \
|
|
389
|
-
"exec env REPLICAS_DESKTOP_DISPLAY=:$DISPLAY_NUM REPLICAS_DESKTOP_WIDTH=$WIDTH REPLICAS_DESKTOP_HEIGHT=$HEIGHT REPLICAS_DESKTOP_STREAM_FPS=$
|
|
390
|
+
"exec env REPLICAS_DESKTOP_DISPLAY=:$DISPLAY_NUM REPLICAS_DESKTOP_WIDTH=$WIDTH REPLICAS_DESKTOP_HEIGHT=$HEIGHT ${REPLICAS_DESKTOP_STREAM_FPS:+REPLICAS_DESKTOP_STREAM_FPS=$REPLICAS_DESKTOP_STREAM_FPS }REPLICAS_DESKTOP_STREAM_PORT=$STREAM_PORT REPLICAS_DESKTOP_STREAM_WEBROOT=${REPLICAS_DESKTOP_STREAM_WEBROOT:-$REPLICAS_DESKTOP_TEMPLATES/stream} replicas-computer stream"
|
|
391
|
+
|
|
392
|
+
# Takes Chrome's cold-start cost at boot instead of on the user's first click;
|
|
393
|
+
# later launches join this instance through the profile singleton.
|
|
394
|
+
if [ "${REPLICAS_DESKTOP_CHROME_PREWARM:-1}" = "1" ] && command -v replicas-chrome >/dev/null 2>&1; then
|
|
395
|
+
start_bg chrome-prewarm \
|
|
396
|
+
"curl -fsS --max-time 1 http://127.0.0.1:${REPLICAS_DESKTOP_CHROME_DEBUG_PORT:-9222}/json/version" \
|
|
397
|
+
"DISPLAY=:$DISPLAY_NUM REPLICAS_DESKTOP_CHROME_PROFILE_DIR=\"$CHROME_PROFILE_DIR\" exec replicas-chrome --no-startup-window"
|
|
398
|
+
fi
|
|
390
399
|
|
|
391
400
|
exit 0
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { dirname, isAbsolute, resolve } from "path";
|
|
|
18
18
|
var DESKTOP_STREAM_PORT = 6080;
|
|
19
19
|
function getDesktopViewerUrl(publicUrl) {
|
|
20
20
|
const url = new URL(`${publicUrl.replace(/\/$/, "")}/`);
|
|
21
|
-
url.searchParams.set("v", "
|
|
21
|
+
url.searchParams.set("v", "15");
|
|
22
22
|
return url.toString();
|
|
23
23
|
}
|
|
24
24
|
var DESKTOP_VIEWER_WIDTH = 1920;
|
|
@@ -2694,11 +2694,13 @@ async function computerLaunchCommand(app, args) {
|
|
|
2694
2694
|
const fullArgs = [...baseArgs.slice(1), ...args];
|
|
2695
2695
|
const expectsNewChromePage = args.some((arg) => !arg.startsWith("-"));
|
|
2696
2696
|
const existingChromePageIds = /* @__PURE__ */ new Set();
|
|
2697
|
+
let chromeAlreadyRunning = false;
|
|
2697
2698
|
if (app === "chrome") {
|
|
2698
2699
|
try {
|
|
2699
2700
|
for (const page of await getChromePages()) {
|
|
2700
2701
|
if (page.id) existingChromePageIds.add(page.id);
|
|
2701
2702
|
}
|
|
2703
|
+
chromeAlreadyRunning = true;
|
|
2702
2704
|
} catch {
|
|
2703
2705
|
}
|
|
2704
2706
|
}
|
|
@@ -2729,10 +2731,12 @@ async function computerLaunchCommand(app, args) {
|
|
|
2729
2731
|
}
|
|
2730
2732
|
}
|
|
2731
2733
|
if (readyPage) break;
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2734
|
+
if (!chromeAlreadyRunning) {
|
|
2735
|
+
try {
|
|
2736
|
+
if (child.pid) process.kill(child.pid, 0);
|
|
2737
|
+
} catch {
|
|
2738
|
+
fail(`Chrome exited before its desktop control channel became ready.`);
|
|
2739
|
+
}
|
|
2736
2740
|
}
|
|
2737
2741
|
await sleep(100);
|
|
2738
2742
|
}
|
|
@@ -2921,7 +2925,7 @@ async function computerStatusCommand() {
|
|
|
2921
2925
|
}
|
|
2922
2926
|
|
|
2923
2927
|
// src/version.ts
|
|
2924
|
-
var COMPUTER_USE_VERSION = "0.2.
|
|
2928
|
+
var COMPUTER_USE_VERSION = "0.2.5";
|
|
2925
2929
|
var COMPUTER_USE_PROTOCOL_VERSION = 1;
|
|
2926
2930
|
|
|
2927
2931
|
// src/program.ts
|
|
@@ -3165,6 +3169,7 @@ import { spawn as spawn4 } from "child_process";
|
|
|
3165
3169
|
import { createSocket } from "dgram";
|
|
3166
3170
|
import { createReadStream, existsSync as existsSync6, statSync as statSync2 } from "fs";
|
|
3167
3171
|
import { createServer as createServer2 } from "http";
|
|
3172
|
+
import { availableParallelism } from "os";
|
|
3168
3173
|
import { dirname as dirname6, extname, resolve as resolve2 } from "path";
|
|
3169
3174
|
import { fileURLToPath } from "url";
|
|
3170
3175
|
|
|
@@ -3357,7 +3362,16 @@ var RtpClock = class {
|
|
|
3357
3362
|
base;
|
|
3358
3363
|
last;
|
|
3359
3364
|
wraps = 0;
|
|
3365
|
+
origin = 0;
|
|
3366
|
+
emitted = 0;
|
|
3367
|
+
/**
|
|
3368
|
+
* A fresh encoder starts RTP timestamps from a new random base, so the
|
|
3369
|
+
* timeline continues from the last value emitted rather than jumping
|
|
3370
|
+
* backwards — decoders and the viewer's media element both require
|
|
3371
|
+
* monotonic presentation timestamps.
|
|
3372
|
+
*/
|
|
3360
3373
|
reset() {
|
|
3374
|
+
this.origin = this.emitted;
|
|
3361
3375
|
this.base = void 0;
|
|
3362
3376
|
this.last = void 0;
|
|
3363
3377
|
this.wraps = 0;
|
|
@@ -3367,7 +3381,8 @@ var RtpClock = class {
|
|
|
3367
3381
|
this.last = timestamp;
|
|
3368
3382
|
const extended = timestamp + this.wraps * 4294967296;
|
|
3369
3383
|
this.base ??= extended;
|
|
3370
|
-
|
|
3384
|
+
this.emitted = this.origin + Math.round((extended - this.base) * 1e6 / this.rate);
|
|
3385
|
+
return this.emitted;
|
|
3371
3386
|
}
|
|
3372
3387
|
};
|
|
3373
3388
|
var startCode = new Uint8Array([0, 0, 0, 1]);
|
|
@@ -3461,9 +3476,30 @@ var H264RtpAssembler = class {
|
|
|
3461
3476
|
import WebSocket2, { WebSocketServer } from "ws";
|
|
3462
3477
|
var width = Number.parseInt(process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH), 10);
|
|
3463
3478
|
var height = Number.parseInt(process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT), 10);
|
|
3464
|
-
var
|
|
3465
|
-
var
|
|
3479
|
+
var cores = availableParallelism();
|
|
3480
|
+
var fps = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_FPS || "", 10) || 60;
|
|
3481
|
+
var x264Preset = cores >= 4 ? "superfast" : "ultrafast";
|
|
3466
3482
|
var port = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_PORT || String(DESKTOP_STREAM_PORT), 10);
|
|
3483
|
+
var configuredTopKbps = (() => {
|
|
3484
|
+
const raw = process.env.REPLICAS_DESKTOP_STREAM_VIDEO_BITRATE?.trim();
|
|
3485
|
+
const match = raw?.match(/^(\d+(?:\.\d+)?)([MmKk]?)$/);
|
|
3486
|
+
if (!match) return 2e4;
|
|
3487
|
+
const value = Number.parseFloat(match[1]);
|
|
3488
|
+
return Math.max(1, Math.round(match[2].toLowerCase() === "m" ? value * 1e3 : value));
|
|
3489
|
+
})();
|
|
3490
|
+
var qualityLadder = [
|
|
3491
|
+
{ fps, kbps: configuredTopKbps, crf: 18 },
|
|
3492
|
+
{ fps: Math.max(1, Math.round(fps / 2)), kbps: 6e3, crf: 20 },
|
|
3493
|
+
{ fps: Math.max(1, Math.round(fps / 2)), kbps: 3e3, crf: 23 },
|
|
3494
|
+
{ fps: Math.max(1, Math.round(fps / 4)), kbps: 1200, crf: 26 }
|
|
3495
|
+
];
|
|
3496
|
+
var keyframeMs = 250;
|
|
3497
|
+
var probeIntervalMs = 250;
|
|
3498
|
+
var targetBacklogMs = 120;
|
|
3499
|
+
var congestedBacklogMs = 250;
|
|
3500
|
+
var congestedDecodeQueue = 3;
|
|
3501
|
+
var fallbackBufferedBytes = 512 * 1024;
|
|
3502
|
+
var maximumBufferedBytes = 4 * 1024 * 1024;
|
|
3467
3503
|
var display = process.env.REPLICAS_DESKTOP_DISPLAY?.replace(/^([^:])/, ":$1") || ":99";
|
|
3468
3504
|
var videoCodec = "avc1.42E02A";
|
|
3469
3505
|
var config = {
|
|
@@ -3508,7 +3544,7 @@ function validInput(value) {
|
|
|
3508
3544
|
if (!value || typeof value !== "object" || !("type" in value) || typeof value.type !== "string") return false;
|
|
3509
3545
|
if (value.type === "release") return true;
|
|
3510
3546
|
if (value.type === "stats") return "videoFps" in value && typeof value.videoFps === "number" && Number.isFinite(value.videoFps) && "audioPacketsPerSecond" in value && typeof value.audioPacketsPerSecond === "number" && Number.isFinite(value.audioPacketsPerSecond) && "decodeQueue" in value && typeof value.decodeQueue === "number" && Number.isFinite(value.decodeQueue) && "rttMs" in value && typeof value.rttMs === "number" && Number.isFinite(value.rttMs);
|
|
3511
|
-
if (value.type === "ping") return "sentAt" in value && typeof value.sentAt === "number";
|
|
3547
|
+
if (value.type === "ping" || value.type === "pong") return "sentAt" in value && typeof value.sentAt === "number";
|
|
3512
3548
|
if (value.type === "key") return "code" in value && typeof value.code === "string" && value.code.length < 40 && "down" in value && typeof value.down === "boolean";
|
|
3513
3549
|
if (!("x" in value) || !("y" in value) || typeof value.x !== "number" || typeof value.y !== "number") return false;
|
|
3514
3550
|
if (value.type === "pointer") return true;
|
|
@@ -3524,23 +3560,30 @@ async function runMediaServer() {
|
|
|
3524
3560
|
const videoAssembler = new H264RtpAssembler();
|
|
3525
3561
|
const audioClock = new RtpClock(48e3);
|
|
3526
3562
|
const clients = /* @__PURE__ */ new Map();
|
|
3527
|
-
let encoder;
|
|
3528
|
-
let encoderRestart;
|
|
3529
3563
|
let encoderStop;
|
|
3564
|
+
let rung = 0;
|
|
3565
|
+
let descents = 0;
|
|
3566
|
+
let congestedSeconds = 0;
|
|
3567
|
+
let comfortableSeconds = 0;
|
|
3530
3568
|
let lastVideoAt = 0;
|
|
3531
3569
|
let lastAudioAt = 0;
|
|
3532
3570
|
let videoFrames = 0;
|
|
3533
3571
|
let audioPackets = 0;
|
|
3534
3572
|
let videoBytes = 0;
|
|
3535
3573
|
let audioBytes = 0;
|
|
3574
|
+
const queueMs = (state) => {
|
|
3575
|
+
if (!state.probeAnswered) return 0;
|
|
3576
|
+
const outstanding = state.probeSentAt ? Date.now() - state.probeSentAt : 0;
|
|
3577
|
+
return Math.max(0, Math.max(state.probeAckedMs, outstanding) - state.baselineMs);
|
|
3578
|
+
};
|
|
3536
3579
|
const broadcast = (packet, keyframe = false) => {
|
|
3537
3580
|
for (const [client, state] of clients) {
|
|
3538
3581
|
if (client.readyState !== WebSocket2.OPEN) continue;
|
|
3539
|
-
if (client.bufferedAmount >
|
|
3582
|
+
if (client.bufferedAmount > maximumBufferedBytes) {
|
|
3540
3583
|
client.close(1013, "stream client is too slow");
|
|
3541
3584
|
continue;
|
|
3542
3585
|
}
|
|
3543
|
-
if (!keyframe && (state.waitingForKeyframe || client.bufferedAmount >
|
|
3586
|
+
if (!keyframe && (state.waitingForKeyframe || queueMs(state) > targetBacklogMs || client.bufferedAmount > fallbackBufferedBytes)) {
|
|
3544
3587
|
state.waitingForKeyframe = true;
|
|
3545
3588
|
continue;
|
|
3546
3589
|
}
|
|
@@ -3567,14 +3610,78 @@ async function runMediaServer() {
|
|
|
3567
3610
|
audioBytes += rtp.payload.byteLength;
|
|
3568
3611
|
broadcast(mediaPacket(3 /* Audio */, audioClock.microseconds(rtp.timestamp), rtp.payload));
|
|
3569
3612
|
});
|
|
3570
|
-
const
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3613
|
+
const superviseEncoder = (label, prepare, buildArgs, niceness = 0) => {
|
|
3614
|
+
let child;
|
|
3615
|
+
let retiring;
|
|
3616
|
+
let restart;
|
|
3617
|
+
let intent = 0;
|
|
3618
|
+
const alive = (process2) => Boolean(process2 && process2.exitCode === null && process2.signalCode === null);
|
|
3619
|
+
const start = () => {
|
|
3620
|
+
clearTimeout(encoderStop);
|
|
3621
|
+
if (alive(child)) return;
|
|
3622
|
+
if (retiring) return;
|
|
3623
|
+
clearTimeout(restart);
|
|
3624
|
+
prepare();
|
|
3625
|
+
const argv = niceness > 0 ? ["nice", "-n", String(niceness), "ffmpeg", ...buildArgs()] : ["ffmpeg", ...buildArgs()];
|
|
3626
|
+
const spawned = spawn4(argv[0], argv.slice(1), { env: { ...process.env, DISPLAY: display }, stdio: ["ignore", "ignore", "pipe"] });
|
|
3627
|
+
spawned.stderr?.on("data", (chunk) => process.stderr.write(`[desktop-stream] ${chunk}`));
|
|
3628
|
+
child = spawned;
|
|
3629
|
+
spawned.once("exit", (code, signal) => {
|
|
3630
|
+
if (child !== spawned) return;
|
|
3631
|
+
child = void 0;
|
|
3632
|
+
if (clients.size === 0) return;
|
|
3633
|
+
process.stderr.write(`[desktop-stream] ${label} encoder exited (${signal || code}); restarting
|
|
3634
|
+
`);
|
|
3635
|
+
restart = setTimeout(start, 500);
|
|
3636
|
+
});
|
|
3637
|
+
};
|
|
3638
|
+
return {
|
|
3639
|
+
start,
|
|
3640
|
+
running: () => alive(child),
|
|
3641
|
+
stop: () => {
|
|
3642
|
+
intent += 1;
|
|
3643
|
+
clearTimeout(restart);
|
|
3644
|
+
child?.kill("SIGTERM");
|
|
3645
|
+
retiring?.kill("SIGKILL");
|
|
3646
|
+
child = void 0;
|
|
3647
|
+
retiring = void 0;
|
|
3648
|
+
},
|
|
3649
|
+
replace: () => {
|
|
3650
|
+
const issued = intent += 1;
|
|
3651
|
+
clearTimeout(restart);
|
|
3652
|
+
const previous = child ?? retiring;
|
|
3653
|
+
child = void 0;
|
|
3654
|
+
retiring = void 0;
|
|
3655
|
+
if (!alive(previous)) {
|
|
3656
|
+
start();
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
3659
|
+
retiring = previous;
|
|
3660
|
+
let settled = false;
|
|
3661
|
+
const resume = () => {
|
|
3662
|
+
if (settled) return;
|
|
3663
|
+
settled = true;
|
|
3664
|
+
clearTimeout(forceExit);
|
|
3665
|
+
clearTimeout(abandonWait);
|
|
3666
|
+
if (intent !== issued) return;
|
|
3667
|
+
retiring = void 0;
|
|
3668
|
+
if (clients.size === 0) return;
|
|
3669
|
+
start();
|
|
3670
|
+
};
|
|
3671
|
+
const forceExit = setTimeout(() => previous.kill("SIGKILL"), 2e3);
|
|
3672
|
+
const abandonWait = setTimeout(resume, 4e3);
|
|
3673
|
+
previous.once("exit", resume);
|
|
3674
|
+
previous.kill("SIGTERM");
|
|
3675
|
+
}
|
|
3676
|
+
};
|
|
3677
|
+
};
|
|
3678
|
+
const videoEncoder = superviseEncoder("video", () => {
|
|
3574
3679
|
videoAssembler.reset();
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3680
|
+
for (const state of clients.values()) state.waitingForKeyframe = true;
|
|
3681
|
+
}, () => {
|
|
3682
|
+
const quality = qualityLadder[rung];
|
|
3683
|
+
const keyframeInterval = Math.max(1, Math.round(quality.fps * keyframeMs / 1e3));
|
|
3684
|
+
return [
|
|
3578
3685
|
"-hide_banner",
|
|
3579
3686
|
"-loglevel",
|
|
3580
3687
|
"warning",
|
|
@@ -3582,16 +3689,10 @@ async function runMediaServer() {
|
|
|
3582
3689
|
"nobuffer",
|
|
3583
3690
|
"-flags",
|
|
3584
3691
|
"low_delay",
|
|
3585
|
-
"-
|
|
3586
|
-
"
|
|
3587
|
-
"-
|
|
3588
|
-
"
|
|
3589
|
-
"-fragment_size",
|
|
3590
|
-
"2048",
|
|
3591
|
-
"-use_wallclock_as_timestamps",
|
|
3592
|
-
"1",
|
|
3593
|
-
"-i",
|
|
3594
|
-
process.env.REPLICAS_DESKTOP_STREAM_AUDIO_SOURCE || "default",
|
|
3692
|
+
"-probesize",
|
|
3693
|
+
"32",
|
|
3694
|
+
"-analyzeduration",
|
|
3695
|
+
"0",
|
|
3595
3696
|
"-thread_queue_size",
|
|
3596
3697
|
"1024",
|
|
3597
3698
|
"-f",
|
|
@@ -3599,20 +3700,18 @@ async function runMediaServer() {
|
|
|
3599
3700
|
"-draw_mouse",
|
|
3600
3701
|
"0",
|
|
3601
3702
|
"-framerate",
|
|
3602
|
-
String(fps),
|
|
3703
|
+
String(quality.fps),
|
|
3603
3704
|
"-video_size",
|
|
3604
3705
|
`${width}x${height}`,
|
|
3605
3706
|
"-use_wallclock_as_timestamps",
|
|
3606
3707
|
"1",
|
|
3607
3708
|
"-i",
|
|
3608
3709
|
`${display}.0`,
|
|
3609
|
-
"-map",
|
|
3610
|
-
"1:v:0",
|
|
3611
3710
|
"-an",
|
|
3612
3711
|
"-c:v",
|
|
3613
3712
|
"libx264",
|
|
3614
3713
|
"-preset",
|
|
3615
|
-
|
|
3714
|
+
x264Preset,
|
|
3616
3715
|
"-tune",
|
|
3617
3716
|
"zerolatency",
|
|
3618
3717
|
"-profile:v",
|
|
@@ -3630,11 +3729,11 @@ async function runMediaServer() {
|
|
|
3630
3729
|
"-sc_threshold",
|
|
3631
3730
|
"0",
|
|
3632
3731
|
"-crf",
|
|
3633
|
-
|
|
3732
|
+
String(quality.crf),
|
|
3634
3733
|
"-maxrate",
|
|
3635
|
-
|
|
3734
|
+
`${quality.kbps}k`,
|
|
3636
3735
|
"-bufsize",
|
|
3637
|
-
|
|
3736
|
+
`${Math.round(quality.kbps * keyframeMs / 1e3)}k`,
|
|
3638
3737
|
"-x264-params",
|
|
3639
3738
|
`repeat-headers=1:sliced-threads=1:slices=4:keyint=${keyframeInterval}:min-keyint=${keyframeInterval}:scenecut=0`,
|
|
3640
3739
|
"-fps_mode",
|
|
@@ -3645,57 +3744,111 @@ async function runMediaServer() {
|
|
|
3645
3744
|
"rtp",
|
|
3646
3745
|
"-payload_type",
|
|
3647
3746
|
"96",
|
|
3648
|
-
`rtp://127.0.0.1:${videoPort}?pkt_size=1200&connect=1&rtcpport=${videoPort}
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3747
|
+
`rtp://127.0.0.1:${videoPort}?pkt_size=1200&connect=1&rtcpport=${videoPort}`
|
|
3748
|
+
];
|
|
3749
|
+
}, 10);
|
|
3750
|
+
const audioEncoder = superviseEncoder("audio", () => audioClock.reset(), () => [
|
|
3751
|
+
"-hide_banner",
|
|
3752
|
+
"-loglevel",
|
|
3753
|
+
"warning",
|
|
3754
|
+
"-fflags",
|
|
3755
|
+
"nobuffer",
|
|
3756
|
+
"-flags",
|
|
3757
|
+
"low_delay",
|
|
3758
|
+
"-probesize",
|
|
3759
|
+
"32",
|
|
3760
|
+
"-analyzeduration",
|
|
3761
|
+
"0",
|
|
3762
|
+
"-thread_queue_size",
|
|
3763
|
+
"1024",
|
|
3764
|
+
"-f",
|
|
3765
|
+
"pulse",
|
|
3766
|
+
"-fragment_size",
|
|
3767
|
+
"2048",
|
|
3768
|
+
"-use_wallclock_as_timestamps",
|
|
3769
|
+
"1",
|
|
3770
|
+
"-i",
|
|
3771
|
+
process.env.REPLICAS_DESKTOP_STREAM_AUDIO_SOURCE || "default",
|
|
3772
|
+
"-vn",
|
|
3773
|
+
"-af",
|
|
3774
|
+
"aresample=async=1:first_pts=0",
|
|
3775
|
+
"-c:a",
|
|
3776
|
+
"libopus",
|
|
3777
|
+
"-application",
|
|
3778
|
+
"lowdelay",
|
|
3779
|
+
"-frame_duration",
|
|
3780
|
+
"20",
|
|
3781
|
+
"-b:a",
|
|
3782
|
+
"128k",
|
|
3783
|
+
"-ar",
|
|
3784
|
+
"48000",
|
|
3785
|
+
"-ac",
|
|
3786
|
+
"2",
|
|
3787
|
+
"-flush_packets",
|
|
3788
|
+
"1",
|
|
3789
|
+
"-f",
|
|
3790
|
+
"rtp",
|
|
3791
|
+
"-payload_type",
|
|
3792
|
+
"111",
|
|
3793
|
+
`rtp://127.0.0.1:${audioPort}?pkt_size=1200&connect=1&rtcpport=${audioPort}`
|
|
3794
|
+
]);
|
|
3795
|
+
const startEncoders = () => {
|
|
3796
|
+
videoEncoder.start();
|
|
3797
|
+
audioEncoder.start();
|
|
3798
|
+
};
|
|
3799
|
+
const setRung = (next) => {
|
|
3800
|
+
if (next === rung) return;
|
|
3801
|
+
if (next > rung) descents += 1;
|
|
3802
|
+
rung = next;
|
|
3803
|
+
congestedSeconds = 0;
|
|
3804
|
+
comfortableSeconds = 0;
|
|
3805
|
+
process.stderr.write(`[desktop-stream] quality rung ${rung} (${qualityLadder[rung].fps} fps, ${qualityLadder[rung].kbps}k)
|
|
3679
3806
|
`);
|
|
3680
|
-
|
|
3681
|
-
}
|
|
3682
|
-
});
|
|
3807
|
+
videoEncoder.replace();
|
|
3683
3808
|
};
|
|
3684
|
-
const
|
|
3809
|
+
const stopEncodersSoon = () => {
|
|
3685
3810
|
clearTimeout(encoderStop);
|
|
3686
3811
|
encoderStop = setTimeout(() => {
|
|
3687
3812
|
if (clients.size > 0) return;
|
|
3688
|
-
|
|
3689
|
-
|
|
3813
|
+
videoEncoder.stop();
|
|
3814
|
+
audioEncoder.stop();
|
|
3690
3815
|
}, 5e3);
|
|
3691
3816
|
};
|
|
3817
|
+
const probe = setInterval(() => {
|
|
3818
|
+
for (const [client, state] of clients) {
|
|
3819
|
+
if (client.readyState !== WebSocket2.OPEN || state.probeSentAt) continue;
|
|
3820
|
+
state.probeSentAt = Date.now();
|
|
3821
|
+
client.send(JSON.stringify({ type: "ping", sentAt: state.probeSentAt }));
|
|
3822
|
+
}
|
|
3823
|
+
}, probeIntervalMs);
|
|
3824
|
+
probe.unref();
|
|
3825
|
+
const control = setInterval(() => {
|
|
3826
|
+
const states = [...clients.values()];
|
|
3827
|
+
if (states.length === 0) return;
|
|
3828
|
+
const struggling = states.filter((state) => queueMs(state) > congestedBacklogMs || (state.stats?.decodeQueue ?? 0) > congestedDecodeQueue).length;
|
|
3829
|
+
if (struggling === states.length) {
|
|
3830
|
+
comfortableSeconds = 0;
|
|
3831
|
+
congestedSeconds += 1;
|
|
3832
|
+
if (congestedSeconds >= 2) setRung(Math.min(qualityLadder.length - 1, rung + 1));
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
congestedSeconds = 0;
|
|
3836
|
+
if (struggling > 0) {
|
|
3837
|
+
comfortableSeconds = 0;
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
comfortableSeconds += 1;
|
|
3841
|
+
if (comfortableSeconds >= Math.min(300, 15 * 2 ** descents)) setRung(Math.max(0, rung - 1));
|
|
3842
|
+
}, 1e3);
|
|
3843
|
+
control.unref();
|
|
3692
3844
|
const httpServer = createServer2((request, response) => {
|
|
3693
3845
|
const url = new URL(request.url || "/", "http://localhost");
|
|
3694
3846
|
if (url.pathname === "/health") {
|
|
3695
3847
|
response.setHeader("content-type", "application/json");
|
|
3696
3848
|
response.end(JSON.stringify({
|
|
3697
3849
|
clients: clients.size,
|
|
3698
|
-
encoder:
|
|
3850
|
+
encoder: videoEncoder.running(),
|
|
3851
|
+
audioEncoder: audioEncoder.running(),
|
|
3699
3852
|
lastVideoFrameMsAgo: lastVideoAt ? Date.now() - lastVideoAt : null,
|
|
3700
3853
|
lastAudioPacketMsAgo: lastAudioAt ? Date.now() - lastAudioAt : null,
|
|
3701
3854
|
videoFrames,
|
|
@@ -3703,9 +3856,12 @@ async function runMediaServer() {
|
|
|
3703
3856
|
videoBytes,
|
|
3704
3857
|
audioBytes,
|
|
3705
3858
|
clientStats: [...clients.values()].flatMap((client) => client.stats ? [client.stats] : []),
|
|
3859
|
+
clientQueueMs: [...clients.values()].map((client) => Math.round(queueMs(client))),
|
|
3860
|
+
clientBaselineMs: [...clients.values()].map((client) => Number.isFinite(client.baselineMs) ? Math.round(client.baselineMs) : null),
|
|
3861
|
+
qualityRung: rung,
|
|
3706
3862
|
width,
|
|
3707
3863
|
height,
|
|
3708
|
-
fps
|
|
3864
|
+
fps: qualityLadder[rung].fps
|
|
3709
3865
|
}));
|
|
3710
3866
|
return;
|
|
3711
3867
|
}
|
|
@@ -3716,7 +3872,11 @@ async function runMediaServer() {
|
|
|
3716
3872
|
response.end("Not found");
|
|
3717
3873
|
return;
|
|
3718
3874
|
}
|
|
3719
|
-
|
|
3875
|
+
if (extname(path) === ".html") {
|
|
3876
|
+
startEncoders();
|
|
3877
|
+
if (clients.size === 0) stopEncodersSoon();
|
|
3878
|
+
}
|
|
3879
|
+
response.setHeader("cache-control", extname(path) === ".html" ? "no-store" : "private, max-age=3600");
|
|
3720
3880
|
response.setHeader("content-type", contentTypes[extname(path)] || "application/octet-stream");
|
|
3721
3881
|
createReadStream(path).pipe(response);
|
|
3722
3882
|
});
|
|
@@ -3729,9 +3889,16 @@ async function runMediaServer() {
|
|
|
3729
3889
|
webSocketServer.handleUpgrade(request, socket, head, (client) => webSocketServer.emit("connection", client, request));
|
|
3730
3890
|
});
|
|
3731
3891
|
webSocketServer.on("connection", (client) => {
|
|
3732
|
-
clients.set(client, {
|
|
3892
|
+
clients.set(client, {
|
|
3893
|
+
waitingForKeyframe: true,
|
|
3894
|
+
input: new InputBridge(display, width, height),
|
|
3895
|
+
probeSentAt: 0,
|
|
3896
|
+
probeAckedMs: 0,
|
|
3897
|
+
probeAnswered: false,
|
|
3898
|
+
baselineMs: Number.POSITIVE_INFINITY
|
|
3899
|
+
});
|
|
3733
3900
|
client.send(JSON.stringify(config));
|
|
3734
|
-
|
|
3901
|
+
startEncoders();
|
|
3735
3902
|
client.on("message", (raw, binary) => {
|
|
3736
3903
|
if (binary) return;
|
|
3737
3904
|
let message;
|
|
@@ -3742,21 +3909,33 @@ async function runMediaServer() {
|
|
|
3742
3909
|
}
|
|
3743
3910
|
if (!validInput(message)) return;
|
|
3744
3911
|
if (message.type === "ping") client.send(JSON.stringify({ type: "pong", sentAt: message.sentAt }));
|
|
3745
|
-
else if (message.type === "
|
|
3912
|
+
else if (message.type === "pong") {
|
|
3913
|
+
const state = clients.get(client);
|
|
3914
|
+
if (!state || message.sentAt !== state.probeSentAt) return;
|
|
3915
|
+
state.probeAckedMs = Date.now() - state.probeSentAt;
|
|
3916
|
+
state.probeSentAt = 0;
|
|
3917
|
+
state.probeAnswered = true;
|
|
3918
|
+
state.baselineMs = Math.min(state.baselineMs, state.probeAckedMs);
|
|
3919
|
+
} else if (message.type === "stats") {
|
|
3746
3920
|
const state = clients.get(client);
|
|
3747
3921
|
if (!state) return;
|
|
3748
3922
|
state.stats = {
|
|
3749
|
-
videoFps: message.videoFps,
|
|
3750
|
-
audioPacketsPerSecond: message.audioPacketsPerSecond,
|
|
3751
|
-
decodeQueue: message.decodeQueue,
|
|
3752
|
-
rttMs: message.rttMs
|
|
3923
|
+
videoFps: clamp(message.videoFps, 0, 1e3),
|
|
3924
|
+
audioPacketsPerSecond: clamp(message.audioPacketsPerSecond, 0, 1e3),
|
|
3925
|
+
decodeQueue: clamp(message.decodeQueue, 0, 1e3),
|
|
3926
|
+
rttMs: clamp(message.rttMs, 0, 6e5)
|
|
3753
3927
|
};
|
|
3754
3928
|
} else clients.get(client)?.input.handle(message);
|
|
3755
3929
|
});
|
|
3756
3930
|
client.once("close", () => {
|
|
3757
3931
|
clients.get(client)?.input.close();
|
|
3758
3932
|
clients.delete(client);
|
|
3759
|
-
if (clients.size
|
|
3933
|
+
if (clients.size > 0) return;
|
|
3934
|
+
rung = 0;
|
|
3935
|
+
descents = 0;
|
|
3936
|
+
congestedSeconds = 0;
|
|
3937
|
+
comfortableSeconds = 0;
|
|
3938
|
+
stopEncodersSoon();
|
|
3760
3939
|
});
|
|
3761
3940
|
});
|
|
3762
3941
|
await new Promise((resolveServer, reject) => {
|
|
@@ -3772,13 +3951,15 @@ async function runMediaServer() {
|
|
|
3772
3951
|
const close = () => {
|
|
3773
3952
|
if (closing) return;
|
|
3774
3953
|
closing = true;
|
|
3775
|
-
|
|
3954
|
+
clearInterval(probe);
|
|
3955
|
+
clearInterval(control);
|
|
3776
3956
|
clearTimeout(encoderStop);
|
|
3777
3957
|
for (const [client, state] of clients) {
|
|
3778
3958
|
state.input.close();
|
|
3779
3959
|
client.close(1001, "server shutting down");
|
|
3780
3960
|
}
|
|
3781
|
-
|
|
3961
|
+
videoEncoder.stop();
|
|
3962
|
+
audioEncoder.stop();
|
|
3782
3963
|
videoSocket.close();
|
|
3783
3964
|
audioSocket.close();
|
|
3784
3965
|
webSocketServer.close();
|
package/dist/stream-viewer.js
CHANGED
|
@@ -166,7 +166,7 @@ var handleBinary = (data) => {
|
|
|
166
166
|
}
|
|
167
167
|
if (!videoDecoder || !activeVideoConfig || videoDecoder.state !== "configured") return;
|
|
168
168
|
const key = kind === 1 /* VideoKey */;
|
|
169
|
-
if (videoDecoder.decodeQueueSize >
|
|
169
|
+
if (videoDecoder.decodeQueueSize > 3) {
|
|
170
170
|
videoDecoder.reset();
|
|
171
171
|
videoDecoder.configure(activeVideoConfig);
|
|
172
172
|
waitingForKeyframe = true;
|
|
@@ -205,6 +205,10 @@ var connect = () => {
|
|
|
205
205
|
rtt = performance.now() - message.sentAt;
|
|
206
206
|
return;
|
|
207
207
|
}
|
|
208
|
+
if (message.type === "ping") {
|
|
209
|
+
send({ type: "pong", sentAt: message.sentAt });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
208
212
|
if (message.type !== "config" || message.protocolVersion !== STREAM_PROTOCOL_VERSION) return;
|
|
209
213
|
config = message;
|
|
210
214
|
fitScreen();
|