replicas-computer 0.3.12 → 0.3.13
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 +1 -1
- package/dist/stream-viewer.js +68 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
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;
|
|
@@ -151,6 +153,18 @@ var inboundAt = 0;
|
|
|
151
153
|
var inboundHistory = [];
|
|
152
154
|
var telemetry;
|
|
153
155
|
var route = { path: "", codec: "", decoded: "", kbps: 0, displayedFps: 0 };
|
|
156
|
+
var qualitySamples = [];
|
|
157
|
+
var qualityWindow;
|
|
158
|
+
var qualityUnseen = { decoded: 0, presented: 0, samples: 0 };
|
|
159
|
+
var qualitySequence = 0;
|
|
160
|
+
var presentedAt = 0;
|
|
161
|
+
var socketDecodedFrames = 0;
|
|
162
|
+
var restartQuality = () => {
|
|
163
|
+
qualitySamples = [];
|
|
164
|
+
qualityWindow = void 0;
|
|
165
|
+
qualitySequence = 0;
|
|
166
|
+
presentedAt = 0;
|
|
167
|
+
};
|
|
154
168
|
var state = {};
|
|
155
169
|
globalThis.__replicasDesktop = state;
|
|
156
170
|
var viewerEvents = [];
|
|
@@ -290,6 +304,7 @@ var configureVideo = async (next, preferHardware = true) => {
|
|
|
290
304
|
activeVideoConfig = selectedConfig;
|
|
291
305
|
const decoder = new VideoDecoder({
|
|
292
306
|
output: (frame) => {
|
|
307
|
+
socketDecodedFrames += 1;
|
|
293
308
|
const writer = videoWriter;
|
|
294
309
|
if (!writer || (writer.desiredSize ?? 1) <= 0) {
|
|
295
310
|
frame.close();
|
|
@@ -443,6 +458,7 @@ var useFallback = (reasonCode, failed = false) => {
|
|
|
443
458
|
if (failed) webrtcSkips = webrtcSkipsAfterFailure;
|
|
444
459
|
note("fallback to socket", reason);
|
|
445
460
|
transport = "websocket";
|
|
461
|
+
restartQuality();
|
|
446
462
|
transportReason = reason;
|
|
447
463
|
route = { ...route, path: "proxy", codec: config.videoCodec };
|
|
448
464
|
announceTransport();
|
|
@@ -624,6 +640,7 @@ var connect = () => {
|
|
|
624
640
|
candidateErrors = 0;
|
|
625
641
|
negotiationErrorName = "";
|
|
626
642
|
lastFrameAt = 0;
|
|
643
|
+
restartQuality();
|
|
627
644
|
route = { path: "", codec: "", decoded: "", kbps: 0, displayedFps: 0 };
|
|
628
645
|
announceTransport();
|
|
629
646
|
socket?.close();
|
|
@@ -685,6 +702,8 @@ var observeFrame = () => {
|
|
|
685
702
|
presentedFrames += lastPresentedCount ? Math.max(0, metadata.presentedFrames - lastPresentedCount) : 1;
|
|
686
703
|
lastPresentedCount = metadata.presentedFrames;
|
|
687
704
|
lastFrameAt = performance.now();
|
|
705
|
+
if (presentedAt && qualityWindow) qualityWindow.longestGapMs = Math.max(qualityWindow.longestGapMs, lastFrameAt - Math.max(presentedAt, qualityWindow.at));
|
|
706
|
+
presentedAt = lastFrameAt;
|
|
688
707
|
status.classList.remove("visible");
|
|
689
708
|
observeFrame();
|
|
690
709
|
});
|
|
@@ -777,6 +796,53 @@ var updateDiagnostics = (stats) => {
|
|
|
777
796
|
viewerEvents.length ? `viewer events: ${describeEvents(viewerEvents.slice(-5))}` : ""
|
|
778
797
|
].filter(Boolean).join("\n");
|
|
779
798
|
};
|
|
799
|
+
var reportQuality = (stats) => {
|
|
800
|
+
if (window.parent === window || !transport || !config || !telemetry) return;
|
|
801
|
+
const now = performance.now();
|
|
802
|
+
const current = qualityWindow ??= { at: now, presented: presentedFrames, freezes: inbound?.freezeCount ?? 0, longestGapMs: 0 };
|
|
803
|
+
const decoded = transport === "webrtc" ? inbound?.framesDecoded ?? 0 : socketDecodedFrames;
|
|
804
|
+
qualityUnseen = { decoded, presented: presentedFrames, samples: decoded > qualityUnseen.decoded && presentedFrames === qualityUnseen.presented ? qualityUnseen.samples + 1 : 0 };
|
|
805
|
+
if (qualityUnseen.samples >= 4) current.unseen = true;
|
|
806
|
+
qualitySamples.push({ stats, telemetry });
|
|
807
|
+
const windowMs = now - current.at;
|
|
808
|
+
if (windowMs < qualityWindowMs) return;
|
|
809
|
+
const samples = qualitySamples;
|
|
810
|
+
const rungs = samples.map((sample) => sample.telemetry.encoder.rung);
|
|
811
|
+
qualitySamples = [];
|
|
812
|
+
qualityWindow = { at: now, presented: presentedFrames, freezes: inbound?.freezeCount ?? 0, longestGapMs: 0, rung: rungs.at(-1) };
|
|
813
|
+
if (current.unseen) return;
|
|
814
|
+
const mean = (values) => {
|
|
815
|
+
const known = values.filter((value) => value !== void 0);
|
|
816
|
+
return known.length ? Math.round(known.reduce((sum, value) => sum + value, 0) * 10 / known.length) / 10 : void 0;
|
|
817
|
+
};
|
|
818
|
+
const displayedFps = Math.round((presentedFrames - current.presented) * 1e4 / windowMs) / 10;
|
|
819
|
+
const decodedFps = transport === "webrtc" ? mean(samples.map((sample) => sample.stats.videoFps)) : void 0;
|
|
820
|
+
const rtts = samples.map((sample) => sample.stats.rttMs);
|
|
821
|
+
window.parent.postMessage({
|
|
822
|
+
type: DESKTOP_QUALITY_MESSAGE,
|
|
823
|
+
transport,
|
|
824
|
+
path: route.path,
|
|
825
|
+
codec: route.codec,
|
|
826
|
+
profile: telemetry.encoder.profile,
|
|
827
|
+
serverVersion: config.serverVersion ?? "unknown",
|
|
828
|
+
sequence: ++qualitySequence,
|
|
829
|
+
windowMs: Math.round(windowMs),
|
|
830
|
+
displayedFps,
|
|
831
|
+
encoderFps: mean(samples.map((sample) => sample.telemetry.encoder.fps)) ?? 0,
|
|
832
|
+
longestFrameGapMs: Math.round(Math.max(current.longestGapMs, now - Math.max(presentedAt, current.at))),
|
|
833
|
+
qualityRung: mean(rungs) ?? 0,
|
|
834
|
+
worstQualityRung: Math.max(...rungs),
|
|
835
|
+
qualityDrops: rungs.filter((rung, index) => rung > (index ? rungs[index - 1] : current.rung ?? rung)).length,
|
|
836
|
+
decodedHeight: screen.videoHeight,
|
|
837
|
+
inputRttMs: mean(rtts) ?? 0,
|
|
838
|
+
worstInputRttMs: Math.round(Math.max(...rtts)),
|
|
839
|
+
cpuLoadPercent: mean(samples.map((sample) => sample.telemetry.cpu.load * 100 / sample.telemetry.cpu.cores)) ?? 0,
|
|
840
|
+
decodedFps,
|
|
841
|
+
receiveToDecodeMs: mean(samples.map((sample) => sample.stats.receiverProcessingMs)),
|
|
842
|
+
lossPercent: mean(samples.map((sample) => sample.stats.lossPercent)),
|
|
843
|
+
freezes: inbound ? Math.max(0, inbound.freezeCount - current.freezes) : void 0
|
|
844
|
+
}, "*");
|
|
845
|
+
};
|
|
780
846
|
var inboundOf = (kind) => (stat) => stat.type === "inbound-rtp" && "kind" in stat && stat.kind === kind;
|
|
781
847
|
var reportWebRtcStats = async (pc, stats) => {
|
|
782
848
|
const entries = [...(await pc.getStats()).values()];
|
|
@@ -868,6 +934,7 @@ var reportWebRtcStats = async (pc, stats) => {
|
|
|
868
934
|
if (pair?.availableIncomingBitrate) stats.availableKbps = pair.availableIncomingBitrate / 1e3;
|
|
869
935
|
sendInput({ type: "stats", ...stats }, true);
|
|
870
936
|
updateDiagnostics(stats);
|
|
937
|
+
reportQuality(stats);
|
|
871
938
|
state.framesDropped = video?.framesDropped;
|
|
872
939
|
};
|
|
873
940
|
setInterval(() => {
|
|
@@ -915,6 +982,7 @@ setInterval(() => {
|
|
|
915
982
|
else if (transport === "websocket") {
|
|
916
983
|
sendInput({ type: "stats", ...stats });
|
|
917
984
|
updateDiagnostics(stats);
|
|
985
|
+
reportQuality(stats);
|
|
918
986
|
route.kbps = 0;
|
|
919
987
|
}
|
|
920
988
|
}, 250);
|