replicas-computer 0.3.12 → 0.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +28 -12
- package/dist/stream-viewer.js +86 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ The versioned computer-use runtime for Replicas workspace images.
|
|
|
17
17
|
- `xsettingsd` broadcasts theme changes to running GTK apps and Chrome. Chrome follows the dashboard theme without reopening tabs; launch it without a `GTK_THEME` override.
|
|
18
18
|
- `assets/desktop/` owns the desktop theme: `brand-wallpaper.svg` (regenerate with `bun scripts/render-wallpaper.ts`), `tint2rc` dock, `openbox-theme/`, `gtk-settings.ini`, the `replicas-terminal` wrapper, `themes/` palettes that `start-services.sh` renders into those templates (`--theme light|dark` switches a running desktop), and the bundled fonts (static TTF instances of General Sans and Geist Mono; variable and WOFF2 builds render with wrong metrics in Pango and VTE).
|
|
19
19
|
|
|
20
|
-
The viewer and agent command paths are independent. One on-demand encoder pair is shared by all viewers, stops when the last viewer disconnects, and restarts without reloading the page.
|
|
20
|
+
The viewer and agent command paths are independent. One on-demand encoder pair is shared by all viewers, stops when the last viewer disconnects, and restarts without reloading the page. A viewer out of sight (the dashboard keeps one connected while the Desktop tab is hidden) reports `standby`, detected with an `IntersectionObserver` because a hidden iframe keeps its last layout size; while every viewer is in standby the encoder runs at 5 fps on the 1.5 Mbps rung, and the first viewer to become visible swaps it back to full rate without a gap.
|
|
21
21
|
|
|
22
22
|
Chrome disables GPU compositing when no `/dev/dri/renderD*` device is available. This avoids Mesa's CPU-rendered compositor overhead before capture while retaining WebGL support. Desktops with a hardware render device keep Chrome's normal compositing path.
|
|
23
23
|
|
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.14";
|
|
2941
2941
|
var COMPUTER_USE_PROTOCOL_VERSION = 1;
|
|
2942
2942
|
|
|
2943
2943
|
// src/program.ts
|
|
@@ -3628,6 +3628,8 @@ function validInput(value) {
|
|
|
3628
3628
|
return true;
|
|
3629
3629
|
case "theme":
|
|
3630
3630
|
return value.theme === "light" || value.theme === "dark";
|
|
3631
|
+
case "standby":
|
|
3632
|
+
return typeof value.standby === "boolean";
|
|
3631
3633
|
case "ping":
|
|
3632
3634
|
case "pong":
|
|
3633
3635
|
return finite(value.sentAt);
|
|
@@ -4069,7 +4071,7 @@ var WebRtcViewer = class {
|
|
|
4069
4071
|
// src/video-encoder.ts
|
|
4070
4072
|
var captureBinary = resolve2(dirname6(fileURLToPath(import.meta.url)), "..", "dist", "replicas-capture");
|
|
4071
4073
|
function superviseVideoEncoder(options) {
|
|
4072
|
-
const { display: display2, width: width2, height: height2,
|
|
4074
|
+
const { display: display2, width: width2, height: height2, colorMode: colorMode2 } = options;
|
|
4073
4075
|
const env = { ...process.env, DISPLAY: display2 };
|
|
4074
4076
|
let nativeCapture = process.env.REPLICAS_DESKTOP_STREAM_CAPTURE !== "ffmpeg" && existsSync6(captureBinary);
|
|
4075
4077
|
let captureMetrics;
|
|
@@ -4098,7 +4100,7 @@ function superviseVideoEncoder(options) {
|
|
|
4098
4100
|
onPacket: options.onPacket,
|
|
4099
4101
|
joinable: (packet) => isKeyframeStart(rtpPayload(packet)),
|
|
4100
4102
|
args: (udpPort, warmupFrames) => {
|
|
4101
|
-
const { quality, keyframeMs, profile: videoProfile, intraRefresh, pixelFormat, chromaQpOffset, preset } = options.params();
|
|
4103
|
+
const { quality, keyframeMs, fps: fps2, profile: videoProfile, intraRefresh, pixelFormat, chromaQpOffset, preset } = options.params();
|
|
4102
4104
|
const keyframeInterval = Math.max(1, Math.round(fps2 * keyframeMs / 1e3));
|
|
4103
4105
|
if (nativeCapture) return [
|
|
4104
4106
|
"--width",
|
|
@@ -4445,6 +4447,8 @@ var width = Number.parseInt(process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP
|
|
|
4445
4447
|
var height = Number.parseInt(process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT), 10);
|
|
4446
4448
|
var basePreset = availableParallelism2() >= 4 ? "superfast" : "ultrafast";
|
|
4447
4449
|
var fps = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_FPS || "", 10) || 60;
|
|
4450
|
+
var standbyFps = 5;
|
|
4451
|
+
var standbyRung = 5;
|
|
4448
4452
|
var port = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_PORT || String(DESKTOP_STREAM_PORT), 10);
|
|
4449
4453
|
var display = process.env.REPLICAS_DESKTOP_DISPLAY?.replace(/^([^:])/, ":$1") || ":99";
|
|
4450
4454
|
var webrtcEnabled = process.env.REPLICAS_DESKTOP_STREAM_WEBRTC !== "0";
|
|
@@ -4601,8 +4605,11 @@ async function runMediaServer() {
|
|
|
4601
4605
|
if (accepted.length === 0) return "main";
|
|
4602
4606
|
return PROFILE_ORDER.find((candidate) => (!requires420 || candidate !== "high444") && accepted.every((set) => set.has(candidate))) ?? "baseline";
|
|
4603
4607
|
};
|
|
4608
|
+
const allStandby = () => viewers.size > 0 && [...viewers.values()].every((viewer) => viewer.standby);
|
|
4609
|
+
const streamFps = () => allStandby() ? Math.min(fps, standbyFps) : fps;
|
|
4604
4610
|
const encoderParams = () => {
|
|
4605
|
-
const quality = ladder()[rung];
|
|
4611
|
+
const quality = ladder()[allStandby() ? Math.max(rung, standbyRung) : rung];
|
|
4612
|
+
const encodeFps = streamFps();
|
|
4606
4613
|
const sockets = socketViewers().length > 0;
|
|
4607
4614
|
const keyframeMs = sockets ? Math.min(quality.keyframeMs, fallbackKeyframeMs) : quality.keyframeMs;
|
|
4608
4615
|
const videoProfile = profile();
|
|
@@ -4615,8 +4622,9 @@ async function runMediaServer() {
|
|
|
4615
4622
|
keyframeMs,
|
|
4616
4623
|
intraRefresh,
|
|
4617
4624
|
pixelFormat,
|
|
4625
|
+
fps: encodeFps,
|
|
4618
4626
|
chromaQpOffset: profiles[activeProfile].chromaQpOffset,
|
|
4619
|
-
signature: `rung ${rung} \xB7 ${activeProfile} crf ${quality.crf} \xB7 ${preset} \xB7 ${videoProfile} \xB7 keyframe ${keyframeMs}ms${intraRefresh ? " refresh" : ""}`
|
|
4627
|
+
signature: `rung ${rung} \xB7 ${activeProfile} crf ${quality.crf} \xB7 ${preset} \xB7 ${videoProfile} \xB7 keyframe ${keyframeMs}ms${intraRefresh ? " refresh" : ""} \xB7 ${encodeFps} fps`
|
|
4620
4628
|
};
|
|
4621
4629
|
};
|
|
4622
4630
|
const budgetBps = () => ladder()[rung].kbps * 1e3 * budgetFactor;
|
|
@@ -4663,7 +4671,7 @@ async function runMediaServer() {
|
|
|
4663
4671
|
}
|
|
4664
4672
|
}
|
|
4665
4673
|
const last = Boolean(packet[1] & 128);
|
|
4666
|
-
videoSplicer.splice(packet, generation, Math.round(9e4 /
|
|
4674
|
+
videoSplicer.splice(packet, generation, Math.round(9e4 / streamFps()));
|
|
4667
4675
|
let rtp;
|
|
4668
4676
|
let sockets = false;
|
|
4669
4677
|
for (const viewer of viewers.values()) {
|
|
@@ -4699,13 +4707,12 @@ async function runMediaServer() {
|
|
|
4699
4707
|
display,
|
|
4700
4708
|
width,
|
|
4701
4709
|
height,
|
|
4702
|
-
fps,
|
|
4703
4710
|
colorMode,
|
|
4704
4711
|
params: encoderParams,
|
|
4705
4712
|
onPacket: onVideoPacket,
|
|
4706
4713
|
record
|
|
4707
4714
|
});
|
|
4708
|
-
const expectedFps = () => videoEncoder.capture().captureBackend === "native" ? Math.max(1, Math.min(
|
|
4715
|
+
const expectedFps = () => videoEncoder.capture().captureBackend === "native" ? Math.max(1, Math.min(streamFps(), encoderFps)) : streamFps();
|
|
4709
4716
|
const audioEncoder = superviseEncoder({
|
|
4710
4717
|
label: "audio",
|
|
4711
4718
|
log: (message) => record("audio encoder", message),
|
|
@@ -4876,7 +4883,7 @@ async function runMediaServer() {
|
|
|
4876
4883
|
if (viewers.size === 0) return;
|
|
4877
4884
|
const { captureBackend, captureMetrics } = videoEncoder.capture();
|
|
4878
4885
|
const captureBusy = captureBackend !== "native" || !captureMetrics || captureMetrics.busyMs / captureMetrics.elapsedMs > 0.8;
|
|
4879
|
-
if (videoEncoder.running() && encoderFps <
|
|
4886
|
+
if (videoEncoder.running() && encoderFps < streamFps() * 0.9 && captureBusy) {
|
|
4880
4887
|
cleanTransportSeconds = 0;
|
|
4881
4888
|
congestedSeconds = 0;
|
|
4882
4889
|
comfortableSeconds = 0;
|
|
@@ -5143,6 +5150,12 @@ async function runMediaServer() {
|
|
|
5143
5150
|
case "theme":
|
|
5144
5151
|
applyDesktopTheme(message.theme);
|
|
5145
5152
|
return;
|
|
5153
|
+
case "standby":
|
|
5154
|
+
if (viewer.standby === message.standby) return;
|
|
5155
|
+
viewer.standby = message.standby;
|
|
5156
|
+
record(message.standby ? "viewer standby" : "viewer active", `${[...viewers.values()].filter((v) => v.standby).length} of ${viewers.size} in standby`);
|
|
5157
|
+
syncEncoder();
|
|
5158
|
+
return;
|
|
5146
5159
|
default:
|
|
5147
5160
|
viewer.input.handle(message);
|
|
5148
5161
|
}
|
|
@@ -5185,6 +5198,8 @@ async function runMediaServer() {
|
|
|
5185
5198
|
audioBytes,
|
|
5186
5199
|
videoKbps,
|
|
5187
5200
|
encoderFps,
|
|
5201
|
+
streamFps: streamFps(),
|
|
5202
|
+
standbyViewers: all.filter((viewer) => viewer.standby).length,
|
|
5188
5203
|
...videoEncoder.capture(),
|
|
5189
5204
|
cpu,
|
|
5190
5205
|
clientStats: all.flatMap((viewer) => viewer.stats ? [viewer.stats] : []),
|
|
@@ -5238,13 +5253,14 @@ async function runMediaServer() {
|
|
|
5238
5253
|
socket.destroy();
|
|
5239
5254
|
return;
|
|
5240
5255
|
}
|
|
5241
|
-
webSocketServer.handleUpgrade(request, socket, head, (client) => webSocketServer.emit("connection", client, parseStreamRequest(url.searchParams, defaultRequest)));
|
|
5256
|
+
webSocketServer.handleUpgrade(request, socket, head, (client) => webSocketServer.emit("connection", client, parseStreamRequest(url.searchParams, defaultRequest), url.searchParams.get("standby") === "1"));
|
|
5242
5257
|
});
|
|
5243
|
-
webSocketServer.on("connection", (socket, request) => {
|
|
5258
|
+
webSocketServer.on("connection", (socket, request, standby) => {
|
|
5244
5259
|
const viewer = {
|
|
5245
5260
|
socket,
|
|
5246
5261
|
input: new InputBridge(display, width, height),
|
|
5247
5262
|
request,
|
|
5263
|
+
standby,
|
|
5248
5264
|
fallback: false,
|
|
5249
5265
|
waitingForKeyframe: true,
|
|
5250
5266
|
withheldFrames: 0,
|
|
@@ -5269,7 +5285,7 @@ async function runMediaServer() {
|
|
|
5269
5285
|
if (!adaptive) rung = 0;
|
|
5270
5286
|
}
|
|
5271
5287
|
viewers.set(socket, viewer);
|
|
5272
|
-
record("viewer connected", `${request.transport} requested, ${viewers.size} connected`);
|
|
5288
|
+
record("viewer connected", `${request.transport} requested${standby ? " in standby" : ""}, ${viewers.size} connected`);
|
|
5273
5289
|
const reply = (message) => {
|
|
5274
5290
|
if (socket.readyState === WebSocket2.OPEN) socket.send(JSON.stringify(message));
|
|
5275
5291
|
};
|
package/dist/stream-viewer.js
CHANGED
|
@@ -45,6 +45,7 @@ var DESKTOP_WEBRTC_FAILURE_REASONS = {
|
|
|
45
45
|
video_stalled: "WebRTC video stopped arriving",
|
|
46
46
|
full_chroma_undecodable: "WebRTC could not decode H.264 High 4:4:4"
|
|
47
47
|
};
|
|
48
|
+
var DESKTOP_QUALITY_MESSAGE = "replicas-desktop-quality";
|
|
48
49
|
|
|
49
50
|
// src/input-validation.ts
|
|
50
51
|
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
@@ -100,6 +101,7 @@ diagnostics.classList.toggle("visible", showDiagnostics);
|
|
|
100
101
|
var webrtcFrameTimeoutMs = 6e3;
|
|
101
102
|
var webrtcSkipsAfterFailure = 3;
|
|
102
103
|
var pointerIntervalMs = 4;
|
|
104
|
+
var qualityWindowMs = 3e4;
|
|
103
105
|
var fullChromaFailureKey = "replicas-desktop-4:4:4-failed";
|
|
104
106
|
var skipFullChroma = false;
|
|
105
107
|
var fullChromaFailure;
|
|
@@ -108,6 +110,8 @@ try {
|
|
|
108
110
|
} catch {
|
|
109
111
|
}
|
|
110
112
|
var socket;
|
|
113
|
+
var standby = innerWidth === 0 || innerHeight === 0;
|
|
114
|
+
var standbyTimer;
|
|
111
115
|
var config;
|
|
112
116
|
var transport;
|
|
113
117
|
var transportReason = "";
|
|
@@ -151,6 +155,18 @@ var inboundAt = 0;
|
|
|
151
155
|
var inboundHistory = [];
|
|
152
156
|
var telemetry;
|
|
153
157
|
var route = { path: "", codec: "", decoded: "", kbps: 0, displayedFps: 0 };
|
|
158
|
+
var qualitySamples = [];
|
|
159
|
+
var qualityWindow;
|
|
160
|
+
var qualityUnseen = { decoded: 0, presented: 0, samples: 0 };
|
|
161
|
+
var qualitySequence = 0;
|
|
162
|
+
var presentedAt = 0;
|
|
163
|
+
var socketDecodedFrames = 0;
|
|
164
|
+
var restartQuality = () => {
|
|
165
|
+
qualitySamples = [];
|
|
166
|
+
qualityWindow = void 0;
|
|
167
|
+
qualitySequence = 0;
|
|
168
|
+
presentedAt = 0;
|
|
169
|
+
};
|
|
154
170
|
var state = {};
|
|
155
171
|
globalThis.__replicasDesktop = state;
|
|
156
172
|
var viewerEvents = [];
|
|
@@ -290,6 +306,7 @@ var configureVideo = async (next, preferHardware = true) => {
|
|
|
290
306
|
activeVideoConfig = selectedConfig;
|
|
291
307
|
const decoder = new VideoDecoder({
|
|
292
308
|
output: (frame) => {
|
|
309
|
+
socketDecodedFrames += 1;
|
|
293
310
|
const writer = videoWriter;
|
|
294
311
|
if (!writer || (writer.desiredSize ?? 1) <= 0) {
|
|
295
312
|
frame.close();
|
|
@@ -443,6 +460,7 @@ var useFallback = (reasonCode, failed = false) => {
|
|
|
443
460
|
if (failed) webrtcSkips = webrtcSkipsAfterFailure;
|
|
444
461
|
note("fallback to socket", reason);
|
|
445
462
|
transport = "websocket";
|
|
463
|
+
restartQuality();
|
|
446
464
|
transportReason = reason;
|
|
447
465
|
route = { ...route, path: "proxy", codec: config.videoCodec };
|
|
448
466
|
announceTransport();
|
|
@@ -624,11 +642,13 @@ var connect = () => {
|
|
|
624
642
|
candidateErrors = 0;
|
|
625
643
|
negotiationErrorName = "";
|
|
626
644
|
lastFrameAt = 0;
|
|
645
|
+
restartQuality();
|
|
627
646
|
route = { path: "", codec: "", decoded: "", kbps: 0, displayedFps: 0 };
|
|
628
647
|
announceTransport();
|
|
629
648
|
socket?.close();
|
|
630
649
|
showStatus(reconnectAttempt ? "reconnecting\u2026" : "connecting\u2026", true);
|
|
631
650
|
const query = new URLSearchParams([...params].filter(([key]) => ["transport", "profile", "adapt", "ice"].includes(key)));
|
|
651
|
+
if (standby) query.set("standby", "1");
|
|
632
652
|
const nextSocket = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/stream?${query}`);
|
|
633
653
|
nextSocket.binaryType = "arraybuffer";
|
|
634
654
|
socket = nextSocket;
|
|
@@ -685,6 +705,8 @@ var observeFrame = () => {
|
|
|
685
705
|
presentedFrames += lastPresentedCount ? Math.max(0, metadata.presentedFrames - lastPresentedCount) : 1;
|
|
686
706
|
lastPresentedCount = metadata.presentedFrames;
|
|
687
707
|
lastFrameAt = performance.now();
|
|
708
|
+
if (presentedAt && qualityWindow) qualityWindow.longestGapMs = Math.max(qualityWindow.longestGapMs, lastFrameAt - Math.max(presentedAt, qualityWindow.at));
|
|
709
|
+
presentedAt = lastFrameAt;
|
|
688
710
|
status.classList.remove("visible");
|
|
689
711
|
observeFrame();
|
|
690
712
|
});
|
|
@@ -730,6 +752,21 @@ window.addEventListener("keyup", (event) => {
|
|
|
730
752
|
}, { capture: true });
|
|
731
753
|
window.addEventListener("blur", () => send({ type: "release" }));
|
|
732
754
|
window.addEventListener("resize", fitScreen);
|
|
755
|
+
var setStandby = (next) => {
|
|
756
|
+
clearTimeout(standbyTimer);
|
|
757
|
+
if (next === standby) return;
|
|
758
|
+
standby = next;
|
|
759
|
+
note(standby ? "standby" : "active");
|
|
760
|
+
sendSocket({ type: "standby", standby });
|
|
761
|
+
if (!standby && !audioPrompt.hidden) unlockAudio();
|
|
762
|
+
};
|
|
763
|
+
new IntersectionObserver(([entry]) => {
|
|
764
|
+
if (entry.isIntersecting) setStandby(false);
|
|
765
|
+
else if (!standby) {
|
|
766
|
+
clearTimeout(standbyTimer);
|
|
767
|
+
standbyTimer = setTimeout(() => setStandby(true), 1e3);
|
|
768
|
+
}
|
|
769
|
+
}).observe(document.documentElement);
|
|
733
770
|
window.addEventListener("online", connect);
|
|
734
771
|
document.addEventListener("visibilitychange", () => {
|
|
735
772
|
if (document.visibilityState === "hidden") {
|
|
@@ -777,6 +814,53 @@ var updateDiagnostics = (stats) => {
|
|
|
777
814
|
viewerEvents.length ? `viewer events: ${describeEvents(viewerEvents.slice(-5))}` : ""
|
|
778
815
|
].filter(Boolean).join("\n");
|
|
779
816
|
};
|
|
817
|
+
var reportQuality = (stats) => {
|
|
818
|
+
if (window.parent === window || !transport || !config || !telemetry) return;
|
|
819
|
+
const now = performance.now();
|
|
820
|
+
const current = qualityWindow ??= { at: now, presented: presentedFrames, freezes: inbound?.freezeCount ?? 0, longestGapMs: 0 };
|
|
821
|
+
const decoded = transport === "webrtc" ? inbound?.framesDecoded ?? 0 : socketDecodedFrames;
|
|
822
|
+
qualityUnseen = { decoded, presented: presentedFrames, samples: decoded > qualityUnseen.decoded && presentedFrames === qualityUnseen.presented ? qualityUnseen.samples + 1 : 0 };
|
|
823
|
+
if (qualityUnseen.samples >= 4) current.unseen = true;
|
|
824
|
+
qualitySamples.push({ stats, telemetry });
|
|
825
|
+
const windowMs = now - current.at;
|
|
826
|
+
if (windowMs < qualityWindowMs) return;
|
|
827
|
+
const samples = qualitySamples;
|
|
828
|
+
const rungs = samples.map((sample) => sample.telemetry.encoder.rung);
|
|
829
|
+
qualitySamples = [];
|
|
830
|
+
qualityWindow = { at: now, presented: presentedFrames, freezes: inbound?.freezeCount ?? 0, longestGapMs: 0, rung: rungs.at(-1) };
|
|
831
|
+
if (current.unseen) return;
|
|
832
|
+
const mean = (values) => {
|
|
833
|
+
const known = values.filter((value) => value !== void 0);
|
|
834
|
+
return known.length ? Math.round(known.reduce((sum, value) => sum + value, 0) * 10 / known.length) / 10 : void 0;
|
|
835
|
+
};
|
|
836
|
+
const displayedFps = Math.round((presentedFrames - current.presented) * 1e4 / windowMs) / 10;
|
|
837
|
+
const decodedFps = transport === "webrtc" ? mean(samples.map((sample) => sample.stats.videoFps)) : void 0;
|
|
838
|
+
const rtts = samples.map((sample) => sample.stats.rttMs);
|
|
839
|
+
window.parent.postMessage({
|
|
840
|
+
type: DESKTOP_QUALITY_MESSAGE,
|
|
841
|
+
transport,
|
|
842
|
+
path: route.path,
|
|
843
|
+
codec: route.codec,
|
|
844
|
+
profile: telemetry.encoder.profile,
|
|
845
|
+
serverVersion: config.serverVersion ?? "unknown",
|
|
846
|
+
sequence: ++qualitySequence,
|
|
847
|
+
windowMs: Math.round(windowMs),
|
|
848
|
+
displayedFps,
|
|
849
|
+
encoderFps: mean(samples.map((sample) => sample.telemetry.encoder.fps)) ?? 0,
|
|
850
|
+
longestFrameGapMs: Math.round(Math.max(current.longestGapMs, now - Math.max(presentedAt, current.at))),
|
|
851
|
+
qualityRung: mean(rungs) ?? 0,
|
|
852
|
+
worstQualityRung: Math.max(...rungs),
|
|
853
|
+
qualityDrops: rungs.filter((rung, index) => rung > (index ? rungs[index - 1] : current.rung ?? rung)).length,
|
|
854
|
+
decodedHeight: screen.videoHeight,
|
|
855
|
+
inputRttMs: mean(rtts) ?? 0,
|
|
856
|
+
worstInputRttMs: Math.round(Math.max(...rtts)),
|
|
857
|
+
cpuLoadPercent: mean(samples.map((sample) => sample.telemetry.cpu.load * 100 / sample.telemetry.cpu.cores)) ?? 0,
|
|
858
|
+
decodedFps,
|
|
859
|
+
receiveToDecodeMs: mean(samples.map((sample) => sample.stats.receiverProcessingMs)),
|
|
860
|
+
lossPercent: mean(samples.map((sample) => sample.stats.lossPercent)),
|
|
861
|
+
freezes: inbound ? Math.max(0, inbound.freezeCount - current.freezes) : void 0
|
|
862
|
+
}, "*");
|
|
863
|
+
};
|
|
780
864
|
var inboundOf = (kind) => (stat) => stat.type === "inbound-rtp" && "kind" in stat && stat.kind === kind;
|
|
781
865
|
var reportWebRtcStats = async (pc, stats) => {
|
|
782
866
|
const entries = [...(await pc.getStats()).values()];
|
|
@@ -868,6 +952,7 @@ var reportWebRtcStats = async (pc, stats) => {
|
|
|
868
952
|
if (pair?.availableIncomingBitrate) stats.availableKbps = pair.availableIncomingBitrate / 1e3;
|
|
869
953
|
sendInput({ type: "stats", ...stats }, true);
|
|
870
954
|
updateDiagnostics(stats);
|
|
955
|
+
reportQuality(stats);
|
|
871
956
|
state.framesDropped = video?.framesDropped;
|
|
872
957
|
};
|
|
873
958
|
setInterval(() => {
|
|
@@ -915,6 +1000,7 @@ setInterval(() => {
|
|
|
915
1000
|
else if (transport === "websocket") {
|
|
916
1001
|
sendInput({ type: "stats", ...stats });
|
|
917
1002
|
updateDiagnostics(stats);
|
|
1003
|
+
reportQuality(stats);
|
|
918
1004
|
route.kbps = 0;
|
|
919
1005
|
}
|
|
920
1006
|
}, 250);
|