replicas-computer 0.3.3 → 0.3.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/README.md +1 -1
- package/dist/index.js +1 -1
- package/dist/stream-viewer.js +7 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ The viewer and agent command paths are independent. One on-demand encoder pair i
|
|
|
21
21
|
|
|
22
22
|
ffmpeg captures the display and encodes 1920×1080 H.264 (and Opus from PulseAudio) as RTP onto loopback. RGB capture is converted with the BT.709 matrix in limited range and the same matrix is signalled in the bitstream, so browsers reconstruct the colours the desktop drew (`REPLICAS_DESKTOP_STREAM_COLOR=legacy` restores the untagged BT.601 conversion). Frames are forwarded unchanged apart from a sequence/timestamp splice and a playout-delay header extension.
|
|
23
23
|
|
|
24
|
-
WebRTC is the default transport, with automatic WebSocket fallback. A viewer can choose `?transport=websocket`; `REPLICAS_DESKTOP_STREAM_TRANSPORT=websocket` changes the server default for viewers that do not choose. Dashboard URLs carry the selected transport explicitly, and saved transport preferences are preserved. The viewer's WebSocket to `/stream` is the control plane either way: it carries the server's SDP offer, the viewer's answer, trickled ICE candidates, keepalives and telemetry. Pointer moves ride an unreliable data channel. When the server advertises `inputRace`, clicks, keys, wheel events and held-input release race a direct copy against ordered WebSocket delivery. The input bridge accepts only the next action ID, ignoring duplicates and out-of-order direct copies; the socket fills gaps without SCTP retransmission delays or a reorder queue. Older peers keep their existing input path. An explicit server-side WebRTC failure, a network that cannot pass UDP, or six seconds without
|
|
24
|
+
WebRTC is the default transport, with automatic WebSocket fallback. A viewer can choose `?transport=websocket`; `REPLICAS_DESKTOP_STREAM_TRANSPORT=websocket` changes the server default for viewers that do not choose. Dashboard URLs carry the selected transport explicitly, and saved transport preferences are preserved. The viewer's WebSocket to `/stream` is the control plane either way: it carries the server's SDP offer, the viewer's answer, trickled ICE candidates, keepalives and telemetry. Pointer moves ride an unreliable data channel. When the server advertises `inputRace`, clicks, keys, wheel events and held-input release race a direct copy against ordered WebSocket delivery. The input bridge accepts only the next action ID, ignoring duplicates and out-of-order direct copies; the socket fills gaps without SCTP retransmission delays or a reorder queue. Older peers keep their existing input path. The watchdog tracks decoded frames so CSS-hidden viewers do not fall back merely because the browser stops presenting video. An explicit server-side WebRTC failure, a network that cannot pass UDP, or six seconds without decoded video switches the viewer to the socket, where the server reassembles frames and WebCodecs decodes them in the browser.
|
|
25
25
|
|
|
26
26
|
The server makes the offer, so payload types and SSRCs are fixed and ffmpeg emits them directly. Four H.264 payload types are offered (High 4:4:4 Predictive, High, Main, Constrained Baseline); the encoder runs the best profile every connected viewer accepts, so one viewer without 4:4:4 support moves everyone to 4:2:0, and socket viewers or caps below 8 Mbps do the same. Two profiles select the encoder tuning: `balanced` (CRF 14, default chroma quantisation) and `text` (CRF 10, chroma QP offset −6); `?profile=` picks one and the most recent viewer's choice applies to the shared encoder. Quality adapts on a six-rung bitrate ladder that keeps resolution and frame rate and adds two CRF per rung under the 20 Mbps cap; `?adapt=0` pins the top rung.
|
|
27
27
|
|
package/dist/index.js
CHANGED
package/dist/stream-viewer.js
CHANGED
|
@@ -506,7 +506,7 @@ var handleServerMessage = (message, viaSocket) => {
|
|
|
506
506
|
useFallback("recent WebRTC failure");
|
|
507
507
|
} else if (!transport) {
|
|
508
508
|
showStatus("Establishing connection\u2026");
|
|
509
|
-
webrtcDeadline ??= setTimeout(() => useFallback("WebRTC did not
|
|
509
|
+
webrtcDeadline ??= setTimeout(() => useFallback("WebRTC did not decode video in time", true), webrtcFrameTimeoutMs);
|
|
510
510
|
}
|
|
511
511
|
};
|
|
512
512
|
var handleServerText = (data, viaSocket) => {
|
|
@@ -578,11 +578,6 @@ var flushPointer = () => {
|
|
|
578
578
|
var frameCallback = 0;
|
|
579
579
|
var observeFrame = () => {
|
|
580
580
|
frameCallback = screen.requestVideoFrameCallback((_now, metadata) => {
|
|
581
|
-
const stream = screen.srcObject;
|
|
582
|
-
if (webrtcDeadline && transport === "webrtc" && stream instanceof MediaStream && peer?.getReceivers().some(({ track }) => stream.getVideoTracks().includes(track))) {
|
|
583
|
-
clearTimeout(webrtcDeadline);
|
|
584
|
-
webrtcDeadline = void 0;
|
|
585
|
-
}
|
|
586
581
|
renderedFrames += 1;
|
|
587
582
|
presentedFrames += lastPresentedCount ? Math.max(0, metadata.presentedFrames - lastPresentedCount) : 1;
|
|
588
583
|
lastPresentedCount = metadata.presentedFrames;
|
|
@@ -711,6 +706,11 @@ var reportWebRtcStats = async (pc, stats) => {
|
|
|
711
706
|
jitterBufferEmittedCount: video?.jitterBufferEmittedCount ?? 0,
|
|
712
707
|
audioPackets: audio?.packetsReceived ?? 0
|
|
713
708
|
};
|
|
709
|
+
if (counters.framesDecoded > (inbound?.framesDecoded ?? 0)) {
|
|
710
|
+
lastFrameAt = now;
|
|
711
|
+
clearTimeout(webrtcDeadline);
|
|
712
|
+
webrtcDeadline = void 0;
|
|
713
|
+
}
|
|
714
714
|
if (!inbound) inboundHistory = [];
|
|
715
715
|
while (inboundHistory.length > 1 && inboundHistory[1].at <= now - 1e3) inboundHistory.shift();
|
|
716
716
|
const oldest = inboundHistory[0];
|
|
@@ -743,8 +743,6 @@ var reportWebRtcStats = async (pc, stats) => {
|
|
|
743
743
|
updateDiagnostics(stats);
|
|
744
744
|
state.framesDropped = video?.framesDropped;
|
|
745
745
|
};
|
|
746
|
-
var watchdogPlaybackFrames = 0;
|
|
747
|
-
var watchdogPlaybackAt = 0;
|
|
748
746
|
setInterval(() => {
|
|
749
747
|
if (transport !== "webrtc") return;
|
|
750
748
|
if (screen.paused && screen.srcObject instanceof MediaStream && screen.srcObject.getVideoTracks().some((track) => track.readyState === "live")) return;
|
|
@@ -753,14 +751,8 @@ setInterval(() => {
|
|
|
753
751
|
useFallback("WebRTC video stopped arriving", true);
|
|
754
752
|
return;
|
|
755
753
|
}
|
|
756
|
-
const quality = screen.getVideoPlaybackQuality();
|
|
757
|
-
const playbackFrames = quality.totalVideoFrames - quality.droppedVideoFrames;
|
|
758
|
-
if (playbackFrames !== watchdogPlaybackFrames) {
|
|
759
|
-
watchdogPlaybackFrames = playbackFrames;
|
|
760
|
-
watchdogPlaybackAt = now;
|
|
761
|
-
}
|
|
762
754
|
if (inputChannel?.readyState !== "open") return;
|
|
763
|
-
if (now -
|
|
755
|
+
if (now - lastFrameAt < 2500 || now - lastKeyframeRequestAt < 1e3) return;
|
|
764
756
|
lastKeyframeRequestAt = now;
|
|
765
757
|
note("keyframe requested", `no frame for ${Math.round(now - lastFrameAt)} ms`);
|
|
766
758
|
sendInput({ type: "keyframe" });
|