replicas-computer 0.2.3 → 0.2.4
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/dist/index.js +240 -80
- package/dist/stream-viewer.js +5 -1
- package/package.json +1 -1
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;
|
|
@@ -2921,7 +2921,7 @@ async function computerStatusCommand() {
|
|
|
2921
2921
|
}
|
|
2922
2922
|
|
|
2923
2923
|
// src/version.ts
|
|
2924
|
-
var COMPUTER_USE_VERSION = "0.2.
|
|
2924
|
+
var COMPUTER_USE_VERSION = "0.2.4";
|
|
2925
2925
|
var COMPUTER_USE_PROTOCOL_VERSION = 1;
|
|
2926
2926
|
|
|
2927
2927
|
// src/program.ts
|
|
@@ -3357,7 +3357,16 @@ var RtpClock = class {
|
|
|
3357
3357
|
base;
|
|
3358
3358
|
last;
|
|
3359
3359
|
wraps = 0;
|
|
3360
|
+
origin = 0;
|
|
3361
|
+
emitted = 0;
|
|
3362
|
+
/**
|
|
3363
|
+
* A fresh encoder starts RTP timestamps from a new random base, so the
|
|
3364
|
+
* timeline continues from the last value emitted rather than jumping
|
|
3365
|
+
* backwards — decoders and the viewer's media element both require
|
|
3366
|
+
* monotonic presentation timestamps.
|
|
3367
|
+
*/
|
|
3360
3368
|
reset() {
|
|
3369
|
+
this.origin = this.emitted;
|
|
3361
3370
|
this.base = void 0;
|
|
3362
3371
|
this.last = void 0;
|
|
3363
3372
|
this.wraps = 0;
|
|
@@ -3367,7 +3376,8 @@ var RtpClock = class {
|
|
|
3367
3376
|
this.last = timestamp;
|
|
3368
3377
|
const extended = timestamp + this.wraps * 4294967296;
|
|
3369
3378
|
this.base ??= extended;
|
|
3370
|
-
|
|
3379
|
+
this.emitted = this.origin + Math.round((extended - this.base) * 1e6 / this.rate);
|
|
3380
|
+
return this.emitted;
|
|
3371
3381
|
}
|
|
3372
3382
|
};
|
|
3373
3383
|
var startCode = new Uint8Array([0, 0, 0, 1]);
|
|
@@ -3462,8 +3472,21 @@ import WebSocket2, { WebSocketServer } from "ws";
|
|
|
3462
3472
|
var width = Number.parseInt(process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH), 10);
|
|
3463
3473
|
var height = Number.parseInt(process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT), 10);
|
|
3464
3474
|
var fps = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_FPS || "60", 10);
|
|
3465
|
-
var keyframeInterval = fps;
|
|
3466
3475
|
var port = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_PORT || String(DESKTOP_STREAM_PORT), 10);
|
|
3476
|
+
var qualityLadder = [
|
|
3477
|
+
{ fps, bitrate: process.env.REPLICAS_DESKTOP_STREAM_VIDEO_BITRATE || "12M", crf: 18 },
|
|
3478
|
+
{ fps: Math.max(1, Math.round(fps / 2)), bitrate: "6M", crf: 20 },
|
|
3479
|
+
{ fps: Math.max(1, Math.round(fps / 2)), bitrate: "3M", crf: 23 },
|
|
3480
|
+
{ fps: Math.max(1, Math.round(fps / 4)), bitrate: "1200k", crf: 26 }
|
|
3481
|
+
];
|
|
3482
|
+
var keyframeMs = 250;
|
|
3483
|
+
var probeIntervalMs = 250;
|
|
3484
|
+
var targetBacklogMs = 120;
|
|
3485
|
+
var congestedBacklogMs = 250;
|
|
3486
|
+
var congestedDecodeQueue = 3;
|
|
3487
|
+
var probeTimeoutMs = 2e3;
|
|
3488
|
+
var fallbackBufferedBytes = 512 * 1024;
|
|
3489
|
+
var maximumBufferedBytes = 4 * 1024 * 1024;
|
|
3467
3490
|
var display = process.env.REPLICAS_DESKTOP_DISPLAY?.replace(/^([^:])/, ":$1") || ":99";
|
|
3468
3491
|
var videoCodec = "avc1.42E02A";
|
|
3469
3492
|
var config = {
|
|
@@ -3508,7 +3531,7 @@ function validInput(value) {
|
|
|
3508
3531
|
if (!value || typeof value !== "object" || !("type" in value) || typeof value.type !== "string") return false;
|
|
3509
3532
|
if (value.type === "release") return true;
|
|
3510
3533
|
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";
|
|
3534
|
+
if (value.type === "ping" || value.type === "pong") return "sentAt" in value && typeof value.sentAt === "number";
|
|
3512
3535
|
if (value.type === "key") return "code" in value && typeof value.code === "string" && value.code.length < 40 && "down" in value && typeof value.down === "boolean";
|
|
3513
3536
|
if (!("x" in value) || !("y" in value) || typeof value.x !== "number" || typeof value.y !== "number") return false;
|
|
3514
3537
|
if (value.type === "pointer") return true;
|
|
@@ -3524,23 +3547,30 @@ async function runMediaServer() {
|
|
|
3524
3547
|
const videoAssembler = new H264RtpAssembler();
|
|
3525
3548
|
const audioClock = new RtpClock(48e3);
|
|
3526
3549
|
const clients = /* @__PURE__ */ new Map();
|
|
3527
|
-
let encoder;
|
|
3528
|
-
let encoderRestart;
|
|
3529
3550
|
let encoderStop;
|
|
3551
|
+
let rung = 0;
|
|
3552
|
+
let descents = 0;
|
|
3553
|
+
let congestedSeconds = 0;
|
|
3554
|
+
let comfortableSeconds = 0;
|
|
3530
3555
|
let lastVideoAt = 0;
|
|
3531
3556
|
let lastAudioAt = 0;
|
|
3532
3557
|
let videoFrames = 0;
|
|
3533
3558
|
let audioPackets = 0;
|
|
3534
3559
|
let videoBytes = 0;
|
|
3535
3560
|
let audioBytes = 0;
|
|
3561
|
+
const queueMs = (state) => {
|
|
3562
|
+
const outstanding = state.probeSentAt ? Date.now() - state.probeSentAt : 0;
|
|
3563
|
+
if (!state.probeAnswered && outstanding > probeTimeoutMs) return 0;
|
|
3564
|
+
return Math.max(state.probeAckedMs, outstanding);
|
|
3565
|
+
};
|
|
3536
3566
|
const broadcast = (packet, keyframe = false) => {
|
|
3537
3567
|
for (const [client, state] of clients) {
|
|
3538
3568
|
if (client.readyState !== WebSocket2.OPEN) continue;
|
|
3539
|
-
if (client.bufferedAmount >
|
|
3569
|
+
if (client.bufferedAmount > maximumBufferedBytes) {
|
|
3540
3570
|
client.close(1013, "stream client is too slow");
|
|
3541
3571
|
continue;
|
|
3542
3572
|
}
|
|
3543
|
-
if (!keyframe && (state.waitingForKeyframe || client.bufferedAmount >
|
|
3573
|
+
if (!keyframe && (state.waitingForKeyframe || queueMs(state) > targetBacklogMs || client.bufferedAmount > fallbackBufferedBytes)) {
|
|
3544
3574
|
state.waitingForKeyframe = true;
|
|
3545
3575
|
continue;
|
|
3546
3576
|
}
|
|
@@ -3567,14 +3597,77 @@ async function runMediaServer() {
|
|
|
3567
3597
|
audioBytes += rtp.payload.byteLength;
|
|
3568
3598
|
broadcast(mediaPacket(3 /* Audio */, audioClock.microseconds(rtp.timestamp), rtp.payload));
|
|
3569
3599
|
});
|
|
3570
|
-
const
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3600
|
+
const superviseEncoder = (label, prepare, buildArgs) => {
|
|
3601
|
+
let child;
|
|
3602
|
+
let retiring;
|
|
3603
|
+
let restart;
|
|
3604
|
+
let intent = 0;
|
|
3605
|
+
const alive = (process2) => Boolean(process2 && process2.exitCode === null && process2.signalCode === null);
|
|
3606
|
+
const start = () => {
|
|
3607
|
+
clearTimeout(encoderStop);
|
|
3608
|
+
if (alive(child)) return;
|
|
3609
|
+
if (retiring) return;
|
|
3610
|
+
clearTimeout(restart);
|
|
3611
|
+
prepare();
|
|
3612
|
+
const spawned = spawn4("ffmpeg", buildArgs(), { env: { ...process.env, DISPLAY: display }, stdio: ["ignore", "ignore", "pipe"] });
|
|
3613
|
+
spawned.stderr?.on("data", (chunk) => process.stderr.write(`[desktop-stream] ${chunk}`));
|
|
3614
|
+
child = spawned;
|
|
3615
|
+
spawned.once("exit", (code, signal) => {
|
|
3616
|
+
if (child !== spawned) return;
|
|
3617
|
+
child = void 0;
|
|
3618
|
+
if (clients.size === 0) return;
|
|
3619
|
+
process.stderr.write(`[desktop-stream] ${label} encoder exited (${signal || code}); restarting
|
|
3620
|
+
`);
|
|
3621
|
+
restart = setTimeout(start, 500);
|
|
3622
|
+
});
|
|
3623
|
+
};
|
|
3624
|
+
return {
|
|
3625
|
+
start,
|
|
3626
|
+
running: () => alive(child),
|
|
3627
|
+
stop: () => {
|
|
3628
|
+
intent += 1;
|
|
3629
|
+
clearTimeout(restart);
|
|
3630
|
+
child?.kill("SIGTERM");
|
|
3631
|
+
retiring?.kill("SIGKILL");
|
|
3632
|
+
child = void 0;
|
|
3633
|
+
retiring = void 0;
|
|
3634
|
+
},
|
|
3635
|
+
replace: () => {
|
|
3636
|
+
const issued = intent += 1;
|
|
3637
|
+
clearTimeout(restart);
|
|
3638
|
+
const previous = child ?? retiring;
|
|
3639
|
+
child = void 0;
|
|
3640
|
+
retiring = void 0;
|
|
3641
|
+
if (!alive(previous)) {
|
|
3642
|
+
start();
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
retiring = previous;
|
|
3646
|
+
let settled = false;
|
|
3647
|
+
const resume = () => {
|
|
3648
|
+
if (settled) return;
|
|
3649
|
+
settled = true;
|
|
3650
|
+
clearTimeout(forceExit);
|
|
3651
|
+
clearTimeout(abandonWait);
|
|
3652
|
+
if (intent !== issued) return;
|
|
3653
|
+
retiring = void 0;
|
|
3654
|
+
if (clients.size === 0) return;
|
|
3655
|
+
start();
|
|
3656
|
+
};
|
|
3657
|
+
const forceExit = setTimeout(() => previous.kill("SIGKILL"), 2e3);
|
|
3658
|
+
const abandonWait = setTimeout(resume, 4e3);
|
|
3659
|
+
previous.once("exit", resume);
|
|
3660
|
+
previous.kill("SIGTERM");
|
|
3661
|
+
}
|
|
3662
|
+
};
|
|
3663
|
+
};
|
|
3664
|
+
const videoEncoder = superviseEncoder("video", () => {
|
|
3574
3665
|
videoAssembler.reset();
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3666
|
+
for (const state of clients.values()) state.waitingForKeyframe = true;
|
|
3667
|
+
}, () => {
|
|
3668
|
+
const quality = qualityLadder[rung];
|
|
3669
|
+
const keyframeInterval = Math.max(1, Math.round(quality.fps * keyframeMs / 1e3));
|
|
3670
|
+
return [
|
|
3578
3671
|
"-hide_banner",
|
|
3579
3672
|
"-loglevel",
|
|
3580
3673
|
"warning",
|
|
@@ -3582,16 +3675,10 @@ async function runMediaServer() {
|
|
|
3582
3675
|
"nobuffer",
|
|
3583
3676
|
"-flags",
|
|
3584
3677
|
"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",
|
|
3678
|
+
"-probesize",
|
|
3679
|
+
"32",
|
|
3680
|
+
"-analyzeduration",
|
|
3681
|
+
"0",
|
|
3595
3682
|
"-thread_queue_size",
|
|
3596
3683
|
"1024",
|
|
3597
3684
|
"-f",
|
|
@@ -3599,15 +3686,13 @@ async function runMediaServer() {
|
|
|
3599
3686
|
"-draw_mouse",
|
|
3600
3687
|
"0",
|
|
3601
3688
|
"-framerate",
|
|
3602
|
-
String(fps),
|
|
3689
|
+
String(quality.fps),
|
|
3603
3690
|
"-video_size",
|
|
3604
3691
|
`${width}x${height}`,
|
|
3605
3692
|
"-use_wallclock_as_timestamps",
|
|
3606
3693
|
"1",
|
|
3607
3694
|
"-i",
|
|
3608
3695
|
`${display}.0`,
|
|
3609
|
-
"-map",
|
|
3610
|
-
"1:v:0",
|
|
3611
3696
|
"-an",
|
|
3612
3697
|
"-c:v",
|
|
3613
3698
|
"libx264",
|
|
@@ -3630,9 +3715,9 @@ async function runMediaServer() {
|
|
|
3630
3715
|
"-sc_threshold",
|
|
3631
3716
|
"0",
|
|
3632
3717
|
"-crf",
|
|
3633
|
-
|
|
3718
|
+
String(quality.crf),
|
|
3634
3719
|
"-maxrate",
|
|
3635
|
-
|
|
3720
|
+
quality.bitrate,
|
|
3636
3721
|
"-bufsize",
|
|
3637
3722
|
"1M",
|
|
3638
3723
|
"-x264-params",
|
|
@@ -3645,57 +3730,111 @@ async function runMediaServer() {
|
|
|
3645
3730
|
"rtp",
|
|
3646
3731
|
"-payload_type",
|
|
3647
3732
|
"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
|
-
|
|
3733
|
+
`rtp://127.0.0.1:${videoPort}?pkt_size=1200&connect=1&rtcpport=${videoPort}`
|
|
3734
|
+
];
|
|
3735
|
+
});
|
|
3736
|
+
const audioEncoder = superviseEncoder("audio", () => audioClock.reset(), () => [
|
|
3737
|
+
"-hide_banner",
|
|
3738
|
+
"-loglevel",
|
|
3739
|
+
"warning",
|
|
3740
|
+
"-fflags",
|
|
3741
|
+
"nobuffer",
|
|
3742
|
+
"-flags",
|
|
3743
|
+
"low_delay",
|
|
3744
|
+
"-probesize",
|
|
3745
|
+
"32",
|
|
3746
|
+
"-analyzeduration",
|
|
3747
|
+
"0",
|
|
3748
|
+
"-thread_queue_size",
|
|
3749
|
+
"1024",
|
|
3750
|
+
"-f",
|
|
3751
|
+
"pulse",
|
|
3752
|
+
"-fragment_size",
|
|
3753
|
+
"2048",
|
|
3754
|
+
"-use_wallclock_as_timestamps",
|
|
3755
|
+
"1",
|
|
3756
|
+
"-i",
|
|
3757
|
+
process.env.REPLICAS_DESKTOP_STREAM_AUDIO_SOURCE || "default",
|
|
3758
|
+
"-vn",
|
|
3759
|
+
"-af",
|
|
3760
|
+
"aresample=async=1:first_pts=0",
|
|
3761
|
+
"-c:a",
|
|
3762
|
+
"libopus",
|
|
3763
|
+
"-application",
|
|
3764
|
+
"lowdelay",
|
|
3765
|
+
"-frame_duration",
|
|
3766
|
+
"20",
|
|
3767
|
+
"-b:a",
|
|
3768
|
+
"128k",
|
|
3769
|
+
"-ar",
|
|
3770
|
+
"48000",
|
|
3771
|
+
"-ac",
|
|
3772
|
+
"2",
|
|
3773
|
+
"-flush_packets",
|
|
3774
|
+
"1",
|
|
3775
|
+
"-f",
|
|
3776
|
+
"rtp",
|
|
3777
|
+
"-payload_type",
|
|
3778
|
+
"111",
|
|
3779
|
+
`rtp://127.0.0.1:${audioPort}?pkt_size=1200&connect=1&rtcpport=${audioPort}`
|
|
3780
|
+
]);
|
|
3781
|
+
const startEncoders = () => {
|
|
3782
|
+
videoEncoder.start();
|
|
3783
|
+
audioEncoder.start();
|
|
3784
|
+
};
|
|
3785
|
+
const setRung = (next) => {
|
|
3786
|
+
if (next === rung) return;
|
|
3787
|
+
if (next > rung) descents += 1;
|
|
3788
|
+
rung = next;
|
|
3789
|
+
congestedSeconds = 0;
|
|
3790
|
+
comfortableSeconds = 0;
|
|
3791
|
+
process.stderr.write(`[desktop-stream] quality rung ${rung} (${qualityLadder[rung].fps} fps, ${qualityLadder[rung].bitrate})
|
|
3679
3792
|
`);
|
|
3680
|
-
|
|
3681
|
-
}
|
|
3682
|
-
});
|
|
3793
|
+
videoEncoder.replace();
|
|
3683
3794
|
};
|
|
3684
|
-
const
|
|
3795
|
+
const stopEncodersSoon = () => {
|
|
3685
3796
|
clearTimeout(encoderStop);
|
|
3686
3797
|
encoderStop = setTimeout(() => {
|
|
3687
3798
|
if (clients.size > 0) return;
|
|
3688
|
-
|
|
3689
|
-
|
|
3799
|
+
videoEncoder.stop();
|
|
3800
|
+
audioEncoder.stop();
|
|
3690
3801
|
}, 5e3);
|
|
3691
3802
|
};
|
|
3803
|
+
const probe = setInterval(() => {
|
|
3804
|
+
for (const [client, state] of clients) {
|
|
3805
|
+
if (client.readyState !== WebSocket2.OPEN || state.probeSentAt) continue;
|
|
3806
|
+
state.probeSentAt = Date.now();
|
|
3807
|
+
client.send(JSON.stringify({ type: "ping", sentAt: state.probeSentAt }));
|
|
3808
|
+
}
|
|
3809
|
+
}, probeIntervalMs);
|
|
3810
|
+
probe.unref();
|
|
3811
|
+
const control = setInterval(() => {
|
|
3812
|
+
const states = [...clients.values()];
|
|
3813
|
+
if (states.length === 0) return;
|
|
3814
|
+
const struggling = states.filter((state) => queueMs(state) > congestedBacklogMs || (state.stats?.decodeQueue ?? 0) > congestedDecodeQueue).length;
|
|
3815
|
+
if (struggling === states.length) {
|
|
3816
|
+
comfortableSeconds = 0;
|
|
3817
|
+
congestedSeconds += 1;
|
|
3818
|
+
if (congestedSeconds >= 2) setRung(Math.min(qualityLadder.length - 1, rung + 1));
|
|
3819
|
+
return;
|
|
3820
|
+
}
|
|
3821
|
+
congestedSeconds = 0;
|
|
3822
|
+
if (struggling > 0) {
|
|
3823
|
+
comfortableSeconds = 0;
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3826
|
+
comfortableSeconds += 1;
|
|
3827
|
+
if (comfortableSeconds >= Math.min(300, 15 * 2 ** descents)) setRung(Math.max(0, rung - 1));
|
|
3828
|
+
}, 1e3);
|
|
3829
|
+
control.unref();
|
|
3692
3830
|
const httpServer = createServer2((request, response) => {
|
|
3693
3831
|
const url = new URL(request.url || "/", "http://localhost");
|
|
3694
3832
|
if (url.pathname === "/health") {
|
|
3695
3833
|
response.setHeader("content-type", "application/json");
|
|
3696
3834
|
response.end(JSON.stringify({
|
|
3697
3835
|
clients: clients.size,
|
|
3698
|
-
encoder:
|
|
3836
|
+
encoder: videoEncoder.running(),
|
|
3837
|
+
audioEncoder: audioEncoder.running(),
|
|
3699
3838
|
lastVideoFrameMsAgo: lastVideoAt ? Date.now() - lastVideoAt : null,
|
|
3700
3839
|
lastAudioPacketMsAgo: lastAudioAt ? Date.now() - lastAudioAt : null,
|
|
3701
3840
|
videoFrames,
|
|
@@ -3703,9 +3842,11 @@ async function runMediaServer() {
|
|
|
3703
3842
|
videoBytes,
|
|
3704
3843
|
audioBytes,
|
|
3705
3844
|
clientStats: [...clients.values()].flatMap((client) => client.stats ? [client.stats] : []),
|
|
3845
|
+
clientQueueMs: [...clients.values()].map((client) => Math.round(queueMs(client))),
|
|
3846
|
+
qualityRung: rung,
|
|
3706
3847
|
width,
|
|
3707
3848
|
height,
|
|
3708
|
-
fps
|
|
3849
|
+
fps: qualityLadder[rung].fps
|
|
3709
3850
|
}));
|
|
3710
3851
|
return;
|
|
3711
3852
|
}
|
|
@@ -3729,9 +3870,15 @@ async function runMediaServer() {
|
|
|
3729
3870
|
webSocketServer.handleUpgrade(request, socket, head, (client) => webSocketServer.emit("connection", client, request));
|
|
3730
3871
|
});
|
|
3731
3872
|
webSocketServer.on("connection", (client) => {
|
|
3732
|
-
clients.set(client, {
|
|
3873
|
+
clients.set(client, {
|
|
3874
|
+
waitingForKeyframe: true,
|
|
3875
|
+
input: new InputBridge(display, width, height),
|
|
3876
|
+
probeSentAt: 0,
|
|
3877
|
+
probeAckedMs: 0,
|
|
3878
|
+
probeAnswered: false
|
|
3879
|
+
});
|
|
3733
3880
|
client.send(JSON.stringify(config));
|
|
3734
|
-
|
|
3881
|
+
startEncoders();
|
|
3735
3882
|
client.on("message", (raw, binary) => {
|
|
3736
3883
|
if (binary) return;
|
|
3737
3884
|
let message;
|
|
@@ -3742,21 +3889,32 @@ async function runMediaServer() {
|
|
|
3742
3889
|
}
|
|
3743
3890
|
if (!validInput(message)) return;
|
|
3744
3891
|
if (message.type === "ping") client.send(JSON.stringify({ type: "pong", sentAt: message.sentAt }));
|
|
3745
|
-
else if (message.type === "
|
|
3892
|
+
else if (message.type === "pong") {
|
|
3893
|
+
const state = clients.get(client);
|
|
3894
|
+
if (!state || message.sentAt !== state.probeSentAt) return;
|
|
3895
|
+
state.probeAckedMs = Date.now() - state.probeSentAt;
|
|
3896
|
+
state.probeSentAt = 0;
|
|
3897
|
+
state.probeAnswered = true;
|
|
3898
|
+
} else if (message.type === "stats") {
|
|
3746
3899
|
const state = clients.get(client);
|
|
3747
3900
|
if (!state) return;
|
|
3748
3901
|
state.stats = {
|
|
3749
|
-
videoFps: message.videoFps,
|
|
3750
|
-
audioPacketsPerSecond: message.audioPacketsPerSecond,
|
|
3751
|
-
decodeQueue: message.decodeQueue,
|
|
3752
|
-
rttMs: message.rttMs
|
|
3902
|
+
videoFps: clamp(message.videoFps, 0, 1e3),
|
|
3903
|
+
audioPacketsPerSecond: clamp(message.audioPacketsPerSecond, 0, 1e3),
|
|
3904
|
+
decodeQueue: clamp(message.decodeQueue, 0, 1e3),
|
|
3905
|
+
rttMs: clamp(message.rttMs, 0, 6e5)
|
|
3753
3906
|
};
|
|
3754
3907
|
} else clients.get(client)?.input.handle(message);
|
|
3755
3908
|
});
|
|
3756
3909
|
client.once("close", () => {
|
|
3757
3910
|
clients.get(client)?.input.close();
|
|
3758
3911
|
clients.delete(client);
|
|
3759
|
-
if (clients.size
|
|
3912
|
+
if (clients.size > 0) return;
|
|
3913
|
+
rung = 0;
|
|
3914
|
+
descents = 0;
|
|
3915
|
+
congestedSeconds = 0;
|
|
3916
|
+
comfortableSeconds = 0;
|
|
3917
|
+
stopEncodersSoon();
|
|
3760
3918
|
});
|
|
3761
3919
|
});
|
|
3762
3920
|
await new Promise((resolveServer, reject) => {
|
|
@@ -3772,13 +3930,15 @@ async function runMediaServer() {
|
|
|
3772
3930
|
const close = () => {
|
|
3773
3931
|
if (closing) return;
|
|
3774
3932
|
closing = true;
|
|
3775
|
-
|
|
3933
|
+
clearInterval(probe);
|
|
3934
|
+
clearInterval(control);
|
|
3776
3935
|
clearTimeout(encoderStop);
|
|
3777
3936
|
for (const [client, state] of clients) {
|
|
3778
3937
|
state.input.close();
|
|
3779
3938
|
client.close(1001, "server shutting down");
|
|
3780
3939
|
}
|
|
3781
|
-
|
|
3940
|
+
videoEncoder.stop();
|
|
3941
|
+
audioEncoder.stop();
|
|
3782
3942
|
videoSocket.close();
|
|
3783
3943
|
audioSocket.close();
|
|
3784
3944
|
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();
|