replicas-computer 0.3.4 → 0.3.6
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 -0
- package/dist/index.js +94 -39
- package/dist/stream-viewer.js +123 -44
- 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.
|
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.6";
|
|
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();
|
|
@@ -3740,6 +3755,62 @@ var InputBridge = class {
|
|
|
3740
3755
|
}
|
|
3741
3756
|
};
|
|
3742
3757
|
|
|
3758
|
+
// ../shared/src/desktop-stream.ts
|
|
3759
|
+
var DEFAULT_STREAM_REQUEST = {
|
|
3760
|
+
transport: "webrtc",
|
|
3761
|
+
profile: "balanced",
|
|
3762
|
+
adaptive: true,
|
|
3763
|
+
iceTransportPolicy: "all"
|
|
3764
|
+
};
|
|
3765
|
+
function parseStreamRequest(query, defaults = DEFAULT_STREAM_REQUEST) {
|
|
3766
|
+
const transport = query.get("transport");
|
|
3767
|
+
const profile = query.get("profile");
|
|
3768
|
+
const adapt = query.get("adapt");
|
|
3769
|
+
const ice = query.get("ice");
|
|
3770
|
+
return {
|
|
3771
|
+
transport: transport === "webrtc" || transport === "websocket" ? transport : defaults.transport,
|
|
3772
|
+
profile: profile === "text" || profile === "balanced" ? profile : defaults.profile,
|
|
3773
|
+
adaptive: adapt === "0" ? false : adapt === "1" ? true : defaults.adaptive,
|
|
3774
|
+
iceTransportPolicy: ice === "relay" || ice === "all" ? ice : defaults.iceTransportPolicy
|
|
3775
|
+
};
|
|
3776
|
+
}
|
|
3777
|
+
var H264_PROFILES = {
|
|
3778
|
+
high444: { payloadType: 99, profileLevelId: "f4001f", codec: "avc1.F4002A", label: "High 4:4:4" },
|
|
3779
|
+
high: { payloadType: 96, profileLevelId: "64001f", codec: "avc1.64002A", label: "High" },
|
|
3780
|
+
main: { payloadType: 97, profileLevelId: "4d001f", codec: "avc1.4D002A", label: "Main" },
|
|
3781
|
+
baseline: { payloadType: 98, profileLevelId: "42e01f", codec: "avc1.42E02A", label: "Baseline" }
|
|
3782
|
+
};
|
|
3783
|
+
var PROFILE_ORDER = ["high444", "high", "main", "baseline"];
|
|
3784
|
+
var DESKTOP_WEBRTC_FAILURE_REASONS = {
|
|
3785
|
+
server_unavailable: "server has no WebRTC",
|
|
3786
|
+
browser_unavailable: "browser has no WebRTC",
|
|
3787
|
+
connection_failed: "WebRTC connection failed",
|
|
3788
|
+
server_connection_failed: "Server WebRTC connection failed",
|
|
3789
|
+
codec_unsupported: "browser cannot receive H.264 over WebRTC",
|
|
3790
|
+
negotiation_failed: "negotiation failed",
|
|
3791
|
+
video_timeout: "WebRTC did not decode video in time",
|
|
3792
|
+
video_stalled: "WebRTC video stopped arriving"
|
|
3793
|
+
};
|
|
3794
|
+
function isDesktopWebRtcFailure(value) {
|
|
3795
|
+
if (!isRecord(value) || typeof value.reasonCode !== "string" || !Object.hasOwn(DESKTOP_WEBRTC_FAILURE_REASONS, value.reasonCode) || typeof value.turnConfigured !== "boolean") return false;
|
|
3796
|
+
return [
|
|
3797
|
+
"id",
|
|
3798
|
+
"connectionState",
|
|
3799
|
+
"iceConnectionState",
|
|
3800
|
+
"iceGatheringState",
|
|
3801
|
+
"signalingState",
|
|
3802
|
+
"iceTransportPolicy",
|
|
3803
|
+
"serverVersion",
|
|
3804
|
+
"serverUnavailableReason",
|
|
3805
|
+
"localCandidateTypes",
|
|
3806
|
+
"remoteCandidateTypes",
|
|
3807
|
+
"iceErrorCodes",
|
|
3808
|
+
"errorName",
|
|
3809
|
+
"path",
|
|
3810
|
+
"codec"
|
|
3811
|
+
].every((key) => typeof value[key] === "string" && value[key].length <= 200) && ["elapsedMs", "candidateErrors", "framesDecoded", "packetsReceived", "packetsLost", "bytesReceived", "lastFrameAgeMs", "encoderFps"].every((key) => typeof value[key] === "number" && Number.isFinite(value[key]));
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3743
3814
|
// src/input-validation.ts
|
|
3744
3815
|
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
3745
3816
|
var optionalFinite = (value) => value === void 0 || finite(value);
|
|
@@ -3765,7 +3836,7 @@ function validInput(value) {
|
|
|
3765
3836
|
case "candidate":
|
|
3766
3837
|
return typeof value.candidate === "string" && value.candidate.length < 1024 && typeof value.mid === "string" && value.mid.length < 16;
|
|
3767
3838
|
case "fallback":
|
|
3768
|
-
return typeof value.reason === "string" && value.reason.length < 200;
|
|
3839
|
+
return typeof value.reason === "string" && value.reason.length < 200 && (value.failure === void 0 || isDesktopWebRtcFailure(value.failure));
|
|
3769
3840
|
case "pointer":
|
|
3770
3841
|
return finite(value.x) && finite(value.y);
|
|
3771
3842
|
case "button":
|
|
@@ -3786,33 +3857,6 @@ function parseInput(raw) {
|
|
|
3786
3857
|
return validInput(value) ? value : void 0;
|
|
3787
3858
|
}
|
|
3788
3859
|
|
|
3789
|
-
// ../shared/src/desktop-stream.ts
|
|
3790
|
-
var DEFAULT_STREAM_REQUEST = {
|
|
3791
|
-
transport: "webrtc",
|
|
3792
|
-
profile: "balanced",
|
|
3793
|
-
adaptive: true,
|
|
3794
|
-
iceTransportPolicy: "all"
|
|
3795
|
-
};
|
|
3796
|
-
function parseStreamRequest(query, defaults = DEFAULT_STREAM_REQUEST) {
|
|
3797
|
-
const transport = query.get("transport");
|
|
3798
|
-
const profile = query.get("profile");
|
|
3799
|
-
const adapt = query.get("adapt");
|
|
3800
|
-
const ice = query.get("ice");
|
|
3801
|
-
return {
|
|
3802
|
-
transport: transport === "webrtc" || transport === "websocket" ? transport : defaults.transport,
|
|
3803
|
-
profile: profile === "text" || profile === "balanced" ? profile : defaults.profile,
|
|
3804
|
-
adaptive: adapt === "0" ? false : adapt === "1" ? true : defaults.adaptive,
|
|
3805
|
-
iceTransportPolicy: ice === "relay" || ice === "all" ? ice : defaults.iceTransportPolicy
|
|
3806
|
-
};
|
|
3807
|
-
}
|
|
3808
|
-
var H264_PROFILES = {
|
|
3809
|
-
high444: { payloadType: 99, profileLevelId: "f4001f", codec: "avc1.F4002A", label: "High 4:4:4" },
|
|
3810
|
-
high: { payloadType: 96, profileLevelId: "64001f", codec: "avc1.64002A", label: "High" },
|
|
3811
|
-
main: { payloadType: 97, profileLevelId: "4d001f", codec: "avc1.4D002A", label: "Main" },
|
|
3812
|
-
baseline: { payloadType: 98, profileLevelId: "42e01f", codec: "avc1.42E02A", label: "Baseline" }
|
|
3813
|
-
};
|
|
3814
|
-
var PROFILE_ORDER = ["high444", "high", "main", "baseline"];
|
|
3815
|
-
|
|
3816
3860
|
// src/media-protocol.ts
|
|
3817
3861
|
var STREAM_PROTOCOL_VERSION = 3;
|
|
3818
3862
|
var MEDIA_PACKET_HEADER_BYTES = 12;
|
|
@@ -4356,6 +4400,7 @@ async function runMediaServer() {
|
|
|
4356
4400
|
let videoBytes = 0;
|
|
4357
4401
|
let audioBytes = 0;
|
|
4358
4402
|
const events = [];
|
|
4403
|
+
const webrtcFailures = [];
|
|
4359
4404
|
const samples = [];
|
|
4360
4405
|
const record = (event, detail) => {
|
|
4361
4406
|
if (events.push({ at: Date.now(), event, detail }) > eventHistory) events.shift();
|
|
@@ -4889,7 +4934,14 @@ async function runMediaServer() {
|
|
|
4889
4934
|
record("keyframe", reason);
|
|
4890
4935
|
void videoEncoder.replace();
|
|
4891
4936
|
};
|
|
4892
|
-
const enterFallback = (viewer, reason) => {
|
|
4937
|
+
const enterFallback = (viewer, reason, failure) => {
|
|
4938
|
+
if (failure && !viewer.failureReported && viewer.request.transport === "webrtc") {
|
|
4939
|
+
viewer.failureReported = true;
|
|
4940
|
+
const entry = { ...failure, at: Date.now() };
|
|
4941
|
+
if (webrtcFailures.push(entry) > 50) webrtcFailures.shift();
|
|
4942
|
+
process.stderr.write(`[desktop-stream] webrtc fallback diagnostics: ${JSON.stringify(entry)}
|
|
4943
|
+
`);
|
|
4944
|
+
}
|
|
4893
4945
|
if (viewer.fallback) return;
|
|
4894
4946
|
viewer.fallback = true;
|
|
4895
4947
|
viewer.waitingForKeyframe = true;
|
|
@@ -4986,7 +5038,7 @@ async function runMediaServer() {
|
|
|
4986
5038
|
viewer.rtc?.candidate(message.candidate, message.mid);
|
|
4987
5039
|
return;
|
|
4988
5040
|
case "fallback":
|
|
4989
|
-
enterFallback(viewer, message.reason);
|
|
5041
|
+
enterFallback(viewer, message.reason, message.failure);
|
|
4990
5042
|
return;
|
|
4991
5043
|
case "keyframe":
|
|
4992
5044
|
if (!viewer.fallback) requestKeyframe("viewer watchdog", void 0, viewer);
|
|
@@ -5001,6 +5053,8 @@ async function runMediaServer() {
|
|
|
5001
5053
|
const config = {
|
|
5002
5054
|
type: "config",
|
|
5003
5055
|
protocolVersion: STREAM_PROTOCOL_VERSION,
|
|
5056
|
+
serverVersion: COMPUTER_USE_VERSION,
|
|
5057
|
+
...!webrtc ? { webrtcUnavailableReason: webrtcEnabled ? "native_module_unavailable" : "disabled" } : {},
|
|
5004
5058
|
inputRace: true,
|
|
5005
5059
|
width,
|
|
5006
5060
|
height,
|
|
@@ -5055,7 +5109,7 @@ async function runMediaServer() {
|
|
|
5055
5109
|
width,
|
|
5056
5110
|
height,
|
|
5057
5111
|
fps,
|
|
5058
|
-
...url.pathname === "/telemetry" ? { events, samples } : {}
|
|
5112
|
+
...url.pathname === "/telemetry" ? { events, samples, webrtcFailures } : {}
|
|
5059
5113
|
}));
|
|
5060
5114
|
return;
|
|
5061
5115
|
}
|
|
@@ -5153,9 +5207,10 @@ async function runMediaServer() {
|
|
|
5153
5207
|
});
|
|
5154
5208
|
} catch (error) {
|
|
5155
5209
|
record("webrtc error", `could not create a peer connection: ${error instanceof Error ? error.message : String(error)}`);
|
|
5210
|
+
reply({ type: "webrtc-failed" });
|
|
5156
5211
|
}
|
|
5157
5212
|
}
|
|
5158
|
-
if (!viewer.rtc) enterFallback(viewer,
|
|
5213
|
+
if (!viewer.rtc) enterFallback(viewer, request.transport === "webrtc" ? "webrtc unavailable" : "websocket requested");
|
|
5159
5214
|
ensureEncoders();
|
|
5160
5215
|
socket.on("message", (raw, binary) => {
|
|
5161
5216
|
if (binary) return;
|
package/dist/stream-viewer.js
CHANGED
|
@@ -3,33 +3,6 @@ function isRecord(value) {
|
|
|
3
3
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
// src/input-validation.ts
|
|
7
|
-
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
8
|
-
var iceServer = (value) => {
|
|
9
|
-
if (!isRecord(value)) return false;
|
|
10
|
-
return typeof value.urls === "string" && (value.username === void 0 || typeof value.username === "string") && (value.credential === void 0 || typeof value.credential === "string");
|
|
11
|
-
};
|
|
12
|
-
function validServerMessage(value) {
|
|
13
|
-
if (!isRecord(value) || typeof value.type !== "string") return false;
|
|
14
|
-
switch (value.type) {
|
|
15
|
-
case "ping":
|
|
16
|
-
case "pong":
|
|
17
|
-
return finite(value.sentAt);
|
|
18
|
-
case "webrtc-failed":
|
|
19
|
-
return true;
|
|
20
|
-
case "offer":
|
|
21
|
-
return typeof value.sdp === "string" && value.sdp.length < 65536;
|
|
22
|
-
case "candidate":
|
|
23
|
-
return typeof value.candidate === "string" && value.candidate.length < 1024 && typeof value.mid === "string" && value.mid.length < 16;
|
|
24
|
-
case "config":
|
|
25
|
-
return finite(value.protocolVersion) && (value.inputRace === void 0 || typeof value.inputRace === "boolean") && finite(value.width) && finite(value.height) && finite(value.fps) && typeof value.videoCodec === "string" && typeof value.audioCodec === "string" && finite(value.audioSampleRate) && finite(value.audioChannels) && isRecord(value.request) && (value.iceServers === void 0 || Array.isArray(value.iceServers) && value.iceServers.every(iceServer));
|
|
26
|
-
case "telemetry":
|
|
27
|
-
return isRecord(value.encoder) && isRecord(value.cpu) && isRecord(value.viewer) && Array.isArray(value.events);
|
|
28
|
-
default:
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
6
|
// ../shared/src/desktop-stream.ts
|
|
34
7
|
var DEFAULT_STREAM_REQUEST = {
|
|
35
8
|
transport: "webrtc",
|
|
@@ -61,6 +34,49 @@ function h264ProfileLabel(sdpFmtpLine) {
|
|
|
61
34
|
return match?.label ?? id ?? "";
|
|
62
35
|
}
|
|
63
36
|
var DESKTOP_TRANSPORT_MESSAGE = "replicas-desktop-transport";
|
|
37
|
+
var DESKTOP_WEBRTC_FAILURE_REASONS = {
|
|
38
|
+
server_unavailable: "server has no WebRTC",
|
|
39
|
+
browser_unavailable: "browser has no WebRTC",
|
|
40
|
+
connection_failed: "WebRTC connection failed",
|
|
41
|
+
server_connection_failed: "Server WebRTC connection failed",
|
|
42
|
+
codec_unsupported: "browser cannot receive H.264 over WebRTC",
|
|
43
|
+
negotiation_failed: "negotiation failed",
|
|
44
|
+
video_timeout: "WebRTC did not decode video in time",
|
|
45
|
+
video_stalled: "WebRTC video stopped arriving"
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/input-validation.ts
|
|
49
|
+
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
50
|
+
var iceServer = (value) => {
|
|
51
|
+
if (!isRecord(value)) return false;
|
|
52
|
+
return typeof value.urls === "string" && (value.username === void 0 || typeof value.username === "string") && (value.credential === void 0 || typeof value.credential === "string");
|
|
53
|
+
};
|
|
54
|
+
function validServerMessage(value) {
|
|
55
|
+
if (!isRecord(value) || typeof value.type !== "string") return false;
|
|
56
|
+
switch (value.type) {
|
|
57
|
+
case "ping":
|
|
58
|
+
case "pong":
|
|
59
|
+
return finite(value.sentAt);
|
|
60
|
+
case "webrtc-failed":
|
|
61
|
+
return true;
|
|
62
|
+
case "offer":
|
|
63
|
+
return typeof value.sdp === "string" && value.sdp.length < 65536;
|
|
64
|
+
case "candidate":
|
|
65
|
+
return typeof value.candidate === "string" && value.candidate.length < 1024 && typeof value.mid === "string" && value.mid.length < 16;
|
|
66
|
+
case "config":
|
|
67
|
+
return finite(value.protocolVersion) && (value.serverVersion === void 0 || typeof value.serverVersion === "string") && (value.webrtcUnavailableReason === void 0 || typeof value.webrtcUnavailableReason === "string") && (value.inputRace === void 0 || typeof value.inputRace === "boolean") && finite(value.width) && finite(value.height) && finite(value.fps) && typeof value.videoCodec === "string" && typeof value.audioCodec === "string" && finite(value.audioSampleRate) && finite(value.audioChannels) && isRecord(value.request) && (value.iceServers === void 0 || Array.isArray(value.iceServers) && value.iceServers.every(iceServer));
|
|
68
|
+
case "telemetry":
|
|
69
|
+
return isRecord(value.encoder) && isRecord(value.cpu) && isRecord(value.viewer) && Array.isArray(value.events);
|
|
70
|
+
default:
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/wheel.ts
|
|
76
|
+
function wheelDeltaPixels(event, pageHeight) {
|
|
77
|
+
const scale = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 32 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? pageHeight : 1;
|
|
78
|
+
return { deltaX: event.deltaX * scale, deltaY: event.deltaY * scale };
|
|
79
|
+
}
|
|
64
80
|
|
|
65
81
|
// src/media-protocol.ts
|
|
66
82
|
var STREAM_PROTOCOL_VERSION = 3;
|
|
@@ -132,6 +148,13 @@ globalThis.__replicasDesktop = state;
|
|
|
132
148
|
var viewerEvents = [];
|
|
133
149
|
state.events = viewerEvents;
|
|
134
150
|
var decoderResets = 0;
|
|
151
|
+
var connectionStartedAt = performance.now();
|
|
152
|
+
var failure;
|
|
153
|
+
var localCandidateTypes = /* @__PURE__ */ new Set();
|
|
154
|
+
var remoteCandidateTypes = /* @__PURE__ */ new Set();
|
|
155
|
+
var iceErrorCodes = /* @__PURE__ */ new Set();
|
|
156
|
+
var candidateErrors = 0;
|
|
157
|
+
var negotiationErrorName = "";
|
|
135
158
|
var note = (event, detail) => {
|
|
136
159
|
if (viewerEvents.push({ at: Date.now(), event, detail }) > 50) viewerEvents.shift();
|
|
137
160
|
};
|
|
@@ -202,7 +225,7 @@ var sendInput = (message, unreliable = false) => {
|
|
|
202
225
|
};
|
|
203
226
|
var announceTransport = () => {
|
|
204
227
|
if (window.parent === window) return;
|
|
205
|
-
window.parent.postMessage({ type: DESKTOP_TRANSPORT_MESSAGE, transport: transport ?? "connecting", reason: transportReason, path: route.path, codec: route.codec }, "*");
|
|
228
|
+
window.parent.postMessage({ type: DESKTOP_TRANSPORT_MESSAGE, transport: transport ?? "connecting", reason: transportReason, path: route.path, codec: route.codec, failure }, "*");
|
|
206
229
|
};
|
|
207
230
|
var closePeer = (preserveDeadline = false) => {
|
|
208
231
|
if (!preserveDeadline) {
|
|
@@ -373,8 +396,38 @@ var unlockAudio = () => {
|
|
|
373
396
|
});
|
|
374
397
|
});
|
|
375
398
|
};
|
|
376
|
-
var useFallback = (
|
|
399
|
+
var useFallback = (reasonCode, failed = false) => {
|
|
377
400
|
if (transport === "websocket" || !config) return;
|
|
401
|
+
const reason = reasonCode === "websocket_selected" ? "WebSocket transport selected" : reasonCode === "recent_failure" ? "recent WebRTC failure" : DESKTOP_WEBRTC_FAILURE_REASONS[reasonCode];
|
|
402
|
+
if (request.transport === "webrtc" && reasonCode !== "websocket_selected" && reasonCode !== "recent_failure") {
|
|
403
|
+
failure = {
|
|
404
|
+
id: crypto.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
405
|
+
reasonCode,
|
|
406
|
+
elapsedMs: Math.round(performance.now() - connectionStartedAt),
|
|
407
|
+
connectionState: peer?.connectionState ?? "unavailable",
|
|
408
|
+
iceConnectionState: peer?.iceConnectionState ?? "unavailable",
|
|
409
|
+
iceGatheringState: peer?.iceGatheringState ?? "unavailable",
|
|
410
|
+
signalingState: peer?.signalingState ?? "unavailable",
|
|
411
|
+
iceTransportPolicy: request.iceTransportPolicy,
|
|
412
|
+
turnConfigured: config.iceServers?.some((server) => /^turns?:/i.test(server.urls)) ?? false,
|
|
413
|
+
serverVersion: config.serverVersion ?? "unknown",
|
|
414
|
+
serverUnavailableReason: config.webrtcUnavailableReason ?? "",
|
|
415
|
+
localCandidateTypes: [...localCandidateTypes].join(","),
|
|
416
|
+
remoteCandidateTypes: [...remoteCandidateTypes].join(","),
|
|
417
|
+
iceErrorCodes: [...iceErrorCodes].join(","),
|
|
418
|
+
candidateErrors,
|
|
419
|
+
errorName: negotiationErrorName,
|
|
420
|
+
path: route.path,
|
|
421
|
+
codec: route.codec,
|
|
422
|
+
framesDecoded: inbound?.framesDecoded ?? 0,
|
|
423
|
+
packetsReceived: inbound?.packetsReceived ?? 0,
|
|
424
|
+
packetsLost: inbound?.packetsLost ?? 0,
|
|
425
|
+
bytesReceived: inbound?.bytesReceived ?? 0,
|
|
426
|
+
lastFrameAgeMs: lastFrameAt ? Math.round(performance.now() - lastFrameAt) : -1,
|
|
427
|
+
encoderFps: telemetry?.encoder.fps ?? -1
|
|
428
|
+
};
|
|
429
|
+
state.lastWebRtcFailure = failure;
|
|
430
|
+
}
|
|
378
431
|
closePeer();
|
|
379
432
|
if (failed) webrtcSkips = webrtcSkipsAfterFailure;
|
|
380
433
|
note("fallback to socket", reason);
|
|
@@ -382,11 +435,11 @@ var useFallback = (reason, failed = false) => {
|
|
|
382
435
|
transportReason = reason;
|
|
383
436
|
route = { ...route, path: "proxy", codec: config.videoCodec };
|
|
384
437
|
announceTransport();
|
|
438
|
+
sendSocket({ type: "fallback", reason, failure });
|
|
385
439
|
if (!TrackGenerator) {
|
|
386
440
|
showStatus("This browser cannot render the desktop stream");
|
|
387
441
|
return;
|
|
388
442
|
}
|
|
389
|
-
sendSocket({ type: "fallback", reason });
|
|
390
443
|
resetVideoTrack();
|
|
391
444
|
void configureVideo(config).catch((error) => showStatus(describe(error)));
|
|
392
445
|
void screen.play().catch(() => showStatus("Click the stream to begin playback"));
|
|
@@ -421,8 +474,14 @@ var negotiate = async (sdp) => {
|
|
|
421
474
|
};
|
|
422
475
|
pc.onicecandidate = (event) => {
|
|
423
476
|
if (peer !== pc) return;
|
|
477
|
+
if (event.candidate?.type) localCandidateTypes.add(event.candidate.type);
|
|
424
478
|
if (event.candidate?.candidate) sendSocket({ type: "candidate", candidate: event.candidate.candidate, mid: event.candidate.sdpMid ?? "0" });
|
|
425
479
|
};
|
|
480
|
+
pc.onicecandidateerror = (event) => {
|
|
481
|
+
if (peer !== pc) return;
|
|
482
|
+
iceErrorCodes.add(event.errorCode);
|
|
483
|
+
note("ice candidate error", String(event.errorCode));
|
|
484
|
+
};
|
|
426
485
|
pc.oniceconnectionstatechange = () => {
|
|
427
486
|
if (peer === pc) note("ice", pc.iceConnectionState);
|
|
428
487
|
};
|
|
@@ -438,7 +497,7 @@ var negotiate = async (sdp) => {
|
|
|
438
497
|
unlockAudio();
|
|
439
498
|
} else if (pc.connectionState === "failed") {
|
|
440
499
|
if (transport === "webrtc") socket?.close();
|
|
441
|
-
else useFallback("
|
|
500
|
+
else useFallback("connection_failed", true);
|
|
442
501
|
}
|
|
443
502
|
};
|
|
444
503
|
try {
|
|
@@ -450,7 +509,7 @@ var negotiate = async (sdp) => {
|
|
|
450
509
|
if (peer !== pc || !answer.sdp) return;
|
|
451
510
|
const video = answer.sdp.split(/^m=/m).find((section) => section.startsWith("video")) ?? "";
|
|
452
511
|
if (video.startsWith("video 0 ") || !/^a=rtpmap:\d+ H264\/90000/m.test(video)) {
|
|
453
|
-
useFallback("
|
|
512
|
+
useFallback("codec_unsupported", true);
|
|
454
513
|
return;
|
|
455
514
|
}
|
|
456
515
|
sendSocket({ type: "answer", sdp: answer.sdp });
|
|
@@ -478,15 +537,22 @@ var handleServerMessage = (message, viaSocket) => {
|
|
|
478
537
|
return;
|
|
479
538
|
}
|
|
480
539
|
if (message.type === "webrtc-failed") {
|
|
481
|
-
useFallback("
|
|
540
|
+
useFallback("server_connection_failed", true);
|
|
482
541
|
return;
|
|
483
542
|
}
|
|
484
543
|
if (message.type === "offer") {
|
|
485
|
-
void negotiate(message.sdp).catch((error) =>
|
|
544
|
+
void negotiate(message.sdp).catch((error) => {
|
|
545
|
+
negotiationErrorName = error instanceof Error ? error.name : "unknown";
|
|
546
|
+
useFallback("negotiation_failed", true);
|
|
547
|
+
});
|
|
486
548
|
return;
|
|
487
549
|
}
|
|
488
550
|
if (message.type === "candidate") {
|
|
489
|
-
|
|
551
|
+
const candidateType = message.candidate.match(/\btyp (host|srflx|prflx|relay)\b/)?.[1];
|
|
552
|
+
if (candidateType) remoteCandidateTypes.add(candidateType);
|
|
553
|
+
const pc = peer;
|
|
554
|
+
void pc?.addIceCandidate({ candidate: message.candidate, sdpMid: message.mid }).catch(() => {
|
|
555
|
+
if (peer === pc) candidateErrors += 1;
|
|
490
556
|
});
|
|
491
557
|
return;
|
|
492
558
|
}
|
|
@@ -498,15 +564,15 @@ var handleServerMessage = (message, viaSocket) => {
|
|
|
498
564
|
config = message;
|
|
499
565
|
request = message.request;
|
|
500
566
|
fitScreen();
|
|
501
|
-
if (request.transport !== "webrtc") useFallback("
|
|
502
|
-
else if (!message.iceServers) useFallback("
|
|
503
|
-
else if (typeof RTCPeerConnection !== "function") useFallback("
|
|
567
|
+
if (request.transport !== "webrtc") useFallback("websocket_selected");
|
|
568
|
+
else if (!message.iceServers) useFallback("server_unavailable");
|
|
569
|
+
else if (typeof RTCPeerConnection !== "function") useFallback("browser_unavailable");
|
|
504
570
|
else if (webrtcSkips > 0) {
|
|
505
571
|
webrtcSkips -= 1;
|
|
506
|
-
useFallback("
|
|
572
|
+
useFallback("recent_failure");
|
|
507
573
|
} else if (!transport) {
|
|
508
574
|
showStatus("Establishing connection\u2026");
|
|
509
|
-
webrtcDeadline ??= setTimeout(() => useFallback("
|
|
575
|
+
webrtcDeadline ??= setTimeout(() => useFallback("video_timeout", true), webrtcFrameTimeoutMs);
|
|
510
576
|
}
|
|
511
577
|
};
|
|
512
578
|
var handleServerText = (data, viaSocket) => {
|
|
@@ -523,6 +589,19 @@ var connect = () => {
|
|
|
523
589
|
clearTimeout(reconnectTimer);
|
|
524
590
|
closePeer();
|
|
525
591
|
transport = void 0;
|
|
592
|
+
transportReason = "";
|
|
593
|
+
failure = void 0;
|
|
594
|
+
config = void 0;
|
|
595
|
+
telemetry = void 0;
|
|
596
|
+
connectionStartedAt = performance.now();
|
|
597
|
+
localCandidateTypes.clear();
|
|
598
|
+
remoteCandidateTypes.clear();
|
|
599
|
+
iceErrorCodes.clear();
|
|
600
|
+
candidateErrors = 0;
|
|
601
|
+
negotiationErrorName = "";
|
|
602
|
+
lastFrameAt = 0;
|
|
603
|
+
route = { path: "", codec: "", decoded: "", kbps: 0, displayedFps: 0 };
|
|
604
|
+
announceTransport();
|
|
526
605
|
socket?.close();
|
|
527
606
|
showStatus(reconnectAttempt ? "reconnecting\u2026" : "connecting\u2026");
|
|
528
607
|
const query = new URLSearchParams([...params].filter(([key]) => ["transport", "profile", "adapt", "ice"].includes(key)));
|
|
@@ -611,7 +690,7 @@ screen.addEventListener("pointerup", (event) => {
|
|
|
611
690
|
screen.addEventListener("pointercancel", () => send({ type: "release" }));
|
|
612
691
|
screen.addEventListener("wheel", (event) => {
|
|
613
692
|
event.preventDefault();
|
|
614
|
-
send({ type: "wheel", ...normalizedPointer(event),
|
|
693
|
+
send({ type: "wheel", ...normalizedPointer(event), ...wheelDeltaPixels(event, screen.clientHeight) });
|
|
615
694
|
}, { passive: false });
|
|
616
695
|
screen.addEventListener("contextmenu", (event) => event.preventDefault());
|
|
617
696
|
window.addEventListener("keydown", (event) => {
|
|
@@ -748,7 +827,7 @@ setInterval(() => {
|
|
|
748
827
|
if (screen.paused && screen.srcObject instanceof MediaStream && screen.srcObject.getVideoTracks().some((track) => track.readyState === "live")) return;
|
|
749
828
|
const now = performance.now();
|
|
750
829
|
if (now - lastFrameAt >= webrtcFrameTimeoutMs) {
|
|
751
|
-
useFallback("
|
|
830
|
+
useFallback("video_stalled", true);
|
|
752
831
|
return;
|
|
753
832
|
}
|
|
754
833
|
if (inputChannel?.readyState !== "open") return;
|
|
@@ -772,7 +851,7 @@ setInterval(() => {
|
|
|
772
851
|
displayedWindow = { at: now, frames: presentedFrames };
|
|
773
852
|
}
|
|
774
853
|
const elapsedSeconds = (now - renderedWindowStartedAt) / 1e3;
|
|
775
|
-
if (transport !== "webrtc" && elapsedSeconds < 1) return;
|
|
854
|
+
if (!peer && transport !== "webrtc" && elapsedSeconds < 1) return;
|
|
776
855
|
const stats = {
|
|
777
856
|
sequence: ++statsSequence,
|
|
778
857
|
videoFps: renderedFrames / elapsedSeconds,
|
|
@@ -783,7 +862,7 @@ setInterval(() => {
|
|
|
783
862
|
renderedFrames = 0;
|
|
784
863
|
decodedAudioPackets = 0;
|
|
785
864
|
renderedWindowStartedAt = now;
|
|
786
|
-
if (
|
|
865
|
+
if (peer) void reportWebRtcStats(peer, stats).catch(() => {
|
|
787
866
|
});
|
|
788
867
|
else if (transport === "websocket") {
|
|
789
868
|
sendInput({ type: "stats", ...stats });
|
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);
|