replicas-computer 0.3.5 → 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/dist/index.js +73 -33
- package/dist/stream-viewer.js +122 -49
- package/package.json +1 -1
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
|
|
@@ -3755,6 +3755,62 @@ var InputBridge = class {
|
|
|
3755
3755
|
}
|
|
3756
3756
|
};
|
|
3757
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
|
+
|
|
3758
3814
|
// src/input-validation.ts
|
|
3759
3815
|
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
3760
3816
|
var optionalFinite = (value) => value === void 0 || finite(value);
|
|
@@ -3780,7 +3836,7 @@ function validInput(value) {
|
|
|
3780
3836
|
case "candidate":
|
|
3781
3837
|
return typeof value.candidate === "string" && value.candidate.length < 1024 && typeof value.mid === "string" && value.mid.length < 16;
|
|
3782
3838
|
case "fallback":
|
|
3783
|
-
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));
|
|
3784
3840
|
case "pointer":
|
|
3785
3841
|
return finite(value.x) && finite(value.y);
|
|
3786
3842
|
case "button":
|
|
@@ -3801,33 +3857,6 @@ function parseInput(raw) {
|
|
|
3801
3857
|
return validInput(value) ? value : void 0;
|
|
3802
3858
|
}
|
|
3803
3859
|
|
|
3804
|
-
// ../shared/src/desktop-stream.ts
|
|
3805
|
-
var DEFAULT_STREAM_REQUEST = {
|
|
3806
|
-
transport: "webrtc",
|
|
3807
|
-
profile: "balanced",
|
|
3808
|
-
adaptive: true,
|
|
3809
|
-
iceTransportPolicy: "all"
|
|
3810
|
-
};
|
|
3811
|
-
function parseStreamRequest(query, defaults = DEFAULT_STREAM_REQUEST) {
|
|
3812
|
-
const transport = query.get("transport");
|
|
3813
|
-
const profile = query.get("profile");
|
|
3814
|
-
const adapt = query.get("adapt");
|
|
3815
|
-
const ice = query.get("ice");
|
|
3816
|
-
return {
|
|
3817
|
-
transport: transport === "webrtc" || transport === "websocket" ? transport : defaults.transport,
|
|
3818
|
-
profile: profile === "text" || profile === "balanced" ? profile : defaults.profile,
|
|
3819
|
-
adaptive: adapt === "0" ? false : adapt === "1" ? true : defaults.adaptive,
|
|
3820
|
-
iceTransportPolicy: ice === "relay" || ice === "all" ? ice : defaults.iceTransportPolicy
|
|
3821
|
-
};
|
|
3822
|
-
}
|
|
3823
|
-
var H264_PROFILES = {
|
|
3824
|
-
high444: { payloadType: 99, profileLevelId: "f4001f", codec: "avc1.F4002A", label: "High 4:4:4" },
|
|
3825
|
-
high: { payloadType: 96, profileLevelId: "64001f", codec: "avc1.64002A", label: "High" },
|
|
3826
|
-
main: { payloadType: 97, profileLevelId: "4d001f", codec: "avc1.4D002A", label: "Main" },
|
|
3827
|
-
baseline: { payloadType: 98, profileLevelId: "42e01f", codec: "avc1.42E02A", label: "Baseline" }
|
|
3828
|
-
};
|
|
3829
|
-
var PROFILE_ORDER = ["high444", "high", "main", "baseline"];
|
|
3830
|
-
|
|
3831
3860
|
// src/media-protocol.ts
|
|
3832
3861
|
var STREAM_PROTOCOL_VERSION = 3;
|
|
3833
3862
|
var MEDIA_PACKET_HEADER_BYTES = 12;
|
|
@@ -4371,6 +4400,7 @@ async function runMediaServer() {
|
|
|
4371
4400
|
let videoBytes = 0;
|
|
4372
4401
|
let audioBytes = 0;
|
|
4373
4402
|
const events = [];
|
|
4403
|
+
const webrtcFailures = [];
|
|
4374
4404
|
const samples = [];
|
|
4375
4405
|
const record = (event, detail) => {
|
|
4376
4406
|
if (events.push({ at: Date.now(), event, detail }) > eventHistory) events.shift();
|
|
@@ -4904,7 +4934,14 @@ async function runMediaServer() {
|
|
|
4904
4934
|
record("keyframe", reason);
|
|
4905
4935
|
void videoEncoder.replace();
|
|
4906
4936
|
};
|
|
4907
|
-
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
|
+
}
|
|
4908
4945
|
if (viewer.fallback) return;
|
|
4909
4946
|
viewer.fallback = true;
|
|
4910
4947
|
viewer.waitingForKeyframe = true;
|
|
@@ -5001,7 +5038,7 @@ async function runMediaServer() {
|
|
|
5001
5038
|
viewer.rtc?.candidate(message.candidate, message.mid);
|
|
5002
5039
|
return;
|
|
5003
5040
|
case "fallback":
|
|
5004
|
-
enterFallback(viewer, message.reason);
|
|
5041
|
+
enterFallback(viewer, message.reason, message.failure);
|
|
5005
5042
|
return;
|
|
5006
5043
|
case "keyframe":
|
|
5007
5044
|
if (!viewer.fallback) requestKeyframe("viewer watchdog", void 0, viewer);
|
|
@@ -5016,6 +5053,8 @@ async function runMediaServer() {
|
|
|
5016
5053
|
const config = {
|
|
5017
5054
|
type: "config",
|
|
5018
5055
|
protocolVersion: STREAM_PROTOCOL_VERSION,
|
|
5056
|
+
serverVersion: COMPUTER_USE_VERSION,
|
|
5057
|
+
...!webrtc ? { webrtcUnavailableReason: webrtcEnabled ? "native_module_unavailable" : "disabled" } : {},
|
|
5019
5058
|
inputRace: true,
|
|
5020
5059
|
width,
|
|
5021
5060
|
height,
|
|
@@ -5070,7 +5109,7 @@ async function runMediaServer() {
|
|
|
5070
5109
|
width,
|
|
5071
5110
|
height,
|
|
5072
5111
|
fps,
|
|
5073
|
-
...url.pathname === "/telemetry" ? { events, samples } : {}
|
|
5112
|
+
...url.pathname === "/telemetry" ? { events, samples, webrtcFailures } : {}
|
|
5074
5113
|
}));
|
|
5075
5114
|
return;
|
|
5076
5115
|
}
|
|
@@ -5168,9 +5207,10 @@ async function runMediaServer() {
|
|
|
5168
5207
|
});
|
|
5169
5208
|
} catch (error) {
|
|
5170
5209
|
record("webrtc error", `could not create a peer connection: ${error instanceof Error ? error.message : String(error)}`);
|
|
5210
|
+
reply({ type: "webrtc-failed" });
|
|
5171
5211
|
}
|
|
5172
5212
|
}
|
|
5173
|
-
if (!viewer.rtc) enterFallback(viewer,
|
|
5213
|
+
if (!viewer.rtc) enterFallback(viewer, request.transport === "webrtc" ? "webrtc unavailable" : "websocket requested");
|
|
5174
5214
|
ensureEncoders();
|
|
5175
5215
|
socket.on("message", (raw, binary) => {
|
|
5176
5216
|
if (binary) return;
|
package/dist/stream-viewer.js
CHANGED
|
@@ -3,39 +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
|
-
// 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
|
-
|
|
39
6
|
// ../shared/src/desktop-stream.ts
|
|
40
7
|
var DEFAULT_STREAM_REQUEST = {
|
|
41
8
|
transport: "webrtc",
|
|
@@ -67,6 +34,49 @@ function h264ProfileLabel(sdpFmtpLine) {
|
|
|
67
34
|
return match?.label ?? id ?? "";
|
|
68
35
|
}
|
|
69
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
|
+
}
|
|
70
80
|
|
|
71
81
|
// src/media-protocol.ts
|
|
72
82
|
var STREAM_PROTOCOL_VERSION = 3;
|
|
@@ -138,6 +148,13 @@ globalThis.__replicasDesktop = state;
|
|
|
138
148
|
var viewerEvents = [];
|
|
139
149
|
state.events = viewerEvents;
|
|
140
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 = "";
|
|
141
158
|
var note = (event, detail) => {
|
|
142
159
|
if (viewerEvents.push({ at: Date.now(), event, detail }) > 50) viewerEvents.shift();
|
|
143
160
|
};
|
|
@@ -208,7 +225,7 @@ var sendInput = (message, unreliable = false) => {
|
|
|
208
225
|
};
|
|
209
226
|
var announceTransport = () => {
|
|
210
227
|
if (window.parent === window) return;
|
|
211
|
-
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 }, "*");
|
|
212
229
|
};
|
|
213
230
|
var closePeer = (preserveDeadline = false) => {
|
|
214
231
|
if (!preserveDeadline) {
|
|
@@ -379,8 +396,38 @@ var unlockAudio = () => {
|
|
|
379
396
|
});
|
|
380
397
|
});
|
|
381
398
|
};
|
|
382
|
-
var useFallback = (
|
|
399
|
+
var useFallback = (reasonCode, failed = false) => {
|
|
383
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
|
+
}
|
|
384
431
|
closePeer();
|
|
385
432
|
if (failed) webrtcSkips = webrtcSkipsAfterFailure;
|
|
386
433
|
note("fallback to socket", reason);
|
|
@@ -388,11 +435,11 @@ var useFallback = (reason, failed = false) => {
|
|
|
388
435
|
transportReason = reason;
|
|
389
436
|
route = { ...route, path: "proxy", codec: config.videoCodec };
|
|
390
437
|
announceTransport();
|
|
438
|
+
sendSocket({ type: "fallback", reason, failure });
|
|
391
439
|
if (!TrackGenerator) {
|
|
392
440
|
showStatus("This browser cannot render the desktop stream");
|
|
393
441
|
return;
|
|
394
442
|
}
|
|
395
|
-
sendSocket({ type: "fallback", reason });
|
|
396
443
|
resetVideoTrack();
|
|
397
444
|
void configureVideo(config).catch((error) => showStatus(describe(error)));
|
|
398
445
|
void screen.play().catch(() => showStatus("Click the stream to begin playback"));
|
|
@@ -427,8 +474,14 @@ var negotiate = async (sdp) => {
|
|
|
427
474
|
};
|
|
428
475
|
pc.onicecandidate = (event) => {
|
|
429
476
|
if (peer !== pc) return;
|
|
477
|
+
if (event.candidate?.type) localCandidateTypes.add(event.candidate.type);
|
|
430
478
|
if (event.candidate?.candidate) sendSocket({ type: "candidate", candidate: event.candidate.candidate, mid: event.candidate.sdpMid ?? "0" });
|
|
431
479
|
};
|
|
480
|
+
pc.onicecandidateerror = (event) => {
|
|
481
|
+
if (peer !== pc) return;
|
|
482
|
+
iceErrorCodes.add(event.errorCode);
|
|
483
|
+
note("ice candidate error", String(event.errorCode));
|
|
484
|
+
};
|
|
432
485
|
pc.oniceconnectionstatechange = () => {
|
|
433
486
|
if (peer === pc) note("ice", pc.iceConnectionState);
|
|
434
487
|
};
|
|
@@ -444,7 +497,7 @@ var negotiate = async (sdp) => {
|
|
|
444
497
|
unlockAudio();
|
|
445
498
|
} else if (pc.connectionState === "failed") {
|
|
446
499
|
if (transport === "webrtc") socket?.close();
|
|
447
|
-
else useFallback("
|
|
500
|
+
else useFallback("connection_failed", true);
|
|
448
501
|
}
|
|
449
502
|
};
|
|
450
503
|
try {
|
|
@@ -456,7 +509,7 @@ var negotiate = async (sdp) => {
|
|
|
456
509
|
if (peer !== pc || !answer.sdp) return;
|
|
457
510
|
const video = answer.sdp.split(/^m=/m).find((section) => section.startsWith("video")) ?? "";
|
|
458
511
|
if (video.startsWith("video 0 ") || !/^a=rtpmap:\d+ H264\/90000/m.test(video)) {
|
|
459
|
-
useFallback("
|
|
512
|
+
useFallback("codec_unsupported", true);
|
|
460
513
|
return;
|
|
461
514
|
}
|
|
462
515
|
sendSocket({ type: "answer", sdp: answer.sdp });
|
|
@@ -484,15 +537,22 @@ var handleServerMessage = (message, viaSocket) => {
|
|
|
484
537
|
return;
|
|
485
538
|
}
|
|
486
539
|
if (message.type === "webrtc-failed") {
|
|
487
|
-
useFallback("
|
|
540
|
+
useFallback("server_connection_failed", true);
|
|
488
541
|
return;
|
|
489
542
|
}
|
|
490
543
|
if (message.type === "offer") {
|
|
491
|
-
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
|
+
});
|
|
492
548
|
return;
|
|
493
549
|
}
|
|
494
550
|
if (message.type === "candidate") {
|
|
495
|
-
|
|
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;
|
|
496
556
|
});
|
|
497
557
|
return;
|
|
498
558
|
}
|
|
@@ -504,15 +564,15 @@ var handleServerMessage = (message, viaSocket) => {
|
|
|
504
564
|
config = message;
|
|
505
565
|
request = message.request;
|
|
506
566
|
fitScreen();
|
|
507
|
-
if (request.transport !== "webrtc") useFallback("
|
|
508
|
-
else if (!message.iceServers) useFallback("
|
|
509
|
-
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");
|
|
510
570
|
else if (webrtcSkips > 0) {
|
|
511
571
|
webrtcSkips -= 1;
|
|
512
|
-
useFallback("
|
|
572
|
+
useFallback("recent_failure");
|
|
513
573
|
} else if (!transport) {
|
|
514
574
|
showStatus("Establishing connection\u2026");
|
|
515
|
-
webrtcDeadline ??= setTimeout(() => useFallback("
|
|
575
|
+
webrtcDeadline ??= setTimeout(() => useFallback("video_timeout", true), webrtcFrameTimeoutMs);
|
|
516
576
|
}
|
|
517
577
|
};
|
|
518
578
|
var handleServerText = (data, viaSocket) => {
|
|
@@ -529,6 +589,19 @@ var connect = () => {
|
|
|
529
589
|
clearTimeout(reconnectTimer);
|
|
530
590
|
closePeer();
|
|
531
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();
|
|
532
605
|
socket?.close();
|
|
533
606
|
showStatus(reconnectAttempt ? "reconnecting\u2026" : "connecting\u2026");
|
|
534
607
|
const query = new URLSearchParams([...params].filter(([key]) => ["transport", "profile", "adapt", "ice"].includes(key)));
|
|
@@ -754,7 +827,7 @@ setInterval(() => {
|
|
|
754
827
|
if (screen.paused && screen.srcObject instanceof MediaStream && screen.srcObject.getVideoTracks().some((track) => track.readyState === "live")) return;
|
|
755
828
|
const now = performance.now();
|
|
756
829
|
if (now - lastFrameAt >= webrtcFrameTimeoutMs) {
|
|
757
|
-
useFallback("
|
|
830
|
+
useFallback("video_stalled", true);
|
|
758
831
|
return;
|
|
759
832
|
}
|
|
760
833
|
if (inputChannel?.readyState !== "open") return;
|
|
@@ -778,7 +851,7 @@ setInterval(() => {
|
|
|
778
851
|
displayedWindow = { at: now, frames: presentedFrames };
|
|
779
852
|
}
|
|
780
853
|
const elapsedSeconds = (now - renderedWindowStartedAt) / 1e3;
|
|
781
|
-
if (transport !== "webrtc" && elapsedSeconds < 1) return;
|
|
854
|
+
if (!peer && transport !== "webrtc" && elapsedSeconds < 1) return;
|
|
782
855
|
const stats = {
|
|
783
856
|
sequence: ++statsSequence,
|
|
784
857
|
videoFps: renderedFrames / elapsedSeconds,
|
|
@@ -789,7 +862,7 @@ setInterval(() => {
|
|
|
789
862
|
renderedFrames = 0;
|
|
790
863
|
decodedAudioPackets = 0;
|
|
791
864
|
renderedWindowStartedAt = now;
|
|
792
|
-
if (
|
|
865
|
+
if (peer) void reportWebRtcStats(peer, stats).catch(() => {
|
|
793
866
|
});
|
|
794
867
|
else if (transport === "websocket") {
|
|
795
868
|
sendInput({ type: "stats", ...stats });
|