replicas-computer 0.3.6 → 0.3.7
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 +3 -3
- package/dist/index.js +14 -3
- package/dist/stream-viewer.js +1 -1
- package/package.json +3 -2
- package/patches/libjuice-ice-role-presence.patch +96 -0
- package/scripts/build-webrtc.sh +1 -0
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ ffmpeg captures the display and encodes 1920×1080 H.264 (and Opus from PulseAud
|
|
|
24
24
|
|
|
25
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.
|
|
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
|
|
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 bitrate ladder down to 750 kbps that keeps resolution and frame rate and adds two CRF per rung under the default 20 Mbps cap; `?adapt=0` pins the top rung.
|
|
28
28
|
|
|
29
29
|
Every video byte for a WebRTC viewer passes through `PacedSender`. libdatachannel's own pacer and NACK responder are not used because repairs bypassed the pacer and neither accounted for duplicates, so recovery traffic could exceed the link while fresh frames queued for seconds. The budget is 1.25× the rung's encoder cap and covers media, repairs and protective copies together: repairs take at most 30% of it over a rolling second, copies are sent only from idle budget while loss is observed and never exceed the original media bytes, and a queue that grows past 150 ms is cut back to whole frames a decoder can still follow (unsent delta frames go, the in-flight frame completes, a keyframe is requested and further deltas are dropped until it arrives). Sequence numbers are assigned as packets leave, so dropped frames never read as loss. Receiver PLI, NACK and REMB are parsed from RTCP. RTT growth (the lowest probe of the last second against the session's baseline, so a single late probe is not a queue), loss, jitter-buffer growth and stale-queue cuts drive the ladder down, and clean seconds drive it up. REMB is not a congestion vote: the receiver's estimate is bounded by what recently arrived and climbs by tens of kbps per second, so it trails every burst of motion on a quiet screen for a minute or more, and its delay detector reads the sender's pacing bursts on a quiet screen as overuse. It only bounds how far a severe descent (heavy loss or a starved decoder) steps, and only while it has stopped climbing below the sent rate. `scripts/probe-viewer.ts` drives the viewer page in a Chrome with remote debugging and prints what it measures and what the server decides each second; `scripts/probe-rtt-jitter.ts` feeds the controller a synthetic WebRTC viewer with jittery RTT probes.
|
|
30
30
|
|
|
@@ -39,7 +39,7 @@ The server records every decision that changes the picture (viewer connects and
|
|
|
39
39
|
| `REPLICAS_DESKTOP_STREAM_TRANSPORT` | `webrtc` | Transport for viewers whose URL does not choose one |
|
|
40
40
|
| `REPLICAS_DESKTOP_STREAM_PROFILE` | `balanced` | Encoder profile for viewers whose URL does not choose one |
|
|
41
41
|
| `REPLICAS_DESKTOP_STREAM_ADAPTIVE` | `1` | `0` pins the top quality rung by default |
|
|
42
|
-
| `REPLICAS_DESKTOP_STREAM_ICE_SERVERS` | `stun:stun.l.google.com:19302` | Comma-separated STUN/TURN servers; `turn:user:secret@host:port?transport=tcp` adds a relay. Re-read from the workspace runtime env on every viewer, so the monolith's `DESKTOP_STREAM_ICE_SERVERS` reaches running workspaces |
|
|
42
|
+
| `REPLICAS_DESKTOP_STREAM_ICE_SERVERS` | `stun:stun.l.google.com:19302` | Comma-separated STUN/TURN servers; `turn:user:secret@host:port?transport=tcp` adds a relay. Percent-encode the username and password; the browser receives their decoded values. Re-read from the workspace runtime env on every viewer, so the monolith's `DESKTOP_STREAM_ICE_SERVERS` reaches running workspaces |
|
|
43
43
|
| `REPLICAS_DESKTOP_STREAM_ICE_PORTS` | ephemeral | `begin-end` UDP port range for the server's ICE candidates |
|
|
44
44
|
| `REPLICAS_DESKTOP_STREAM_WEBRTC` | `1` | `0` serves the WebSocket transport only |
|
|
45
45
|
| `REPLICAS_DESKTOP_STREAM_COLOR` | `bt709` | `legacy` disables the explicit BT.709 conversion and signalling |
|
|
@@ -55,4 +55,4 @@ replicas-computer daemon
|
|
|
55
55
|
|
|
56
56
|
`setup --check` reports missing OS dependencies, including the native WebRTC module, without changing the machine. `setup` installs package-owned assets and the `replicas-computer.service` systemd unit. Operational commands use the daemon over a user-only Unix socket.
|
|
57
57
|
|
|
58
|
-
The sandbox image builds node-datachannel from pinned libdatachannel source with `scripts/build-webrtc.sh`, which applies `patches/libdatachannel-dtls-bio.patch` (upstream DTLS BIO synchronisation fix, [2059d2a](https://github.com/paullouisageneau/libdatachannel/commit/2059d2a3bb81dc3dc24686fdc92c90b6167ec4fd)) and `patches/libdatachannel-nack-duplicates.patch`. It needs CMake, Ninja, a C++ compiler, Git and OpenSSL development libraries. Remove each patch when the pinned addon includes its fix.
|
|
58
|
+
The sandbox image builds node-datachannel from pinned libdatachannel source with `scripts/build-webrtc.sh`, which applies `patches/libdatachannel-dtls-bio.patch` (upstream DTLS BIO synchronisation fix, [2059d2a](https://github.com/paullouisageneau/libdatachannel/commit/2059d2a3bb81dc3dc24686fdc92c90b6167ec4fd)) and `patches/libdatachannel-nack-duplicates.patch`. It also applies `patches/libjuice-ice-role-presence.patch` so valid zero-valued ICE role attributes do not break Chrome TURN/TCP connectivity. It needs CMake, Ninja, a C++ compiler, Git and OpenSSL development libraries. Remove each patch when the pinned addon includes its fix.
|
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.7";
|
|
2941
2941
|
var COMPUTER_USE_PROTOCOL_VERSION = 1;
|
|
2942
2942
|
|
|
2943
2943
|
// src/program.ts
|
|
@@ -4126,7 +4126,15 @@ function parseIceServers(spec) {
|
|
|
4126
4126
|
const native = (spec ?? "stun:stun.l.google.com:19302").split(",").map((server) => server.trim()).filter(Boolean);
|
|
4127
4127
|
const browser = native.map((url) => {
|
|
4128
4128
|
const match = url.match(/^(turns?):([^:@]+):(.+)@([^@]+)$/);
|
|
4129
|
-
|
|
4129
|
+
if (!match) return { urls: url };
|
|
4130
|
+
const [username, credential] = match.slice(2, 4).map((value) => {
|
|
4131
|
+
try {
|
|
4132
|
+
return decodeURIComponent(value);
|
|
4133
|
+
} catch {
|
|
4134
|
+
return value;
|
|
4135
|
+
}
|
|
4136
|
+
});
|
|
4137
|
+
return { urls: `${match[1]}:${match[4]}`, username, credential };
|
|
4130
4138
|
});
|
|
4131
4139
|
return { native, browser };
|
|
4132
4140
|
}
|
|
@@ -4284,7 +4292,7 @@ var profiles = {
|
|
|
4284
4292
|
balanced: { crf: 14, chromaQpOffset: 0 },
|
|
4285
4293
|
text: { crf: 10, chromaQpOffset: -6 }
|
|
4286
4294
|
};
|
|
4287
|
-
var rungCaps = [configuredTopKbps, 12e3, 8e3, 5e3, 3e3, 1500].map((kbps) => Math.min(kbps, configuredTopKbps));
|
|
4295
|
+
var rungCaps = [configuredTopKbps, 12e3, 8e3, 5e3, 3e3, 1500, 750].map((kbps) => Math.min(kbps, configuredTopKbps));
|
|
4288
4296
|
var qualityLadder = (profile) => rungCaps.map((kbps, index) => ({ kbps, crf: profiles[profile].crf + 2 * index, keyframeMs: index >= 4 ? 2e3 : 1e3 }));
|
|
4289
4297
|
var fullChromaMinKbps = 8e3;
|
|
4290
4298
|
var fallbackKeyframeMs = 250;
|
|
@@ -4527,6 +4535,7 @@ async function runMediaServer() {
|
|
|
4527
4535
|
colorMode === "bt709" ? `scale=out_color_matrix=bt709:out_range=tv,format=${pixelFormat}` : `format=${pixelFormat}`
|
|
4528
4536
|
];
|
|
4529
4537
|
const x264 = [
|
|
4538
|
+
`fps=${fps}/1`,
|
|
4530
4539
|
"repeat-headers=1",
|
|
4531
4540
|
"sliced-threads=1",
|
|
4532
4541
|
"slices=4",
|
|
@@ -4564,6 +4573,8 @@ async function runMediaServer() {
|
|
|
4564
4573
|
"1",
|
|
4565
4574
|
"-i",
|
|
4566
4575
|
`${display}.0`,
|
|
4576
|
+
"-filter_threads",
|
|
4577
|
+
"1",
|
|
4567
4578
|
"-vf",
|
|
4568
4579
|
filters.join(","),
|
|
4569
4580
|
...warmupFrames ? ["-force_key_frames", `expr:eq(n,${warmupFrames})`, "-forced-idr", "1"] : [],
|
package/dist/stream-viewer.js
CHANGED
|
@@ -362,7 +362,7 @@ var handleBinary = (data) => {
|
|
|
362
362
|
}
|
|
363
363
|
if (!videoDecoder || !activeVideoConfig || videoDecoder.state !== "configured") return;
|
|
364
364
|
const key = kind === 1 /* VideoKey */;
|
|
365
|
-
if (videoDecoder.decodeQueueSize >
|
|
365
|
+
if (videoDecoder.decodeQueueSize > 12) {
|
|
366
366
|
videoDecoder.reset();
|
|
367
367
|
videoDecoder.configure(activeVideoConfig);
|
|
368
368
|
waitingForKeyframe = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "replicas-computer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Replicas workspace computer-use runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"assets",
|
|
13
13
|
"scripts/build-webrtc.sh",
|
|
14
14
|
"patches/libdatachannel-dtls-bio.patch",
|
|
15
|
-
"patches/libdatachannel-nack-duplicates.patch"
|
|
15
|
+
"patches/libdatachannel-nack-duplicates.patch",
|
|
16
|
+
"patches/libjuice-ice-role-presence.patch"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
18
19
|
"build": "bun run typecheck && tsup",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
diff --git a/src/agent.c b/src/agent.c
|
|
2
|
+
--- a/src/agent.c
|
|
3
|
+
+++ b/src/agent.c
|
|
4
|
+
@@ -1437,7 +1437,7 @@
|
|
5
|
+
return -1;
|
|
6
|
+
|
|
7
|
+
ice_candidate_pair_t *pair = entry->pair;
|
|
8
|
+
- if (msg->ice_controlling == msg->ice_controlled) {
|
|
9
|
+
+ if (msg->has_ice_controlling == msg->has_ice_controlled) {
|
|
10
|
+
JLOG_WARN("Controlling and controlled attributes mismatch in request");
|
|
11
|
+
agent_send_stun_binding(agent, entry, STUN_CLASS_RESP_ERROR, 400, msg->transaction_id,
|
|
12
|
+
NULL);
|
|
13
|
+
@@ -1451,7 +1451,7 @@
|
|
14
|
+
// ERROR-CODE attribute with a value of 487 (Role Conflict) but retains its role.
|
|
15
|
+
// * If the agent's tiebreaker value is less than the contents of the ICE-CONTROLLING
|
|
16
|
+
// attribute, the agent switches to the controlled role.
|
|
17
|
+
- if (agent->mode == AGENT_MODE_CONTROLLING && msg->ice_controlling) {
|
|
18
|
+
+ if (agent->mode == AGENT_MODE_CONTROLLING && msg->has_ice_controlling) {
|
|
19
|
+
JLOG_WARN("ICE role conflict (both controlling)");
|
|
20
|
+
if (agent->ice_tiebreaker >= msg->ice_controlling) {
|
|
21
|
+
JLOG_DEBUG("Asking remote peer to switch roles");
|
|
22
|
+
@@ -1471,7 +1471,7 @@
|
|
23
|
+
// * If the agent's tiebreaker value is less than the contents of the ICE-CONTROLLED
|
|
24
|
+
// attribute, the agent generates a Binding error response and includes an ERROR-CODE
|
|
25
|
+
// attribute with a value of 487 (Role Conflict) but retains its role.
|
|
26
|
+
- if (agent->mode == AGENT_MODE_CONTROLLED && msg->ice_controlled) {
|
|
27
|
+
+ if (agent->mode == AGENT_MODE_CONTROLLED && msg->has_ice_controlled) {
|
|
28
|
+
JLOG_WARN("ICE role conflict (both controlled)");
|
|
29
|
+
if (agent->ice_tiebreaker >= msg->ice_controlling) {
|
|
30
|
+
JLOG_DEBUG("Switching to controlling role");
|
|
31
|
+
@@ -1485,7 +1485,7 @@
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
if (msg->use_candidate) {
|
|
35
|
+
- if (!msg->ice_controlling) {
|
|
36
|
+
+ if (!msg->has_ice_controlling) {
|
|
37
|
+
JLOG_WARN("STUN message use_candidate missing ice_controlling attribute");
|
|
38
|
+
agent_send_stun_binding(agent, entry, STUN_CLASS_RESP_ERROR, 400,
|
|
39
|
+
msg->transaction_id, NULL);
|
|
40
|
+
@@ -1715,6 +1715,8 @@
|
|
41
|
+
password = agent->remote.ice_pwd;
|
|
42
|
+
msg.ice_controlling = agent->mode == AGENT_MODE_CONTROLLING ? agent->ice_tiebreaker : 0;
|
|
43
|
+
msg.ice_controlled = agent->mode == AGENT_MODE_CONTROLLED ? agent->ice_tiebreaker : 0;
|
|
44
|
+
+ msg.has_ice_controlling = agent->mode == AGENT_MODE_CONTROLLING;
|
|
45
|
+
+ msg.has_ice_controlled = agent->mode == AGENT_MODE_CONTROLLED;
|
|
46
|
+
|
|
47
|
+
// RFC 8445 7.1.1. PRIORITY
|
|
48
|
+
// The PRIORITY attribute MUST be included in a Binding request and be set to the value
|
|
49
|
+
diff --git a/src/stun.c b/src/stun.c
|
|
50
|
+
--- a/src/stun.c
|
|
51
|
+
+++ b/src/stun.c
|
|
52
|
+
@@ -157,14 +157,14 @@
|
|
53
|
+
goto overflow;
|
|
54
|
+
pos += len;
|
|
55
|
+
}
|
|
56
|
+
- if (msg->ice_controlling) {
|
|
57
|
+
+ if (msg->has_ice_controlling || msg->ice_controlling) {
|
|
58
|
+
uint64_t ice_controlling = htonll(msg->ice_controlling);
|
|
59
|
+
len = stun_write_attr(pos, end - pos, STUN_ATTR_ICE_CONTROLLING, &ice_controlling, 8);
|
|
60
|
+
if (len <= 0)
|
|
61
|
+
goto overflow;
|
|
62
|
+
pos += len;
|
|
63
|
+
}
|
|
64
|
+
- if (msg->ice_controlled) {
|
|
65
|
+
+ if (msg->has_ice_controlled || msg->ice_controlled) {
|
|
66
|
+
uint64_t ice_controlled = htonll(msg->ice_controlled);
|
|
67
|
+
len = stun_write_attr(pos, end - pos, STUN_ATTR_ICE_CONTROLLED, &ice_controlled, 8);
|
|
68
|
+
if (len <= 0)
|
|
69
|
+
@@ -916,6 +916,7 @@
|
|
70
|
+
}
|
|
71
|
+
uint32_t *value32 = (uint32_t *)attr->value;
|
|
72
|
+
msg->ice_controlling = ((uint64_t)ntohl(value32[0]) << 32) | ntohl(value32[1]);
|
|
73
|
+
+ msg->has_ice_controlling = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case STUN_ATTR_ICE_CONTROLLED: {
|
|
77
|
+
@@ -926,6 +927,7 @@
|
|
78
|
+
}
|
|
79
|
+
uint32_t *value32 = (uint32_t *)attr->value;
|
|
80
|
+
msg->ice_controlled = ((uint64_t)ntohl(value32[0]) << 32) | ntohl(value32[1]);
|
|
81
|
+
+ msg->has_ice_controlled = true;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case STUN_ATTR_CHANNEL_NUMBER: {
|
|
85
|
+
diff --git a/src/stun.h b/src/stun.h
|
|
86
|
+
--- a/src/stun.h
|
|
87
|
+
+++ b/src/stun.h
|
|
88
|
+
@@ -319,6 +319,8 @@
|
|
89
|
+
uint32_t priority;
|
|
90
|
+
uint64_t ice_controlling;
|
|
91
|
+
uint64_t ice_controlled;
|
|
92
|
+
+ bool has_ice_controlling;
|
|
93
|
+
+ bool has_ice_controlled;
|
|
94
|
+
bool use_candidate;
|
|
95
|
+
addr_record_t mapped;
|
|
96
|
+
|
package/scripts/build-webrtc.sh
CHANGED
|
@@ -19,6 +19,7 @@ git -C "$source_dir" checkout -q --detach FETCH_HEAD
|
|
|
19
19
|
git -C "$source_dir" submodule update --init --recursive --depth 1
|
|
20
20
|
git -C "$source_dir" apply "$script_dir/../patches/libdatachannel-dtls-bio.patch"
|
|
21
21
|
git -C "$source_dir" apply "$script_dir/../patches/libdatachannel-nack-duplicates.patch"
|
|
22
|
+
git -C "$source_dir/deps/libjuice" apply "$script_dir/../patches/libjuice-ice-role-presence.patch"
|
|
22
23
|
|
|
23
24
|
cmake -S "$build_root" -B "$build_root/build" -G Ninja \
|
|
24
25
|
-DCMAKE_BUILD_TYPE=Release \
|