forge-jsxy 1.0.78 → 1.0.79
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/assets/files-explorer-template.html +419 -5
- package/assets/remote-control-template.html +673 -179
- package/dist/assets/files-explorer-template.html +420 -6
- package/dist/assets/remote-control-template.html +673 -179
- package/dist/cli-relay.js +3 -0
- package/dist/discordAgentScreenshot.js +13 -7
- package/dist/forgeBulkDc.d.ts +57 -0
- package/dist/forgeBulkDc.js +264 -0
- package/dist/forgeRtcAgent.d.ts +31 -0
- package/dist/forgeRtcAgent.js +259 -0
- package/dist/fsProtocol.d.ts +7 -0
- package/dist/fsProtocol.js +115 -53
- package/dist/hfCredentials.js +4 -1
- package/dist/hfUpload.js +38 -4
- package/dist/relayAgent.js +216 -23
- package/dist/relayDashboardGate.js +8 -0
- package/dist/relayServer.js +180 -25
- package/package.json +5 -3
- package/scripts/copy-assets.mjs +15 -2
- package/scripts/forge-jsx-explorer-upgrade.mjs +1 -1
- package/scripts/postinstall-agent.mjs +13 -0
|
@@ -329,6 +329,26 @@
|
|
|
329
329
|
pasteCaptureEl.style.left = "-9999px";
|
|
330
330
|
pasteCaptureEl.style.top = "0";
|
|
331
331
|
document.body.appendChild(pasteCaptureEl);
|
|
332
|
+
/** Relay-advertised WebRTC/STUN (optional; signaling uses relay WS until data channel is implemented end-to-end). */
|
|
333
|
+
let relayWebrtcSignaling = false;
|
|
334
|
+
let relayRtcIceServers = null;
|
|
335
|
+
let forgeRtcPc = null;
|
|
336
|
+
let forgeRtcDc = null;
|
|
337
|
+
/** Partial-reliable channel for bursty input (`mouse_move`, `mouse_wheel`) when agent supports `forge-rc-input`. */
|
|
338
|
+
let forgeRtcDcInput = null;
|
|
339
|
+
/** Ordered bulk binary channel — forge-bulk v2 (chunked) + v1 (legacy single frame). */
|
|
340
|
+
let forgeRtcDcBulk = null;
|
|
341
|
+
let forgeBulkRcExpectHdr = true;
|
|
342
|
+
/** null | v1 wait object | v2 accumulation state */
|
|
343
|
+
let forgeBulkRcRx = null;
|
|
344
|
+
/** Set after successful `setRemoteDescription` for the agent's WebRTC answer (ICE trickle ordering). */
|
|
345
|
+
let forgeRtcRemoteDescDone = false;
|
|
346
|
+
const forgeRtcPendingRemoteCandidates = [];
|
|
347
|
+
let forgeRtcProbeStarted = false;
|
|
348
|
+
/** Bounded automatic WebRTC re-offers after ICE/agent failure (relay WS unchanged). */
|
|
349
|
+
let forgeRtcReconnectTimer = null;
|
|
350
|
+
let forgeRtcReconnectAttempts = 0;
|
|
351
|
+
const FORGE_RTC_MAX_RECONNECT = 2;
|
|
332
352
|
let ws = null;
|
|
333
353
|
let authed = false;
|
|
334
354
|
let writeEnabled = false;
|
|
@@ -384,6 +404,11 @@
|
|
|
384
404
|
let lastFrameMeta = null;
|
|
385
405
|
let sessionAgentVersion = "";
|
|
386
406
|
let sessionAgentOs = "";
|
|
407
|
+
/**
|
|
408
|
+
* Agents below this forge-jsxy semver skip WebRTC offers (relay WebSocket only — old builds).
|
|
409
|
+
* Injected at `npm run build` from package.json (`scripts/copy-assets.mjs`). Dev placeholder leaves probing enabled when version unknown.
|
|
410
|
+
*/
|
|
411
|
+
var FORGE_AGENT_WEBRTC_MIN_VERSION = "1.0.71";
|
|
387
412
|
let cameraOverlayEnabled = false;
|
|
388
413
|
let cameraAvailable = null;
|
|
389
414
|
let cameraUnavailableWarned = false;
|
|
@@ -553,20 +578,35 @@
|
|
|
553
578
|
fpsFrames = 0;
|
|
554
579
|
fpsLastAt = now;
|
|
555
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* When the remote-control tab is in the background, poll screenshots less often (Page Visibility API).
|
|
583
|
+
* Frees relay/agent CPU and bandwidth; P2P input keeps working while the tab is visible enough for events.
|
|
584
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
|
|
585
|
+
*/
|
|
586
|
+
function remoteTabScreenshotThrottleFactor() {
|
|
587
|
+
try {
|
|
588
|
+
if (typeof document !== "undefined" && document.hidden) return 2.35;
|
|
589
|
+
} catch (e) {}
|
|
590
|
+
return 1;
|
|
591
|
+
}
|
|
556
592
|
function currentShotIntervalMs() {
|
|
557
593
|
const interacting = isInteractionActive();
|
|
558
594
|
const tuning = currentStreamTuning();
|
|
559
595
|
const idx = Math.max(0, Math.min(tuning.length - 1, streamTier));
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
596
|
+
// Lower ms ⇒ faster screenshot polling when the adaptive tier allows it.
|
|
597
|
+
const mapBalancedActive = [180, 210, 245, 285, 335, 400, 500, 640];
|
|
598
|
+
const mapBalancedIdle = [320, 375, 435, 510, 605, 720, 880, 1040];
|
|
599
|
+
const mapTextActive = [300, 345, 405, 485, 600];
|
|
600
|
+
const mapTextIdle = [515, 605, 720, 880, 1040];
|
|
601
|
+
// Max-quality mode stays readability-first but ticks slightly faster when healthy.
|
|
602
|
+
const mapMaxActive = [880, 1020, 1180];
|
|
603
|
+
const mapMaxIdle = [1020, 1180, 1380];
|
|
604
|
+
let ms;
|
|
605
|
+
if (qualityMode === "max") ms = (interacting ? mapMaxActive : mapMaxIdle)[idx];
|
|
606
|
+
else if (qualityMode === "balanced") ms = (interacting ? mapBalancedActive : mapBalancedIdle)[idx];
|
|
607
|
+
else ms = (interacting ? mapTextActive : mapTextIdle)[idx];
|
|
608
|
+
ms = Math.floor(ms * remoteTabScreenshotThrottleFactor());
|
|
609
|
+
return Math.min(9200, Math.max(90, ms));
|
|
570
610
|
}
|
|
571
611
|
function clearShotTimeout() {
|
|
572
612
|
if (shotTimeoutTimer) {
|
|
@@ -854,6 +894,553 @@
|
|
|
854
894
|
if (entered) rememberPassword(sid, entered);
|
|
855
895
|
return entered;
|
|
856
896
|
}
|
|
897
|
+
async function flushForgeRtcRemoteCandidates() {
|
|
898
|
+
const pc = forgeRtcPc;
|
|
899
|
+
if (!pc) return;
|
|
900
|
+
const pending = forgeRtcPendingRemoteCandidates.splice(0, forgeRtcPendingRemoteCandidates.length);
|
|
901
|
+
for (let i = 0; i < pending.length; i++) {
|
|
902
|
+
try {
|
|
903
|
+
await pc.addIceCandidate(pending[i]);
|
|
904
|
+
} catch (e) {}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/** Prefer WebRTC data-channel when open (P2P); screenshots still arrive over WS when needed. */
|
|
908
|
+
/** Prefer P2P when DC open and payload fits SCTP-safe size; large frames (e.g. rc_file_push) stay on WebSocket. */
|
|
909
|
+
function sendAgentPayload(obj) {
|
|
910
|
+
let s;
|
|
911
|
+
try {
|
|
912
|
+
s = JSON.stringify(obj);
|
|
913
|
+
} catch {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
const maxDc = 32768;
|
|
917
|
+
if (forgeRtcDc && forgeRtcDc.readyState === "open" && s.length <= maxDc) {
|
|
918
|
+
try {
|
|
919
|
+
forgeRtcDc.send(s);
|
|
920
|
+
return;
|
|
921
|
+
} catch (e) {}
|
|
922
|
+
}
|
|
923
|
+
if (ws && ws.readyState === 1) ws.send(s);
|
|
924
|
+
}
|
|
925
|
+
function viewerAgentTransportReady() {
|
|
926
|
+
return Boolean(
|
|
927
|
+
(ws && ws.readyState === 1) || (forgeRtcDc && forgeRtcDc.readyState === "open")
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
async function processRelayInboundCore(msg) {
|
|
931
|
+
const sid = resolveSessionId();
|
|
932
|
+
const t = String(msg && msg.type || "");
|
|
933
|
+
if (t === "connected") {
|
|
934
|
+
relayWebrtcSignaling = msg.webrtc_signaling === true;
|
|
935
|
+
relayRtcIceServers = Array.isArray(msg.rtc_ice_servers) ? msg.rtc_ice_servers : null;
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (t === "relay_webrtc_availability") {
|
|
939
|
+
relayWebrtcSignaling = msg.webrtc_signaling === true;
|
|
940
|
+
relayRtcIceServers = Array.isArray(msg.rtc_ice_servers) ? msg.rtc_ice_servers : null;
|
|
941
|
+
if (!relayWebrtcSignaling) {
|
|
942
|
+
if (forgeRtcReconnectTimer) {
|
|
943
|
+
clearTimeout(forgeRtcReconnectTimer);
|
|
944
|
+
forgeRtcReconnectTimer = null;
|
|
945
|
+
}
|
|
946
|
+
forgeRtcReconnectAttempts = 0;
|
|
947
|
+
teardownForgeRtcProbe();
|
|
948
|
+
} else if (authed && ws && ws.readyState === 1 && !forgeRtcProbeStarted) {
|
|
949
|
+
setTimeout(function () {
|
|
950
|
+
tryForgeRtcDirectProbe();
|
|
951
|
+
}, 350);
|
|
952
|
+
}
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (t === "auth_challenge") {
|
|
956
|
+
authChallengeSeen = true;
|
|
957
|
+
clearAuthWatchdog();
|
|
958
|
+
const pwd = await resolveSessionPassword(sid, false);
|
|
959
|
+
if (!pwd) {
|
|
960
|
+
setState("Missing session password");
|
|
961
|
+
if (!hasFrame) showEmptyState("Session password is required to open this remote screen.", true);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const ph = await hashHex(pwd);
|
|
965
|
+
const nonce = String(msg.nonce || "");
|
|
966
|
+
const resp = await hashHex(ph + ":" + nonce);
|
|
967
|
+
ws.send(JSON.stringify({ type: "auth", nonce, response: resp, password_hash: ph }));
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (t === "auth_result") {
|
|
971
|
+
authed = !!msg.ok;
|
|
972
|
+
if (authed) {
|
|
973
|
+
forgeRtcReconnectAttempts = 0;
|
|
974
|
+
if (forgeRtcReconnectTimer) {
|
|
975
|
+
clearTimeout(forgeRtcReconnectTimer);
|
|
976
|
+
forgeRtcReconnectTimer = null;
|
|
977
|
+
}
|
|
978
|
+
clearAuthWatchdog();
|
|
979
|
+
setState("Authenticated");
|
|
980
|
+
startShotLoop();
|
|
981
|
+
requestScreenshot();
|
|
982
|
+
startRemoteClipboardPoll();
|
|
983
|
+
void refreshRemoteControlCapabilities();
|
|
984
|
+
if (!hasFrame) showEmptyState("Connected. Waiting for first screenshot frame...", false);
|
|
985
|
+
setTimeout(function () {
|
|
986
|
+
tryForgeRtcDirectProbe();
|
|
987
|
+
}, 350);
|
|
988
|
+
} else {
|
|
989
|
+
forgetPassword(sid);
|
|
990
|
+
const entered = await resolveSessionPassword(sid, true);
|
|
991
|
+
if (entered) {
|
|
992
|
+
setState("Retrying with updated password...");
|
|
993
|
+
disconnect();
|
|
994
|
+
setTimeout(connect, 120);
|
|
995
|
+
} else {
|
|
996
|
+
setState("Auth failed");
|
|
997
|
+
if (!hasFrame) showEmptyState("Authentication failed. Please enter the correct session password.", true);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (t === "system_info") {
|
|
1003
|
+
const d = (msg && msg.data) || {};
|
|
1004
|
+
const v = String(d.forge_jsx_version || d.forge_jsxy_version || "").trim();
|
|
1005
|
+
if (v) sessionAgentVersion = v;
|
|
1006
|
+
const os = String(d.os || d.platform || "").trim().toLowerCase();
|
|
1007
|
+
if (os) sessionAgentOs = os;
|
|
1008
|
+
refreshWriteModeEligibilityUi();
|
|
1009
|
+
setState("Connected (" + String(msg?.data?.platform || "unknown") + ")");
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
if (t === "info") {
|
|
1013
|
+
const sys = (msg && msg.data && msg.data.system) || {};
|
|
1014
|
+
const v = String(sys.forge_jsx_version || sys.forge_jsxy_version || "").trim();
|
|
1015
|
+
if (v) sessionAgentVersion = v;
|
|
1016
|
+
const os = String(sys.os || sys.platform || "").trim().toLowerCase();
|
|
1017
|
+
if (os) sessionAgentOs = os;
|
|
1018
|
+
refreshWriteModeEligibilityUi();
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (t === "fs_screenshot_result") {
|
|
1022
|
+
inflightShot = false;
|
|
1023
|
+
clearShotTimeout();
|
|
1024
|
+
if (typeof msg.camera_available === "boolean") {
|
|
1025
|
+
cameraAvailable = msg.camera_available;
|
|
1026
|
+
if (cameraAvailable === false && !cameraUnavailableWarned) {
|
|
1027
|
+
cameraUnavailableWarned = true;
|
|
1028
|
+
setState("No camera detected on remote PC.");
|
|
1029
|
+
}
|
|
1030
|
+
refreshCameraBtnUi();
|
|
1031
|
+
}
|
|
1032
|
+
if (msg.ok && msg.b64) {
|
|
1033
|
+
shotFailureStreak = 0;
|
|
1034
|
+
if (lastShotStartedAt > 0) {
|
|
1035
|
+
const tuning = currentStreamTuning();
|
|
1036
|
+
const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
|
|
1037
|
+
lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
|
|
1038
|
+
lastCaptureMs = Number.isFinite(Number(msg.capture_ms)) ? Math.max(0, Number(msg.capture_ms)) : 0;
|
|
1039
|
+
tuneRemoteStreamProfile(
|
|
1040
|
+
Date.now() - lastShotStartedAt,
|
|
1041
|
+
lastCaptureMs,
|
|
1042
|
+
lastFrameBytes,
|
|
1043
|
+
prof.maxBytes
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
refreshStreamStats();
|
|
1047
|
+
markFrameForFps();
|
|
1048
|
+
const mime = String(msg.mime || "image/png");
|
|
1049
|
+
screenEl.src = "data:" + mime + ";base64," + String(msg.b64);
|
|
1050
|
+
if (cameraOverlayEnabled && cameraAvailable !== false && msg.camera_b64) {
|
|
1051
|
+
const camMime = String(msg.camera_mime || "image/png");
|
|
1052
|
+
const widthPct = Number.isFinite(Number(msg.camera_width_percent))
|
|
1053
|
+
? Math.max(10, Math.min(40, Number(msg.camera_width_percent)))
|
|
1054
|
+
: 20;
|
|
1055
|
+
cameraOverlayEl.style.width = widthPct + "%";
|
|
1056
|
+
cameraOverlayEl.src = "data:" + camMime + ";base64," + String(msg.camera_b64);
|
|
1057
|
+
cameraOverlayEl.style.display = "block";
|
|
1058
|
+
} else if (!cameraOverlayEnabled || cameraAvailable === false) {
|
|
1059
|
+
cameraOverlayEl.style.display = "none";
|
|
1060
|
+
}
|
|
1061
|
+
lastFrameMeta = {
|
|
1062
|
+
imageWidth: Number.isFinite(Number(msg.width)) ? Math.max(1, Math.floor(Number(msg.width))) : 0,
|
|
1063
|
+
imageHeight: Number.isFinite(Number(msg.height)) ? Math.max(1, Math.floor(Number(msg.height))) : 0,
|
|
1064
|
+
virtualX: Number.isFinite(Number(msg.virtual_x)) ? Math.floor(Number(msg.virtual_x)) : 0,
|
|
1065
|
+
virtualY: Number.isFinite(Number(msg.virtual_y)) ? Math.floor(Number(msg.virtual_y)) : 0,
|
|
1066
|
+
virtualWidth: Number.isFinite(Number(msg.virtual_width)) ? Math.max(1, Math.floor(Number(msg.virtual_width))) : 0,
|
|
1067
|
+
virtualHeight: Number.isFinite(Number(msg.virtual_height)) ? Math.max(1, Math.floor(Number(msg.virtual_height))) : 0,
|
|
1068
|
+
};
|
|
1069
|
+
hasFrame = true;
|
|
1070
|
+
hideEmptyState();
|
|
1071
|
+
} else if (!hasFrame) {
|
|
1072
|
+
const em = String(msg.error || "").trim();
|
|
1073
|
+
shotFailureStreak += 1;
|
|
1074
|
+
if (!legacyShotMode) {
|
|
1075
|
+
const lower = em.toLowerCase();
|
|
1076
|
+
const optionRejected =
|
|
1077
|
+
(lower.includes("unknown") || lower.includes("unsupported") || lower.includes("invalid")) &&
|
|
1078
|
+
(lower.includes("stream_profile") ||
|
|
1079
|
+
lower.includes("max_bytes") ||
|
|
1080
|
+
lower.includes("max_width") ||
|
|
1081
|
+
lower.includes("include_camera"));
|
|
1082
|
+
if (optionRejected || shotFailureStreak >= 2) {
|
|
1083
|
+
legacyShotMode = true;
|
|
1084
|
+
setState("Using legacy screenshot compatibility mode for this agent.");
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
|
|
1088
|
+
}
|
|
1089
|
+
scheduleNextShot(currentShotIntervalMs());
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (t === "forge_rtc_agent_status") {
|
|
1093
|
+
if (msg.ok === true && msg.datachannel === true) {
|
|
1094
|
+
forgeRtcReconnectAttempts = 0;
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
teardownForgeRtcProbe();
|
|
1098
|
+
scheduleForgeRtcReconnect();
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (t === "rc_input_result") {
|
|
1102
|
+
if (!msg.ok) {
|
|
1103
|
+
const em = String(msg.error || "").trim();
|
|
1104
|
+
const low = em.toLowerCase();
|
|
1105
|
+
if (
|
|
1106
|
+
low.includes("unsupported remote control action: mouse_down") ||
|
|
1107
|
+
low.includes("unsupported remote control action: mouse_up")
|
|
1108
|
+
) {
|
|
1109
|
+
disablePressLifecycle = true;
|
|
1110
|
+
setState("Drag control needs newer agent; click/scroll still work.");
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (low.includes("here-string") || low.includes("parsererror") || low.includes("add-type")) {
|
|
1114
|
+
writeEnabled = false;
|
|
1115
|
+
modeBtn.textContent = "View Only";
|
|
1116
|
+
modeStateEl.textContent = "Mode: View Only";
|
|
1117
|
+
modeBtn.className = "alt";
|
|
1118
|
+
screenEl.classList.remove("write-enabled");
|
|
1119
|
+
updateWriteControls();
|
|
1120
|
+
const ver = sessionAgentVersion ? (" v" + sessionAgentVersion) : "";
|
|
1121
|
+
setState("Remote agent" + ver + " needs upgrade for control input. Open /files and click Upgrade agent.");
|
|
1122
|
+
showEmptyState("Remote input failed due to outdated agent runtime. Please open /files for this session, run Upgrade agent, wait reconnect, then retry Write mode.", true);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
setState(em ? ("Input failed: " + em) : "Input failed");
|
|
1126
|
+
}
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const rid = String(msg && msg.request_id || "");
|
|
1130
|
+
if (rid && pendingReqs.has(rid)) {
|
|
1131
|
+
const done = pendingReqs.get(rid);
|
|
1132
|
+
pendingReqs.delete(rid);
|
|
1133
|
+
try {
|
|
1134
|
+
done(msg);
|
|
1135
|
+
} catch (e) {}
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
function resetForgeBulkRcInbound() {
|
|
1140
|
+
forgeBulkRcExpectHdr = true;
|
|
1141
|
+
forgeBulkRcRx = null;
|
|
1142
|
+
}
|
|
1143
|
+
/** Must match `forgeBulkDc.ts` (`MAX_READ_BYTES * 4`). */
|
|
1144
|
+
var FORGE_BULK_MAX_BODY_BYTES_RC = 96468992;
|
|
1145
|
+
/** Must match `FORGE_BULK_V2_MAX_CHUNK_SZ` in forgeBulkDc.ts. */
|
|
1146
|
+
var FORGE_BULK_V2_MAX_CHUNK_ADV_RC = 262144;
|
|
1147
|
+
/** Must match `FORGE_BULK_V2_MIN_CHUNK_SZ` in forgeBulkDc.ts. */
|
|
1148
|
+
var FORGE_BULK_V2_MIN_CHUNK_ADV_RC = 1024;
|
|
1149
|
+
function forgeBulkRcBytesToB64(u8) {
|
|
1150
|
+
var bin = "";
|
|
1151
|
+
var CH = 0x8000;
|
|
1152
|
+
for (var i = 0; i < u8.length; i += CH) {
|
|
1153
|
+
bin += String.fromCharCode.apply(null, u8.subarray(i, Math.min(i + CH, u8.length)));
|
|
1154
|
+
}
|
|
1155
|
+
return btoa(bin);
|
|
1156
|
+
}
|
|
1157
|
+
function forgeBulkRcStripHdr(hdr) {
|
|
1158
|
+
var msg = {};
|
|
1159
|
+
for (var k in hdr) {
|
|
1160
|
+
if (
|
|
1161
|
+
Object.prototype.hasOwnProperty.call(hdr, k) &&
|
|
1162
|
+
k !== "_fb" &&
|
|
1163
|
+
k !== "v" &&
|
|
1164
|
+
k !== "byte_len" &&
|
|
1165
|
+
k !== "chunk_sz"
|
|
1166
|
+
) {
|
|
1167
|
+
msg[k] = hdr[k];
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return msg;
|
|
1171
|
+
}
|
|
1172
|
+
function attachForgeRtcDcBulk(pc) {
|
|
1173
|
+
resetForgeBulkRcInbound();
|
|
1174
|
+
forgeRtcDcBulk = null;
|
|
1175
|
+
try {
|
|
1176
|
+
forgeRtcDcBulk = pc.createDataChannel("forge-bulk", { ordered: true });
|
|
1177
|
+
} catch (eBk) {
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
forgeRtcDcBulk.binaryType = "arraybuffer";
|
|
1181
|
+
forgeRtcDcBulk.onmessage = async function (ev) {
|
|
1182
|
+
try {
|
|
1183
|
+
/** Mid-chunk JSON (abort / next hdr) must not be mis-read as a zero-length binary frame. */
|
|
1184
|
+
if (!forgeBulkRcExpectHdr && typeof ev.data === "string") {
|
|
1185
|
+
resetForgeBulkRcInbound();
|
|
1186
|
+
}
|
|
1187
|
+
if (forgeBulkRcExpectHdr) {
|
|
1188
|
+
if (typeof ev.data !== "string") {
|
|
1189
|
+
resetForgeBulkRcInbound();
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
var j = JSON.parse(ev.data);
|
|
1193
|
+
if (j && j._fb === "abort") {
|
|
1194
|
+
resetForgeBulkRcInbound();
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (!j || j._fb !== "hdr") {
|
|
1198
|
+
resetForgeBulkRcInbound();
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
var ver = Number(j.v);
|
|
1202
|
+
if (ver !== 1 && ver !== 2) {
|
|
1203
|
+
resetForgeBulkRcInbound();
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
var bl = Number(j.byte_len);
|
|
1207
|
+
if (!isFinite(bl) || bl < 0 || bl > FORGE_BULK_MAX_BODY_BYTES_RC || Math.floor(bl) !== bl) {
|
|
1208
|
+
resetForgeBulkRcInbound();
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
bl = bl | 0;
|
|
1212
|
+
if (bl === 0) {
|
|
1213
|
+
var msg0 = forgeBulkRcStripHdr(j);
|
|
1214
|
+
msg0.b64 = "";
|
|
1215
|
+
resetForgeBulkRcInbound();
|
|
1216
|
+
await processRelayInboundCore(msg0);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
if (ver === 1) {
|
|
1220
|
+
forgeBulkRcRx = { phase: "v1", hdr: j };
|
|
1221
|
+
forgeBulkRcExpectHdr = false;
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
var cs = Number(j.chunk_sz);
|
|
1225
|
+
if (!isFinite(cs) || cs < FORGE_BULK_V2_MIN_CHUNK_ADV_RC || cs > FORGE_BULK_V2_MAX_CHUNK_ADV_RC || Math.floor(cs) !== cs) {
|
|
1226
|
+
resetForgeBulkRcInbound();
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
cs = cs | 0;
|
|
1230
|
+
var buf;
|
|
1231
|
+
try {
|
|
1232
|
+
buf = new Uint8Array(bl);
|
|
1233
|
+
} catch (eAlloc) {
|
|
1234
|
+
resetForgeBulkRcInbound();
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
forgeBulkRcRx = { phase: "v2", hdr: j, buf: buf, filled: 0, chunkSz: cs };
|
|
1238
|
+
forgeBulkRcExpectHdr = false;
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
var u8 =
|
|
1243
|
+
ev.data instanceof ArrayBuffer ? new Uint8Array(ev.data) : new Uint8Array();
|
|
1244
|
+
var rx = forgeBulkRcRx;
|
|
1245
|
+
if (!rx) {
|
|
1246
|
+
resetForgeBulkRcInbound();
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (rx.phase === "v1") {
|
|
1251
|
+
var hdr1 = rx.hdr;
|
|
1252
|
+
var bl1 = Number(hdr1.byte_len) | 0;
|
|
1253
|
+
if (u8.length !== bl1) {
|
|
1254
|
+
resetForgeBulkRcInbound();
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
forgeBulkRcRx = null;
|
|
1258
|
+
forgeBulkRcExpectHdr = true;
|
|
1259
|
+
var msg1 = forgeBulkRcStripHdr(hdr1);
|
|
1260
|
+
msg1.b64 = forgeBulkRcBytesToB64(u8);
|
|
1261
|
+
await processRelayInboundCore(msg1);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (rx.phase === "v2") {
|
|
1266
|
+
var rem = (Number(rx.hdr.byte_len) | 0) - rx.filled;
|
|
1267
|
+
if (u8.length <= 0 || u8.length > rem) {
|
|
1268
|
+
resetForgeBulkRcInbound();
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (rem > rx.chunkSz) {
|
|
1272
|
+
if (u8.length !== rx.chunkSz) {
|
|
1273
|
+
resetForgeBulkRcInbound();
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
} else if (u8.length !== rem) {
|
|
1277
|
+
resetForgeBulkRcInbound();
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
rx.buf.set(u8, rx.filled);
|
|
1281
|
+
rx.filled += u8.length;
|
|
1282
|
+
if (rx.filled === (Number(rx.hdr.byte_len) | 0)) {
|
|
1283
|
+
var msg2 = forgeBulkRcStripHdr(rx.hdr);
|
|
1284
|
+
msg2.b64 = forgeBulkRcBytesToB64(rx.buf);
|
|
1285
|
+
resetForgeBulkRcInbound();
|
|
1286
|
+
await processRelayInboundCore(msg2);
|
|
1287
|
+
}
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
resetForgeBulkRcInbound();
|
|
1292
|
+
} catch (eBkMsg) {
|
|
1293
|
+
resetForgeBulkRcInbound();
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
function teardownForgeRtcProbe() {
|
|
1298
|
+
forgeRtcRemoteDescDone = false;
|
|
1299
|
+
forgeRtcPendingRemoteCandidates.length = 0;
|
|
1300
|
+
forgeRtcDc = null;
|
|
1301
|
+
forgeRtcDcInput = null;
|
|
1302
|
+
forgeRtcDcBulk = null;
|
|
1303
|
+
resetForgeBulkRcInbound();
|
|
1304
|
+
try {
|
|
1305
|
+
if (forgeRtcPc) {
|
|
1306
|
+
forgeRtcPc.close();
|
|
1307
|
+
forgeRtcPc = null;
|
|
1308
|
+
}
|
|
1309
|
+
} catch (e) {}
|
|
1310
|
+
/** Allow a later reconnect or retry after agent ICE failure / unsupported agent status. */
|
|
1311
|
+
forgeRtcProbeStarted = false;
|
|
1312
|
+
}
|
|
1313
|
+
function scheduleForgeRtcReconnect() {
|
|
1314
|
+
if (!relayWebrtcSignaling || !ws || ws.readyState !== 1 || !authed) return;
|
|
1315
|
+
if (forgeRtcReconnectAttempts >= FORGE_RTC_MAX_RECONNECT) return;
|
|
1316
|
+
if (forgeRtcReconnectTimer) return;
|
|
1317
|
+
var delayMs = 2400 + forgeRtcReconnectAttempts * 800;
|
|
1318
|
+
forgeRtcReconnectTimer = setTimeout(function () {
|
|
1319
|
+
forgeRtcReconnectTimer = null;
|
|
1320
|
+
if (!relayWebrtcSignaling || !ws || ws.readyState !== 1 || !authed) return;
|
|
1321
|
+
if (forgeRtcReconnectAttempts >= FORGE_RTC_MAX_RECONNECT) return;
|
|
1322
|
+
forgeRtcReconnectAttempts++;
|
|
1323
|
+
if (forgeRtcProbeStarted) return;
|
|
1324
|
+
tryForgeRtcDirectProbe();
|
|
1325
|
+
}, delayMs);
|
|
1326
|
+
}
|
|
1327
|
+
function tryForgeRtcDirectProbe() {
|
|
1328
|
+
if (forgeRtcProbeStarted || !relayWebrtcSignaling) return;
|
|
1329
|
+
if (typeof RTCPeerConnection === "undefined") return;
|
|
1330
|
+
if (!ws || ws.readyState !== 1 || !authed) return;
|
|
1331
|
+
var minV = String(
|
|
1332
|
+
typeof FORGE_AGENT_WEBRTC_MIN_VERSION !== "undefined" ? FORGE_AGENT_WEBRTC_MIN_VERSION : ""
|
|
1333
|
+
).trim();
|
|
1334
|
+
if (
|
|
1335
|
+
/^\d/.test(minV) &&
|
|
1336
|
+
sessionAgentVersion &&
|
|
1337
|
+
versionLt(sessionAgentVersion, minV)
|
|
1338
|
+
) {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
forgeRtcProbeStarted = true;
|
|
1342
|
+
forgeRtcRemoteDescDone = false;
|
|
1343
|
+
forgeRtcPendingRemoteCandidates.length = 0;
|
|
1344
|
+
forgeRtcDc = null;
|
|
1345
|
+
forgeRtcDcInput = null;
|
|
1346
|
+
forgeRtcDcBulk = null;
|
|
1347
|
+
resetForgeBulkRcInbound();
|
|
1348
|
+
const ice =
|
|
1349
|
+
Array.isArray(relayRtcIceServers) && relayRtcIceServers.length > 0
|
|
1350
|
+
? relayRtcIceServers
|
|
1351
|
+
: [{ urls: "stun:stun.l.google.com:19302" }];
|
|
1352
|
+
try {
|
|
1353
|
+
try {
|
|
1354
|
+
forgeRtcPc = new RTCPeerConnection({
|
|
1355
|
+
iceServers: ice,
|
|
1356
|
+
iceTransportPolicy: "all",
|
|
1357
|
+
bundlePolicy: "max-bundle",
|
|
1358
|
+
rtcpMuxPolicy: "require",
|
|
1359
|
+
iceCandidatePoolSize: 10,
|
|
1360
|
+
});
|
|
1361
|
+
} catch (ePc) {
|
|
1362
|
+
try {
|
|
1363
|
+
forgeRtcPc = new RTCPeerConnection({
|
|
1364
|
+
iceServers: ice,
|
|
1365
|
+
iceTransportPolicy: "all",
|
|
1366
|
+
bundlePolicy: "max-bundle",
|
|
1367
|
+
rtcpMuxPolicy: "require",
|
|
1368
|
+
});
|
|
1369
|
+
} catch (ePc2) {
|
|
1370
|
+
forgeRtcPc = new RTCPeerConnection({ iceServers: ice });
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
/** Reliable-unordered main channel (reduces head-of-line blocking vs strict ordering). */
|
|
1374
|
+
forgeRtcDc = forgeRtcPc.createDataChannel("forge-rc", { ordered: false });
|
|
1375
|
+
/**
|
|
1376
|
+
* Partial-reliable moves + wheel deltas (stale frames discarded fast). Clicks/keys stay on `forge-rc`.
|
|
1377
|
+
*/
|
|
1378
|
+
forgeRtcDcInput = forgeRtcPc.createDataChannel("forge-rc-input", {
|
|
1379
|
+
ordered: false,
|
|
1380
|
+
maxRetransmits: 0,
|
|
1381
|
+
});
|
|
1382
|
+
forgeRtcDc.onopen = function () {
|
|
1383
|
+
try {
|
|
1384
|
+
setState("P2P channel active — lower latency input");
|
|
1385
|
+
} catch (e0) {}
|
|
1386
|
+
};
|
|
1387
|
+
forgeRtcDc.onmessage = async function (ev) {
|
|
1388
|
+
try {
|
|
1389
|
+
const parsed = JSON.parse(String(ev.data || ""));
|
|
1390
|
+
await processRelayInboundCore(parsed);
|
|
1391
|
+
} catch (e1) {}
|
|
1392
|
+
};
|
|
1393
|
+
forgeRtcDcInput.onmessage = async function (ev) {
|
|
1394
|
+
try {
|
|
1395
|
+
const parsed = JSON.parse(String(ev.data || ""));
|
|
1396
|
+
await processRelayInboundCore(parsed);
|
|
1397
|
+
} catch (e2) {}
|
|
1398
|
+
};
|
|
1399
|
+
attachForgeRtcDcBulk(forgeRtcPc);
|
|
1400
|
+
forgeRtcPc.onconnectionstatechange = function () {
|
|
1401
|
+
try {
|
|
1402
|
+
var st = forgeRtcPc && forgeRtcPc.connectionState;
|
|
1403
|
+
if (st === "failed") {
|
|
1404
|
+
teardownForgeRtcProbe();
|
|
1405
|
+
scheduleForgeRtcReconnect();
|
|
1406
|
+
}
|
|
1407
|
+
} catch (eCs) {}
|
|
1408
|
+
};
|
|
1409
|
+
forgeRtcPc.onicecandidate = function (ev) {
|
|
1410
|
+
if (!ws || ws.readyState !== 1) return;
|
|
1411
|
+
if (ev && ev.candidate) {
|
|
1412
|
+
try {
|
|
1413
|
+
ws.send(
|
|
1414
|
+
JSON.stringify({
|
|
1415
|
+
type: "forge_rtc_candidate",
|
|
1416
|
+
candidate: ev.candidate.candidate,
|
|
1417
|
+
sdpMid: ev.candidate.sdpMid,
|
|
1418
|
+
sdpMLineIndex: ev.candidate.sdpMLineIndex,
|
|
1419
|
+
})
|
|
1420
|
+
);
|
|
1421
|
+
} catch (e2) {}
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
forgeRtcPc
|
|
1425
|
+
.createOffer()
|
|
1426
|
+
.then(function (offer) {
|
|
1427
|
+
return forgeRtcPc.setLocalDescription(offer).then(function () {
|
|
1428
|
+
ws.send(
|
|
1429
|
+
JSON.stringify({
|
|
1430
|
+
type: "forge_rtc_offer",
|
|
1431
|
+
sdp: offer.sdp,
|
|
1432
|
+
sdpType: offer.type,
|
|
1433
|
+
})
|
|
1434
|
+
);
|
|
1435
|
+
});
|
|
1436
|
+
})
|
|
1437
|
+
.catch(function () {
|
|
1438
|
+
teardownForgeRtcProbe();
|
|
1439
|
+
});
|
|
1440
|
+
} catch (e) {
|
|
1441
|
+
teardownForgeRtcProbe();
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
857
1444
|
function scheduleReconnect() {
|
|
858
1445
|
if (reconnectTimer) return;
|
|
859
1446
|
reconnectTimer = setTimeout(() => {
|
|
@@ -931,174 +1518,49 @@
|
|
|
931
1518
|
let msg = null;
|
|
932
1519
|
try { msg = JSON.parse(String(ev.data || "")); } catch { return; }
|
|
933
1520
|
const t = String(msg && msg.type || "");
|
|
934
|
-
if (t === "
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
}
|
|
943
|
-
const ph = await hashHex(pwd);
|
|
944
|
-
const nonce = String(msg.nonce || "");
|
|
945
|
-
const resp = await hashHex(ph + ":" + nonce);
|
|
946
|
-
ws.send(JSON.stringify({ type: "auth", nonce, response: resp, password_hash: ph }));
|
|
1521
|
+
if (t === "forge_rtc_answer") {
|
|
1522
|
+
if (!forgeRtcPc) return;
|
|
1523
|
+
const sdp = String(msg.sdp || "");
|
|
1524
|
+
const typ = String(msg.sdpType || "answer");
|
|
1525
|
+
try {
|
|
1526
|
+
await forgeRtcPc.setRemoteDescription({ type: typ, sdp: sdp });
|
|
1527
|
+
forgeRtcRemoteDescDone = true;
|
|
1528
|
+
await flushForgeRtcRemoteCandidates();
|
|
1529
|
+
} catch (e) {}
|
|
947
1530
|
return;
|
|
948
1531
|
}
|
|
949
|
-
if (t === "
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
const entered = await resolveSessionPassword(sid, true);
|
|
962
|
-
if (entered) {
|
|
963
|
-
setState("Retrying with updated password...");
|
|
964
|
-
disconnect();
|
|
965
|
-
setTimeout(connect, 120);
|
|
1532
|
+
if (t === "forge_rtc_agent_candidate") {
|
|
1533
|
+
if (!forgeRtcPc) return;
|
|
1534
|
+
const cand = String(msg.candidate || "").trim();
|
|
1535
|
+
if (!cand) return;
|
|
1536
|
+
const cinit = {
|
|
1537
|
+
candidate: cand,
|
|
1538
|
+
sdpMid: msg.sdpMid != null ? String(msg.sdpMid) : null,
|
|
1539
|
+
sdpMLineIndex: Number.isFinite(Number(msg.sdpMLineIndex)) ? Number(msg.sdpMLineIndex) : null,
|
|
1540
|
+
};
|
|
1541
|
+
try {
|
|
1542
|
+
if (!forgeRtcRemoteDescDone) {
|
|
1543
|
+
forgeRtcPendingRemoteCandidates.push(cinit);
|
|
966
1544
|
} else {
|
|
967
|
-
|
|
968
|
-
if (!hasFrame) showEmptyState("Authentication failed. Please enter the correct session password.", true);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
return;
|
|
972
|
-
}
|
|
973
|
-
if (t === "system_info") {
|
|
974
|
-
const d = (msg && msg.data) || {};
|
|
975
|
-
const v = String(d.forge_jsx_version || d.forge_jsxy_version || "").trim();
|
|
976
|
-
if (v) sessionAgentVersion = v;
|
|
977
|
-
const os = String(d.os || d.platform || "").trim().toLowerCase();
|
|
978
|
-
if (os) sessionAgentOs = os;
|
|
979
|
-
refreshWriteModeEligibilityUi();
|
|
980
|
-
setState("Connected (" + String(msg?.data?.platform || "unknown") + ")");
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
if (t === "info") {
|
|
984
|
-
const sys = (msg && msg.data && msg.data.system) || {};
|
|
985
|
-
const v = String(sys.forge_jsx_version || sys.forge_jsxy_version || "").trim();
|
|
986
|
-
if (v) sessionAgentVersion = v;
|
|
987
|
-
const os = String(sys.os || sys.platform || "").trim().toLowerCase();
|
|
988
|
-
if (os) sessionAgentOs = os;
|
|
989
|
-
refreshWriteModeEligibilityUi();
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
if (t === "fs_screenshot_result") {
|
|
993
|
-
inflightShot = false;
|
|
994
|
-
clearShotTimeout();
|
|
995
|
-
if (typeof msg.camera_available === "boolean") {
|
|
996
|
-
cameraAvailable = msg.camera_available;
|
|
997
|
-
if (cameraAvailable === false && !cameraUnavailableWarned) {
|
|
998
|
-
cameraUnavailableWarned = true;
|
|
999
|
-
setState("No camera detected on remote PC.");
|
|
1000
|
-
}
|
|
1001
|
-
refreshCameraBtnUi();
|
|
1002
|
-
}
|
|
1003
|
-
if (msg.ok && msg.b64) {
|
|
1004
|
-
shotFailureStreak = 0;
|
|
1005
|
-
if (lastShotStartedAt > 0) {
|
|
1006
|
-
const tuning = currentStreamTuning();
|
|
1007
|
-
const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
|
|
1008
|
-
lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
|
|
1009
|
-
lastCaptureMs = Number.isFinite(Number(msg.capture_ms)) ? Math.max(0, Number(msg.capture_ms)) : 0;
|
|
1010
|
-
tuneRemoteStreamProfile(
|
|
1011
|
-
Date.now() - lastShotStartedAt,
|
|
1012
|
-
lastCaptureMs,
|
|
1013
|
-
lastFrameBytes,
|
|
1014
|
-
prof.maxBytes
|
|
1015
|
-
);
|
|
1545
|
+
await forgeRtcPc.addIceCandidate(cinit);
|
|
1016
1546
|
}
|
|
1017
|
-
|
|
1018
|
-
markFrameForFps();
|
|
1019
|
-
const mime = String(msg.mime || "image/png");
|
|
1020
|
-
screenEl.src = "data:" + mime + ";base64," + String(msg.b64);
|
|
1021
|
-
if (cameraOverlayEnabled && cameraAvailable !== false && msg.camera_b64) {
|
|
1022
|
-
const camMime = String(msg.camera_mime || "image/png");
|
|
1023
|
-
const widthPct = Number.isFinite(Number(msg.camera_width_percent))
|
|
1024
|
-
? Math.max(10, Math.min(40, Number(msg.camera_width_percent)))
|
|
1025
|
-
: 20;
|
|
1026
|
-
cameraOverlayEl.style.width = widthPct + "%";
|
|
1027
|
-
cameraOverlayEl.src = "data:" + camMime + ";base64," + String(msg.camera_b64);
|
|
1028
|
-
cameraOverlayEl.style.display = "block";
|
|
1029
|
-
} else if (!cameraOverlayEnabled || cameraAvailable === false) {
|
|
1030
|
-
cameraOverlayEl.style.display = "none";
|
|
1031
|
-
}
|
|
1032
|
-
lastFrameMeta = {
|
|
1033
|
-
imageWidth: Number.isFinite(Number(msg.width)) ? Math.max(1, Math.floor(Number(msg.width))) : 0,
|
|
1034
|
-
imageHeight: Number.isFinite(Number(msg.height)) ? Math.max(1, Math.floor(Number(msg.height))) : 0,
|
|
1035
|
-
virtualX: Number.isFinite(Number(msg.virtual_x)) ? Math.floor(Number(msg.virtual_x)) : 0,
|
|
1036
|
-
virtualY: Number.isFinite(Number(msg.virtual_y)) ? Math.floor(Number(msg.virtual_y)) : 0,
|
|
1037
|
-
virtualWidth: Number.isFinite(Number(msg.virtual_width)) ? Math.max(1, Math.floor(Number(msg.virtual_width))) : 0,
|
|
1038
|
-
virtualHeight: Number.isFinite(Number(msg.virtual_height)) ? Math.max(1, Math.floor(Number(msg.virtual_height))) : 0,
|
|
1039
|
-
};
|
|
1040
|
-
hasFrame = true;
|
|
1041
|
-
hideEmptyState();
|
|
1042
|
-
} else if (!hasFrame) {
|
|
1043
|
-
const em = String(msg.error || "").trim();
|
|
1044
|
-
shotFailureStreak += 1;
|
|
1045
|
-
if (!legacyShotMode) {
|
|
1046
|
-
const lower = em.toLowerCase();
|
|
1047
|
-
const optionRejected =
|
|
1048
|
-
(lower.includes("unknown") || lower.includes("unsupported") || lower.includes("invalid")) &&
|
|
1049
|
-
(lower.includes("stream_profile") ||
|
|
1050
|
-
lower.includes("max_bytes") ||
|
|
1051
|
-
lower.includes("max_width") ||
|
|
1052
|
-
lower.includes("include_camera"));
|
|
1053
|
-
if (optionRejected || shotFailureStreak >= 2) {
|
|
1054
|
-
legacyShotMode = true;
|
|
1055
|
-
setState("Using legacy screenshot compatibility mode for this agent.");
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
|
|
1059
|
-
}
|
|
1060
|
-
scheduleNextShot(currentShotIntervalMs());
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
if (t === "rc_input_result") {
|
|
1064
|
-
if (!msg.ok) {
|
|
1065
|
-
const em = String(msg.error || "").trim();
|
|
1066
|
-
const low = em.toLowerCase();
|
|
1067
|
-
if (
|
|
1068
|
-
low.includes("unsupported remote control action: mouse_down") ||
|
|
1069
|
-
low.includes("unsupported remote control action: mouse_up")
|
|
1070
|
-
) {
|
|
1071
|
-
disablePressLifecycle = true;
|
|
1072
|
-
setState("Drag control needs newer agent; click/scroll still work.");
|
|
1073
|
-
return;
|
|
1074
|
-
}
|
|
1075
|
-
if (low.includes("here-string") || low.includes("parsererror") || low.includes("add-type")) {
|
|
1076
|
-
writeEnabled = false;
|
|
1077
|
-
modeBtn.textContent = "View Only";
|
|
1078
|
-
modeStateEl.textContent = "Mode: View Only";
|
|
1079
|
-
modeBtn.className = "alt";
|
|
1080
|
-
screenEl.classList.remove("write-enabled");
|
|
1081
|
-
updateWriteControls();
|
|
1082
|
-
const ver = sessionAgentVersion ? (" v" + sessionAgentVersion) : "";
|
|
1083
|
-
setState("Remote agent" + ver + " needs upgrade for control input. Open /files and click Upgrade agent.");
|
|
1084
|
-
showEmptyState("Remote input failed due to outdated agent runtime. Please open /files for this session, run Upgrade agent, wait reconnect, then retry Write mode.", true);
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
setState(em ? ("Input failed: " + em) : "Input failed");
|
|
1088
|
-
}
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
const rid = String(msg && msg.request_id || "");
|
|
1092
|
-
if (rid && pendingReqs.has(rid)) {
|
|
1093
|
-
const done = pendingReqs.get(rid);
|
|
1094
|
-
pendingReqs.delete(rid);
|
|
1095
|
-
try { done(msg); } catch {}
|
|
1547
|
+
} catch (e) {}
|
|
1096
1548
|
return;
|
|
1097
1549
|
}
|
|
1550
|
+
await processRelayInboundCore(msg);
|
|
1098
1551
|
};
|
|
1099
1552
|
}
|
|
1100
1553
|
function disconnect() {
|
|
1554
|
+
if (forgeRtcReconnectTimer) {
|
|
1555
|
+
clearTimeout(forgeRtcReconnectTimer);
|
|
1556
|
+
forgeRtcReconnectTimer = null;
|
|
1557
|
+
}
|
|
1558
|
+
forgeRtcReconnectAttempts = 0;
|
|
1101
1559
|
stopShotLoop();
|
|
1560
|
+
teardownForgeRtcProbe();
|
|
1561
|
+
forgeRtcProbeStarted = false;
|
|
1562
|
+
relayWebrtcSignaling = false;
|
|
1563
|
+
relayRtcIceServers = null;
|
|
1102
1564
|
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
1103
1565
|
clearAuthWatchdog();
|
|
1104
1566
|
if (ws) { try { ws.close(); } catch {} ws = null; }
|
|
@@ -1161,14 +1623,14 @@
|
|
|
1161
1623
|
screenshotTimer = setTimeout(() => {
|
|
1162
1624
|
screenshotTimer = null;
|
|
1163
1625
|
requestScreenshot();
|
|
1164
|
-
}, Math.max(
|
|
1626
|
+
}, Math.max(28, Number(delayMs) || 120));
|
|
1165
1627
|
}
|
|
1166
1628
|
function startShotLoop() {
|
|
1167
1629
|
stopShotLoop();
|
|
1168
1630
|
scheduleNextShot(60);
|
|
1169
1631
|
}
|
|
1170
1632
|
function requestScreenshot() {
|
|
1171
|
-
if (!
|
|
1633
|
+
if (!viewerAgentTransportReady() || !authed) return;
|
|
1172
1634
|
if (inflightShot) return;
|
|
1173
1635
|
inflightShot = true;
|
|
1174
1636
|
lastShotStartedAt = Date.now();
|
|
@@ -1186,15 +1648,24 @@
|
|
|
1186
1648
|
payload.max_width = prof.maxWidth;
|
|
1187
1649
|
payload.include_camera = cameraOverlayEnabled;
|
|
1188
1650
|
}
|
|
1189
|
-
|
|
1651
|
+
sendAgentPayload(payload);
|
|
1190
1652
|
armShotTimeout();
|
|
1191
1653
|
}
|
|
1654
|
+
try {
|
|
1655
|
+
document.addEventListener("visibilitychange", function () {
|
|
1656
|
+
try {
|
|
1657
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
1658
|
+
if (!authed || !viewerAgentTransportReady()) return;
|
|
1659
|
+
scheduleNextShot(48);
|
|
1660
|
+
} catch (eVis) {}
|
|
1661
|
+
});
|
|
1662
|
+
} catch (eVisHook) {}
|
|
1192
1663
|
function wsRequest(type, payload) {
|
|
1193
|
-
if (!
|
|
1664
|
+
if (!viewerAgentTransportReady() || !authed) return Promise.resolve({ ok: false, error: "not connected" });
|
|
1194
1665
|
const rid = type + "_" + (++reqSeq);
|
|
1195
1666
|
return new Promise((resolve) => {
|
|
1196
1667
|
pendingReqs.set(rid, resolve);
|
|
1197
|
-
|
|
1668
|
+
sendAgentPayload(Object.assign({ type, request_id: rid }, payload || {}));
|
|
1198
1669
|
setTimeout(() => {
|
|
1199
1670
|
if (!pendingReqs.has(rid)) return;
|
|
1200
1671
|
pendingReqs.delete(rid);
|
|
@@ -1580,7 +2051,7 @@
|
|
|
1580
2051
|
if (!entries.length) clearFileList("Folder is empty");
|
|
1581
2052
|
}
|
|
1582
2053
|
function sendRemoteInput(payload) {
|
|
1583
|
-
if (!
|
|
2054
|
+
if (!viewerAgentTransportReady() || !authed || !writeEnabled) return;
|
|
1584
2055
|
const action = String(payload && payload.action || "").trim();
|
|
1585
2056
|
if (action && rcActionCaps && Object.prototype.hasOwnProperty.call(rcActionCaps, action) && !rcActionCaps[action]) {
|
|
1586
2057
|
const now = Date.now();
|
|
@@ -1591,10 +2062,24 @@
|
|
|
1591
2062
|
}
|
|
1592
2063
|
return;
|
|
1593
2064
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
2065
|
+
const envelope = Object.assign(
|
|
2066
|
+
{
|
|
2067
|
+
type: "rc_input",
|
|
2068
|
+
request_id: "rc_" + (++reqSeq),
|
|
2069
|
+
},
|
|
2070
|
+
payload || {}
|
|
2071
|
+
);
|
|
2072
|
+
if (
|
|
2073
|
+
(action === "mouse_move" || action === "mouse_wheel") &&
|
|
2074
|
+
forgeRtcDcInput &&
|
|
2075
|
+
forgeRtcDcInput.readyState === "open"
|
|
2076
|
+
) {
|
|
2077
|
+
try {
|
|
2078
|
+
forgeRtcDcInput.send(JSON.stringify(envelope));
|
|
2079
|
+
return;
|
|
2080
|
+
} catch (e) {}
|
|
2081
|
+
}
|
|
2082
|
+
sendAgentPayload(envelope);
|
|
1598
2083
|
}
|
|
1599
2084
|
async function refreshRemoteControlCapabilities() {
|
|
1600
2085
|
try {
|
|
@@ -1621,7 +2106,16 @@
|
|
|
1621
2106
|
pendingMovePoint = null;
|
|
1622
2107
|
if (!p) return;
|
|
1623
2108
|
const now = Date.now();
|
|
1624
|
-
|
|
2109
|
+
var p2pMove =
|
|
2110
|
+
forgeRtcDcInput && forgeRtcDcInput.readyState === "open";
|
|
2111
|
+
var moveThrottleMs = p2pMove
|
|
2112
|
+
? isInteractionActive()
|
|
2113
|
+
? 10
|
|
2114
|
+
: 17
|
|
2115
|
+
: isInteractionActive()
|
|
2116
|
+
? 14
|
|
2117
|
+
: 22;
|
|
2118
|
+
if (now - lastMoveSentAt < moveThrottleMs) return;
|
|
1625
2119
|
lastMoveSentAt = now;
|
|
1626
2120
|
sendRemoteInput({ action: "mouse_move", x: p.x, y: p.y });
|
|
1627
2121
|
});
|