replicas-computer 0.3.3 → 0.3.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/README.md +2 -1
- package/dist/index.js +22 -7
- package/dist/stream-viewer.js +14 -16
- package/dist/viewer.js +10 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ The versioned computer-use runtime for Replicas workspace images.
|
|
|
10
10
|
- `src/pacer.ts` owns the per-viewer send budget: fresh media, NACK repairs and protective copies share one token bucket.
|
|
11
11
|
- `src/rtp.ts` owns RTP continuity, keyframe detection, and frame reassembly for the socket fallback.
|
|
12
12
|
- `src/stream-viewer.ts` owns the browser side: WebRTC playback, input routing, the WebSocket fallback, diagnostics and reconnects.
|
|
13
|
+
- `src/input-bridge.ts` converts takeover input into desktop events, accumulating small trackpad movements while preserving full mouse-wheel steps.
|
|
13
14
|
- `src/viewer.ts` owns the optional noVNC fallback.
|
|
14
15
|
- `assets/desktop/start-services.sh` owns the X11 and stream process lifecycle.
|
|
15
16
|
- `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.
|
|
@@ -21,7 +22,7 @@ The viewer and agent command paths are independent. One on-demand encoder pair i
|
|
|
21
22
|
|
|
22
23
|
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
24
|
|
|
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
|
|
25
|
+
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
26
|
|
|
26
27
|
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
28
|
|
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.5";
|
|
2941
2941
|
var COMPUTER_USE_PROTOCOL_VERSION = 1;
|
|
2942
2942
|
|
|
2943
2943
|
// src/program.ts
|
|
@@ -3556,6 +3556,7 @@ function superviseEncoder(options) {
|
|
|
3556
3556
|
|
|
3557
3557
|
// src/input-bridge.ts
|
|
3558
3558
|
import { spawn as spawn4 } from "child_process";
|
|
3559
|
+
var scrollPixelsPerStep = 80;
|
|
3559
3560
|
var namedKeys = {
|
|
3560
3561
|
AltLeft: "Alt_L",
|
|
3561
3562
|
AltRight: "Alt_R",
|
|
@@ -3615,6 +3616,8 @@ var InputBridge = class {
|
|
|
3615
3616
|
pointerFlush;
|
|
3616
3617
|
positionSequence = -1;
|
|
3617
3618
|
actionId = 0;
|
|
3619
|
+
scrollRemainder = 0;
|
|
3620
|
+
scrollButton = 0;
|
|
3618
3621
|
latestPosition;
|
|
3619
3622
|
pressedButtons = /* @__PURE__ */ new Set();
|
|
3620
3623
|
pressedKeys = /* @__PURE__ */ new Set();
|
|
@@ -3673,13 +3676,23 @@ var InputBridge = class {
|
|
|
3673
3676
|
if (message.type === "wheel") {
|
|
3674
3677
|
this.pointer = this.coordinates(message.x, message.y);
|
|
3675
3678
|
this.flushPointer(true);
|
|
3676
|
-
const vertical = Math.
|
|
3677
|
-
const
|
|
3678
|
-
if (
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3679
|
+
const vertical = Math.abs(message.deltaY) >= Math.abs(message.deltaX);
|
|
3680
|
+
const delta = vertical ? message.deltaY : message.deltaX;
|
|
3681
|
+
if (!delta) return;
|
|
3682
|
+
const button = vertical ? delta < 0 ? 4 : 5 : delta < 0 ? 6 : 7;
|
|
3683
|
+
if (button !== this.scrollButton) this.scrollRemainder = 0;
|
|
3684
|
+
this.scrollButton = button;
|
|
3685
|
+
const distance = Math.abs(delta);
|
|
3686
|
+
let steps;
|
|
3687
|
+
if (distance >= scrollPixelsPerStep) {
|
|
3688
|
+
steps = Math.min(4, Math.round(distance / scrollPixelsPerStep));
|
|
3689
|
+
this.scrollRemainder = 0;
|
|
3690
|
+
} else {
|
|
3691
|
+
this.scrollRemainder += distance;
|
|
3692
|
+
steps = Math.floor(this.scrollRemainder / scrollPixelsPerStep);
|
|
3693
|
+
this.scrollRemainder %= scrollPixelsPerStep;
|
|
3682
3694
|
}
|
|
3695
|
+
for (let index = 0; index < steps; index += 1) this.write(`mouseclick ${button}`);
|
|
3683
3696
|
return;
|
|
3684
3697
|
}
|
|
3685
3698
|
if (message.type === "key") {
|
|
@@ -3701,6 +3714,8 @@ var InputBridge = class {
|
|
|
3701
3714
|
if (this.pointerFlush) clearImmediate(this.pointerFlush);
|
|
3702
3715
|
this.pointerFlush = void 0;
|
|
3703
3716
|
this.pointer = void 0;
|
|
3717
|
+
this.scrollRemainder = 0;
|
|
3718
|
+
this.scrollButton = 0;
|
|
3704
3719
|
for (const button of this.pressedButtons) this.write(`mouseup ${button}`);
|
|
3705
3720
|
for (const key of this.pressedKeys) this.write(`keyup ${key}`);
|
|
3706
3721
|
this.pressedButtons.clear();
|
package/dist/stream-viewer.js
CHANGED
|
@@ -30,6 +30,12 @@ function validServerMessage(value) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// src/wheel.ts
|
|
34
|
+
function wheelDeltaPixels(event, pageHeight) {
|
|
35
|
+
const scale = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 32 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? pageHeight : 1;
|
|
36
|
+
return { deltaX: event.deltaX * scale, deltaY: event.deltaY * scale };
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
// ../shared/src/desktop-stream.ts
|
|
34
40
|
var DEFAULT_STREAM_REQUEST = {
|
|
35
41
|
transport: "webrtc",
|
|
@@ -506,7 +512,7 @@ var handleServerMessage = (message, viaSocket) => {
|
|
|
506
512
|
useFallback("recent WebRTC failure");
|
|
507
513
|
} else if (!transport) {
|
|
508
514
|
showStatus("Establishing connection\u2026");
|
|
509
|
-
webrtcDeadline ??= setTimeout(() => useFallback("WebRTC did not
|
|
515
|
+
webrtcDeadline ??= setTimeout(() => useFallback("WebRTC did not decode video in time", true), webrtcFrameTimeoutMs);
|
|
510
516
|
}
|
|
511
517
|
};
|
|
512
518
|
var handleServerText = (data, viaSocket) => {
|
|
@@ -578,11 +584,6 @@ var flushPointer = () => {
|
|
|
578
584
|
var frameCallback = 0;
|
|
579
585
|
var observeFrame = () => {
|
|
580
586
|
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
587
|
renderedFrames += 1;
|
|
587
588
|
presentedFrames += lastPresentedCount ? Math.max(0, metadata.presentedFrames - lastPresentedCount) : 1;
|
|
588
589
|
lastPresentedCount = metadata.presentedFrames;
|
|
@@ -616,7 +617,7 @@ screen.addEventListener("pointerup", (event) => {
|
|
|
616
617
|
screen.addEventListener("pointercancel", () => send({ type: "release" }));
|
|
617
618
|
screen.addEventListener("wheel", (event) => {
|
|
618
619
|
event.preventDefault();
|
|
619
|
-
send({ type: "wheel", ...normalizedPointer(event),
|
|
620
|
+
send({ type: "wheel", ...normalizedPointer(event), ...wheelDeltaPixels(event, screen.clientHeight) });
|
|
620
621
|
}, { passive: false });
|
|
621
622
|
screen.addEventListener("contextmenu", (event) => event.preventDefault());
|
|
622
623
|
window.addEventListener("keydown", (event) => {
|
|
@@ -711,6 +712,11 @@ var reportWebRtcStats = async (pc, stats) => {
|
|
|
711
712
|
jitterBufferEmittedCount: video?.jitterBufferEmittedCount ?? 0,
|
|
712
713
|
audioPackets: audio?.packetsReceived ?? 0
|
|
713
714
|
};
|
|
715
|
+
if (counters.framesDecoded > (inbound?.framesDecoded ?? 0)) {
|
|
716
|
+
lastFrameAt = now;
|
|
717
|
+
clearTimeout(webrtcDeadline);
|
|
718
|
+
webrtcDeadline = void 0;
|
|
719
|
+
}
|
|
714
720
|
if (!inbound) inboundHistory = [];
|
|
715
721
|
while (inboundHistory.length > 1 && inboundHistory[1].at <= now - 1e3) inboundHistory.shift();
|
|
716
722
|
const oldest = inboundHistory[0];
|
|
@@ -743,8 +749,6 @@ var reportWebRtcStats = async (pc, stats) => {
|
|
|
743
749
|
updateDiagnostics(stats);
|
|
744
750
|
state.framesDropped = video?.framesDropped;
|
|
745
751
|
};
|
|
746
|
-
var watchdogPlaybackFrames = 0;
|
|
747
|
-
var watchdogPlaybackAt = 0;
|
|
748
752
|
setInterval(() => {
|
|
749
753
|
if (transport !== "webrtc") return;
|
|
750
754
|
if (screen.paused && screen.srcObject instanceof MediaStream && screen.srcObject.getVideoTracks().some((track) => track.readyState === "live")) return;
|
|
@@ -753,14 +757,8 @@ setInterval(() => {
|
|
|
753
757
|
useFallback("WebRTC video stopped arriving", true);
|
|
754
758
|
return;
|
|
755
759
|
}
|
|
756
|
-
const quality = screen.getVideoPlaybackQuality();
|
|
757
|
-
const playbackFrames = quality.totalVideoFrames - quality.droppedVideoFrames;
|
|
758
|
-
if (playbackFrames !== watchdogPlaybackFrames) {
|
|
759
|
-
watchdogPlaybackFrames = playbackFrames;
|
|
760
|
-
watchdogPlaybackAt = now;
|
|
761
|
-
}
|
|
762
760
|
if (inputChannel?.readyState !== "open") return;
|
|
763
|
-
if (now -
|
|
761
|
+
if (now - lastFrameAt < 2500 || now - lastKeyframeRequestAt < 1e3) return;
|
|
764
762
|
lastKeyframeRequestAt = now;
|
|
765
763
|
note("keyframe requested", `no frame for ${Math.round(now - lastFrameAt)} ms`);
|
|
766
764
|
sendInput({ type: "keyframe" });
|
package/dist/viewer.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// src/viewer.ts
|
|
2
2
|
import RFB from "./core/rfb.js";
|
|
3
3
|
import { encodings } from "./core/encodings.js";
|
|
4
|
+
|
|
5
|
+
// src/wheel.ts
|
|
6
|
+
function wheelDeltaPixels(event, pageHeight) {
|
|
7
|
+
const scale = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 32 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? pageHeight : 1;
|
|
8
|
+
return { deltaX: event.deltaX * scale, deltaY: event.deltaY * scale };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/viewer.ts
|
|
4
12
|
var screen = document.querySelector("#screen");
|
|
5
13
|
var status = document.querySelector("#status");
|
|
6
14
|
var statusText = document.querySelector("#status-text");
|
|
@@ -145,9 +153,9 @@ var shiftCanvasForWheel = (event) => {
|
|
|
145
153
|
const canvas = screen.querySelector("canvas");
|
|
146
154
|
const context = canvas?.getContext("2d");
|
|
147
155
|
if (!canvas || !context || !canvas.width || !canvas.height) return;
|
|
148
|
-
const
|
|
156
|
+
const { deltaY } = wheelDeltaPixels(event, canvas.height);
|
|
149
157
|
const limit = Math.max(0, Math.min(optimisticScrollMax, canvas.height - 1));
|
|
150
|
-
const offset = Math.trunc(Math.max(-limit, Math.min(limit,
|
|
158
|
+
const offset = Math.trunc(Math.max(-limit, Math.min(limit, deltaY)));
|
|
151
159
|
if (offset > 0) {
|
|
152
160
|
context.drawImage(canvas, 0, offset, canvas.width, canvas.height - offset, 0, 0, canvas.width, canvas.height - offset);
|
|
153
161
|
context.drawImage(canvas, 0, canvas.height - offset - 1, canvas.width, 1, 0, canvas.height - offset, canvas.width, offset);
|