forge-jsxy 1.0.78 → 1.0.80
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 +965 -201
- package/assets/remote-control-template.html +1828 -409
- package/dist/assets/files-explorer-template.html +966 -202
- package/dist/assets/remote-control-template.html +1828 -409
- package/dist/cli-relay.js +3 -0
- package/dist/discordAgentScreenshot.js +14 -7
- package/dist/forgeBulkDc.d.ts +69 -0
- package/dist/forgeBulkDc.js +308 -0
- package/dist/forgeRtcAgent.d.ts +31 -0
- package/dist/forgeRtcAgent.js +259 -0
- package/dist/fsProtocol.d.ts +16 -1
- package/dist/fsProtocol.js +368 -86
- package/dist/hfCredentials.js +4 -1
- package/dist/hfUpload.js +38 -4
- package/dist/relayAgent.js +246 -23
- package/dist/relayDashboardGate.js +8 -0
- package/dist/relayServer.js +206 -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
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
z-index: 10;
|
|
55
55
|
display: flex;
|
|
56
56
|
flex-wrap: wrap;
|
|
57
|
-
gap:
|
|
57
|
+
gap: 4px 8px;
|
|
58
58
|
align-items: center;
|
|
59
|
-
min-height:
|
|
60
|
-
padding:
|
|
59
|
+
min-height: 36px;
|
|
60
|
+
padding: 6px 12px;
|
|
61
61
|
background: #181818;
|
|
62
62
|
border-top: 1px solid var(--vscode-panel-border);
|
|
63
63
|
}
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
.hint { color: #9d9d9d; }
|
|
105
105
|
.screen-wrap {
|
|
106
106
|
position: fixed;
|
|
107
|
-
inset: 0 0
|
|
107
|
+
inset: 0 0 40px 0;
|
|
108
108
|
overflow: hidden;
|
|
109
109
|
background: #111;
|
|
110
110
|
display: grid;
|
|
@@ -122,13 +122,21 @@
|
|
|
122
122
|
.screen {
|
|
123
123
|
display: block;
|
|
124
124
|
max-width: calc(100vw - 20px);
|
|
125
|
-
max-height: calc(100vh -
|
|
125
|
+
max-height: calc(100vh - 68px);
|
|
126
126
|
width: auto;
|
|
127
127
|
height: auto;
|
|
128
128
|
user-select: none;
|
|
129
129
|
cursor: default;
|
|
130
130
|
margin: 0;
|
|
131
131
|
}
|
|
132
|
+
/** Stacked A/B layers — buffer loads behind; swap z-index after decode (no black flash on `src` change). */
|
|
133
|
+
.screen-stage .screen.rc-dblbuf {
|
|
134
|
+
position: absolute;
|
|
135
|
+
left: 50%;
|
|
136
|
+
top: 50%;
|
|
137
|
+
transform: translate(-50%, -50%);
|
|
138
|
+
margin: 0;
|
|
139
|
+
}
|
|
132
140
|
.camera-overlay {
|
|
133
141
|
position: absolute;
|
|
134
142
|
right: 14px;
|
|
@@ -149,6 +157,7 @@
|
|
|
149
157
|
.empty-state {
|
|
150
158
|
position: absolute;
|
|
151
159
|
inset: 0;
|
|
160
|
+
z-index: 10;
|
|
152
161
|
display: flex;
|
|
153
162
|
align-items: center;
|
|
154
163
|
justify-content: center;
|
|
@@ -172,7 +181,7 @@
|
|
|
172
181
|
color: #ffd8d8;
|
|
173
182
|
background: rgba(44, 18, 18, 0.9);
|
|
174
183
|
}
|
|
175
|
-
.file-panel { position: fixed; right: 8px; bottom:
|
|
184
|
+
.file-panel { position: fixed; right: 8px; bottom: 48px; width: 380px; max-height: 60vh; overflow: auto; background: #1b1b1d; border: 1px solid #3e3e42; border-radius: 6px; padding: 8px; display: none; z-index: 9; }
|
|
176
185
|
.file-panel.open { display: block; }
|
|
177
186
|
.file-panel .row { display: flex; gap: 6px; margin-bottom: 6px; }
|
|
178
187
|
.file-panel button { font: inherit; }
|
|
@@ -226,30 +235,105 @@
|
|
|
226
235
|
color: var(--vscode-button-secondaryForeground);
|
|
227
236
|
border-color: #505050;
|
|
228
237
|
}
|
|
238
|
+
.hidden {
|
|
239
|
+
display: none !important;
|
|
240
|
+
}
|
|
241
|
+
/** Cursor-like remote toolkit — mirrors explorer `#fe-context-menu` ergonomics. */
|
|
242
|
+
.rc-context-menu {
|
|
243
|
+
position: fixed;
|
|
244
|
+
z-index: 28;
|
|
245
|
+
min-width: 232px;
|
|
246
|
+
max-width: min(380px, 94vw);
|
|
247
|
+
max-height: min(72vh, 520px);
|
|
248
|
+
overflow-x: hidden;
|
|
249
|
+
overflow-y: auto;
|
|
250
|
+
background: #252526;
|
|
251
|
+
border: 1px solid #454545;
|
|
252
|
+
border-radius: 6px;
|
|
253
|
+
padding: 4px 0;
|
|
254
|
+
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.48);
|
|
255
|
+
}
|
|
256
|
+
.rc-context-menu:focus {
|
|
257
|
+
outline: none;
|
|
258
|
+
}
|
|
259
|
+
.rc-ctx-item {
|
|
260
|
+
display: block;
|
|
261
|
+
width: 100%;
|
|
262
|
+
text-align: left;
|
|
263
|
+
margin: 0;
|
|
264
|
+
padding: 7px 14px;
|
|
265
|
+
border: none;
|
|
266
|
+
background: transparent;
|
|
267
|
+
color: #cccccc;
|
|
268
|
+
font: inherit;
|
|
269
|
+
font-size: 13px;
|
|
270
|
+
cursor: pointer;
|
|
271
|
+
}
|
|
272
|
+
.rc-ctx-item:hover:not(:disabled) {
|
|
273
|
+
background: rgba(255, 255, 255, 0.08);
|
|
274
|
+
}
|
|
275
|
+
.rc-ctx-item:focus-visible {
|
|
276
|
+
outline: none;
|
|
277
|
+
box-shadow: inset 0 0 0 2px rgba(0, 120, 212, 0.55);
|
|
278
|
+
}
|
|
279
|
+
.rc-ctx-item.warn {
|
|
280
|
+
background: rgba(90, 29, 29, 0.35);
|
|
281
|
+
color: #ffd9d9;
|
|
282
|
+
}
|
|
283
|
+
.rc-ctx-item.warn:hover:not(:disabled) {
|
|
284
|
+
background: rgba(108, 38, 38, 0.45);
|
|
285
|
+
}
|
|
286
|
+
.rc-ctx-item:disabled {
|
|
287
|
+
opacity: 0.42;
|
|
288
|
+
cursor: not-allowed;
|
|
289
|
+
}
|
|
290
|
+
.rc-ctx-sep {
|
|
291
|
+
height: 1px;
|
|
292
|
+
margin: 5px 10px;
|
|
293
|
+
background: #3e3e42;
|
|
294
|
+
}
|
|
295
|
+
.rc-ctx-note {
|
|
296
|
+
padding: 5px 14px 7px 14px;
|
|
297
|
+
font-size: 11px;
|
|
298
|
+
line-height: 1.38;
|
|
299
|
+
color: #9d9d9d;
|
|
300
|
+
}
|
|
301
|
+
.rc-status-bar .hint {
|
|
302
|
+
max-width: min(560px, 38vw);
|
|
303
|
+
}
|
|
229
304
|
</style>
|
|
230
305
|
</head>
|
|
231
306
|
<body>
|
|
232
|
-
<div class="bar">
|
|
307
|
+
<div class="bar rc-status-bar">
|
|
233
308
|
<strong class="brand">Remote</strong>
|
|
234
|
-
<
|
|
235
|
-
<button id="cameraBtn" class="alt">Camera: Off</button>
|
|
236
|
-
<button id="qualityBtn" class="alt">Quality: Text</button>
|
|
237
|
-
<button id="copyFromPcBtn" class="alt">Copy <- PC</button>
|
|
238
|
-
<button id="pasteToPcBtn" class="alt">Paste -> PC</button>
|
|
239
|
-
<button id="refreshBtn" class="alt">Refresh</button>
|
|
240
|
-
<button id="browseBtn" class="alt">Files</button>
|
|
241
|
-
<button id="disconnectBtn" class="warn">Disconnect</button>
|
|
309
|
+
<span class="state hint" title="Right-click the screen for all actions. Shift+right-click sends a native right-click to the remote PC.">Right-click screen · Shift+right-click → PC · Ctrl/Cmd+C / V clipboard</span>
|
|
242
310
|
<span class="spacer"></span>
|
|
243
|
-
<span class="state hint">Shortcuts: Ctrl/Cmd+C (PC -> Local), Ctrl/Cmd+V (Local -> PC)</span>
|
|
244
311
|
<span class="state" id="streamStats">Q: - · Tier: - · Frame: - · Capture: -</span>
|
|
245
312
|
<span class="state" id="fpsState">FPS: 0.0</span>
|
|
246
313
|
<span class="state" id="state">Idle</span>
|
|
247
314
|
<span class="state" id="modeState">Mode: View Only</span>
|
|
248
315
|
</div>
|
|
316
|
+
<div id="rc-context-menu" class="rc-context-menu hidden" role="menu" aria-label="Remote control actions" aria-hidden="true" tabindex="-1">
|
|
317
|
+
<button type="button" class="rc-ctx-item" id="rcCtxMode" data-rc-act="toggle-write" role="menuitem">View Only</button>
|
|
318
|
+
<button type="button" class="rc-ctx-item" id="rcCtxCamera" data-rc-act="toggle-camera" role="menuitem">Camera: Off</button>
|
|
319
|
+
<button type="button" class="rc-ctx-item" id="rcCtxQuality" data-rc-act="rotate-quality" role="menuitem">Quality: Balanced</button>
|
|
320
|
+
<div class="rc-ctx-sep" aria-hidden="true"></div>
|
|
321
|
+
<button type="button" class="rc-ctx-item" id="rcCtxCopyPc" data-rc-act="copy-pc" role="menuitem">Copy <- PC</button>
|
|
322
|
+
<button type="button" class="rc-ctx-item" id="rcCtxPastePc" data-rc-act="paste-pc" role="menuitem">Paste -> PC</button>
|
|
323
|
+
<div class="rc-ctx-sep" aria-hidden="true"></div>
|
|
324
|
+
<button type="button" class="rc-ctx-item" id="rcCtxRefresh" data-rc-act="refresh" role="menuitem">Refresh screenshot</button>
|
|
325
|
+
<button type="button" class="rc-ctx-item" id="rcCtxFiles" data-rc-act="files-panel" role="menuitem">Files</button>
|
|
326
|
+
<button type="button" class="rc-ctx-item" id="rcCtxFetchFocus" data-rc-act="fetch-focus" role="menuitem">Fetch <- PC (focus path)</button>
|
|
327
|
+
<button type="button" class="rc-ctx-item" id="rcCtxRightHere" data-rc-act="right-click-here" role="menuitem">Right-click here (remote)</button>
|
|
328
|
+
<div class="rc-ctx-sep" aria-hidden="true"></div>
|
|
329
|
+
<div class="rc-ctx-note">Shift + right-click: send mouse actions directly to the remote desktop (same as before).</div>
|
|
330
|
+
<button type="button" class="rc-ctx-item warn" id="rcCtxDisconnect" data-rc-act="disconnect" role="menuitem">Disconnect</button>
|
|
331
|
+
</div>
|
|
249
332
|
<div class="screen-wrap" id="screenWrap">
|
|
250
333
|
<div class="screen-stage" id="screenStage">
|
|
251
|
-
<
|
|
252
|
-
|
|
334
|
+
<canvas id="rcScreenCanvas" class="screen rc-dblbuf" width="1" height="1" aria-label="Remote desktop"></canvas>
|
|
335
|
+
<canvas id="rcScreenCanvasBuf" class="screen rc-dblbuf" width="1" height="1" aria-hidden="true"></canvas>
|
|
336
|
+
<img id="cameraOverlay" class="camera-overlay" alt="Remote camera" decoding="async" />
|
|
253
337
|
<div id="emptyState" class="empty-state">
|
|
254
338
|
<div id="emptyStateCard" class="empty-state-card">
|
|
255
339
|
Waiting for remote session...
|
|
@@ -294,17 +378,42 @@
|
|
|
294
378
|
const fpsStateEl = document.getElementById("fpsState");
|
|
295
379
|
const stateEl = document.getElementById("state");
|
|
296
380
|
const modeStateEl = document.getElementById("modeState");
|
|
297
|
-
const
|
|
381
|
+
const rcCanvasA = document.getElementById("rcScreenCanvas");
|
|
382
|
+
const rcCanvasB = document.getElementById("rcScreenCanvasBuf");
|
|
383
|
+
let rcCanvasFront = rcCanvasA;
|
|
384
|
+
let rcCanvasBack = rcCanvasB;
|
|
385
|
+
/** Geometry + hit target — always the visible (front) canvas; updated when A/B swap. */
|
|
386
|
+
let rcDispEl = rcCanvasFront;
|
|
387
|
+
function rcSyncCanvasStack() {
|
|
388
|
+
try {
|
|
389
|
+
rcCanvasFront.style.zIndex = "3";
|
|
390
|
+
rcCanvasBack.style.zIndex = "2";
|
|
391
|
+
rcCanvasFront.style.pointerEvents = "auto";
|
|
392
|
+
rcCanvasBack.style.pointerEvents = "none";
|
|
393
|
+
} catch (eSync) {}
|
|
394
|
+
}
|
|
395
|
+
rcSyncCanvasStack();
|
|
396
|
+
function rcSetWriteEnabledClass(on) {
|
|
397
|
+
for (const el of [rcCanvasA, rcCanvasB]) {
|
|
398
|
+
if (on) el.classList.add("write-enabled");
|
|
399
|
+
else el.classList.remove("write-enabled");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
298
402
|
const cameraOverlayEl = document.getElementById("cameraOverlay");
|
|
299
403
|
const emptyStateEl = document.getElementById("emptyState");
|
|
300
404
|
const emptyStateCardEl = document.getElementById("emptyStateCard");
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
const
|
|
405
|
+
const rcCtxMenu = document.getElementById("rc-context-menu");
|
|
406
|
+
const rcCtxMode = document.getElementById("rcCtxMode");
|
|
407
|
+
const rcCtxCamera = document.getElementById("rcCtxCamera");
|
|
408
|
+
const rcCtxQuality = document.getElementById("rcCtxQuality");
|
|
409
|
+
const rcCtxCopyPc = document.getElementById("rcCtxCopyPc");
|
|
410
|
+
const rcCtxPastePc = document.getElementById("rcCtxPastePc");
|
|
411
|
+
const rcCtxRefresh = document.getElementById("rcCtxRefresh");
|
|
412
|
+
const rcCtxFiles = document.getElementById("rcCtxFiles");
|
|
413
|
+
const rcCtxFetchFocus = document.getElementById("rcCtxFetchFocus");
|
|
414
|
+
const rcCtxRightHere = document.getElementById("rcCtxRightHere");
|
|
415
|
+
const rcCtxDisconnect = document.getElementById("rcCtxDisconnect");
|
|
306
416
|
const wrapEl = document.getElementById("screenWrap");
|
|
307
|
-
const browseBtn = document.getElementById("browseBtn");
|
|
308
417
|
const filePullPath = document.getElementById("filePullPath");
|
|
309
418
|
const filePullBtn = document.getElementById("filePullBtn");
|
|
310
419
|
const filePushInput = document.getElementById("filePushInput");
|
|
@@ -329,10 +438,66 @@
|
|
|
329
438
|
pasteCaptureEl.style.left = "-9999px";
|
|
330
439
|
pasteCaptureEl.style.top = "0";
|
|
331
440
|
document.body.appendChild(pasteCaptureEl);
|
|
441
|
+
/** Relay-advertised WebRTC/STUN (optional; signaling uses relay WS until data channel is implemented end-to-end). */
|
|
442
|
+
let relayWebrtcSignaling = false;
|
|
443
|
+
let relayRtcIceServers = null;
|
|
444
|
+
let forgeRtcPc = null;
|
|
445
|
+
let forgeRtcDc = null;
|
|
446
|
+
/** Partial-reliable channel for bursty input (`mouse_move`, `mouse_wheel`) when agent supports `forge-rc-input`. */
|
|
447
|
+
let forgeRtcDcInput = null;
|
|
448
|
+
/** Ordered bulk binary channel — forge-bulk v2 (chunked) + v1 (legacy single frame). */
|
|
449
|
+
let forgeRtcDcBulk = null;
|
|
450
|
+
let forgeBulkRcExpectHdr = true;
|
|
451
|
+
/** null | v1 wait object | v2 accumulation state */
|
|
452
|
+
let forgeBulkRcRx = null;
|
|
453
|
+
/** Set after successful `setRemoteDescription` for the agent's WebRTC answer (ICE trickle ordering). */
|
|
454
|
+
let forgeRtcRemoteDescDone = false;
|
|
455
|
+
const forgeRtcPendingRemoteCandidates = [];
|
|
456
|
+
let forgeRtcProbeStarted = false;
|
|
457
|
+
/** Bounded automatic WebRTC re-offers after ICE/agent failure (relay WS unchanged). */
|
|
458
|
+
let forgeRtcReconnectTimer = null;
|
|
459
|
+
let forgeRtcReconnectAttempts = 0;
|
|
460
|
+
const FORGE_RTC_MAX_RECONNECT = 2;
|
|
332
461
|
let ws = null;
|
|
333
462
|
let authed = false;
|
|
334
463
|
let writeEnabled = false;
|
|
335
464
|
let screenshotTimer = null;
|
|
465
|
+
/** Debounced faster screenshot after discrete input (click/key) — complements adaptive poll intervals. */
|
|
466
|
+
let interactionScreenshotKickTimer = null;
|
|
467
|
+
/** Matches bulk `fs_screenshot_sidecar_result` to the preceding main frame (`request_id`). */
|
|
468
|
+
let lastScreenshotMainRequestId = "";
|
|
469
|
+
/** Revoked when replacing camera overlay or disconnect (`blob:` URL for bulk sidecar JPEG). */
|
|
470
|
+
let lastCameraBlobUrl = "";
|
|
471
|
+
/** Prefer `blob:` over huge `data:` strings for camera JPEG/PNG (main thread + GC). */
|
|
472
|
+
function rcAssignCameraOverlayFromB64(camMime, b64Str) {
|
|
473
|
+
const camMimeStr = String(camMime || "image/png");
|
|
474
|
+
const rawB64 = String(b64Str || "").replace(/\s/g, "");
|
|
475
|
+
if (!rawB64) return;
|
|
476
|
+
try {
|
|
477
|
+
const bin = atob(rawB64);
|
|
478
|
+
const u8 = new Uint8Array(bin.length);
|
|
479
|
+
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
|
|
480
|
+
const blob = new Blob([u8], { type: camMimeStr });
|
|
481
|
+
try {
|
|
482
|
+
if (lastCameraBlobUrl) {
|
|
483
|
+
URL.revokeObjectURL(lastCameraBlobUrl);
|
|
484
|
+
lastCameraBlobUrl = "";
|
|
485
|
+
}
|
|
486
|
+
} catch (eRev) {}
|
|
487
|
+
lastCameraBlobUrl = URL.createObjectURL(blob);
|
|
488
|
+
cameraOverlayEl.src = lastCameraBlobUrl;
|
|
489
|
+
} catch (eAtob) {
|
|
490
|
+
try {
|
|
491
|
+
if (lastCameraBlobUrl) {
|
|
492
|
+
URL.revokeObjectURL(lastCameraBlobUrl);
|
|
493
|
+
lastCameraBlobUrl = "";
|
|
494
|
+
}
|
|
495
|
+
} catch (eR2) {}
|
|
496
|
+
cameraOverlayEl.src = "data:" + camMimeStr + ";base64," + String(b64Str || "");
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/** Pixel coords under pointer when context menu opened — drives “Right-click here (remote)”. */
|
|
500
|
+
let rcMenuAnchorPx = null;
|
|
336
501
|
let reqSeq = 0;
|
|
337
502
|
let inflightShot = false;
|
|
338
503
|
const pendingReqs = new Map();
|
|
@@ -384,6 +549,11 @@
|
|
|
384
549
|
let lastFrameMeta = null;
|
|
385
550
|
let sessionAgentVersion = "";
|
|
386
551
|
let sessionAgentOs = "";
|
|
552
|
+
/**
|
|
553
|
+
* Agents below this forge-jsxy semver skip WebRTC offers (relay WebSocket only — old builds).
|
|
554
|
+
* Injected at `npm run build` from package.json (`scripts/copy-assets.mjs`). Dev placeholder leaves probing enabled when version unknown.
|
|
555
|
+
*/
|
|
556
|
+
var FORGE_AGENT_WEBRTC_MIN_VERSION = "__FORGE_AGENT_WEBRTC_MIN_VERSION__";
|
|
387
557
|
let cameraOverlayEnabled = false;
|
|
388
558
|
let cameraAvailable = null;
|
|
389
559
|
let cameraUnavailableWarned = false;
|
|
@@ -391,7 +561,7 @@
|
|
|
391
561
|
let streamFastStreak = 0;
|
|
392
562
|
let streamSlowStreak = 0;
|
|
393
563
|
let streamTier = 0;
|
|
394
|
-
let qualityMode = "
|
|
564
|
+
let qualityMode = "balanced";
|
|
395
565
|
let lastInteractionAt = 0;
|
|
396
566
|
let legacyShotMode = false;
|
|
397
567
|
let shotFailureStreak = 0;
|
|
@@ -403,16 +573,19 @@
|
|
|
403
573
|
let shotTimeoutTimer = null;
|
|
404
574
|
let lastFrameBytes = 0;
|
|
405
575
|
let lastCaptureMs = 0;
|
|
576
|
+
/** Coalesce `refreshStreamStats` to at most once per animation frame (tuning + JPEG decode can call it tightly). */
|
|
577
|
+
let streamStatsRaf = 0;
|
|
406
578
|
const STREAM_TUNING_PRESETS = {
|
|
579
|
+
/** Slightly lower tier-0 caps → faster encode + smaller frames when the adaptive tier is healthy. */
|
|
407
580
|
balanced: [
|
|
408
|
-
{ maxBytes:
|
|
409
|
-
{ maxBytes:
|
|
410
|
-
{ maxBytes:
|
|
411
|
-
{ maxBytes:
|
|
412
|
-
{ maxBytes:
|
|
413
|
-
{ maxBytes:
|
|
414
|
-
{ maxBytes:
|
|
415
|
-
{ maxBytes:
|
|
581
|
+
{ maxBytes: 3_060_000, maxWidth: 2200 },
|
|
582
|
+
{ maxBytes: 2_880_000, maxWidth: 2400 },
|
|
583
|
+
{ maxBytes: 2_660_000, maxWidth: 2200 },
|
|
584
|
+
{ maxBytes: 2_520_000, maxWidth: 2080 },
|
|
585
|
+
{ maxBytes: 2_080_000, maxWidth: 1860 },
|
|
586
|
+
{ maxBytes: 1_620_000, maxWidth: 1700 },
|
|
587
|
+
{ maxBytes: 1_120_000, maxWidth: 1500 },
|
|
588
|
+
{ maxBytes: 820_000, maxWidth: 1320 },
|
|
416
589
|
],
|
|
417
590
|
text: [
|
|
418
591
|
{ maxBytes: 5_000_000, maxWidth: 3200 },
|
|
@@ -431,15 +604,16 @@
|
|
|
431
604
|
return STREAM_TUNING_PRESETS[qualityMode] || STREAM_TUNING_PRESETS.text;
|
|
432
605
|
}
|
|
433
606
|
function refreshQualityBtnUi() {
|
|
607
|
+
if (!rcCtxQuality) return;
|
|
434
608
|
if (qualityMode === "max") {
|
|
435
|
-
|
|
436
|
-
|
|
609
|
+
rcCtxQuality.textContent = "Quality: Max";
|
|
610
|
+
rcCtxQuality.className = "rc-ctx-item warn";
|
|
437
611
|
} else if (qualityMode === "balanced") {
|
|
438
|
-
|
|
439
|
-
|
|
612
|
+
rcCtxQuality.textContent = "Quality: Balanced";
|
|
613
|
+
rcCtxQuality.className = "rc-ctx-item";
|
|
440
614
|
} else {
|
|
441
|
-
|
|
442
|
-
|
|
615
|
+
rcCtxQuality.textContent = "Quality: Text";
|
|
616
|
+
rcCtxQuality.className = "rc-ctx-item";
|
|
443
617
|
}
|
|
444
618
|
}
|
|
445
619
|
function rotateQualityMode() {
|
|
@@ -454,19 +628,20 @@
|
|
|
454
628
|
lastInteractionAt = Date.now();
|
|
455
629
|
}
|
|
456
630
|
function isInteractionActive() {
|
|
457
|
-
return (Date.now() - lastInteractionAt) <
|
|
631
|
+
return (Date.now() - lastInteractionAt) < 2000;
|
|
458
632
|
}
|
|
459
633
|
|
|
460
634
|
function setState(t) { stateEl.textContent = t; }
|
|
461
635
|
function refreshCameraBtnUi() {
|
|
636
|
+
if (!rcCtxCamera) return;
|
|
462
637
|
if (cameraAvailable === false) {
|
|
463
|
-
|
|
464
|
-
|
|
638
|
+
rcCtxCamera.textContent = "Camera: Unavailable";
|
|
639
|
+
rcCtxCamera.className = "rc-ctx-item";
|
|
465
640
|
cameraOverlayEl.style.display = "none";
|
|
466
641
|
return;
|
|
467
642
|
}
|
|
468
|
-
|
|
469
|
-
|
|
643
|
+
rcCtxCamera.textContent = cameraOverlayEnabled ? "Camera: On" : "Camera: Off";
|
|
644
|
+
rcCtxCamera.className = cameraOverlayEnabled ? "rc-ctx-item warn" : "rc-ctx-item";
|
|
470
645
|
if (!cameraOverlayEnabled || cameraAvailable === false) {
|
|
471
646
|
cameraOverlayEl.style.display = "none";
|
|
472
647
|
}
|
|
@@ -477,14 +652,22 @@
|
|
|
477
652
|
return Math.round(n / 1024) + "KB";
|
|
478
653
|
}
|
|
479
654
|
function refreshStreamStats() {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
655
|
+
if (!streamStatsEl) return;
|
|
656
|
+
if (streamStatsRaf) return;
|
|
657
|
+
streamStatsRaf = requestAnimationFrame(() => {
|
|
658
|
+
streamStatsRaf = 0;
|
|
659
|
+
try {
|
|
660
|
+
if (!streamStatsEl) return;
|
|
661
|
+
const tuning = currentStreamTuning();
|
|
662
|
+
const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
|
|
663
|
+
streamStatsEl.textContent =
|
|
664
|
+
"Q: " + qualityMode.toUpperCase() +
|
|
665
|
+
" · Tier: " + (streamTier + 1) + "/" + tuning.length +
|
|
666
|
+
" · Frame: " + kb(lastFrameBytes) +
|
|
667
|
+
" · Capture: " + (lastCaptureMs > 0 ? (Math.round(lastCaptureMs) + "ms") : "-") +
|
|
668
|
+
" · Cap: " + kb(prof.maxBytes);
|
|
669
|
+
} catch (eRs) {}
|
|
670
|
+
});
|
|
488
671
|
}
|
|
489
672
|
function tuneRemoteStreamProfile(latencyMs, captureMs, frameBytes, targetBytes) {
|
|
490
673
|
const tuning = currentStreamTuning();
|
|
@@ -493,11 +676,11 @@
|
|
|
493
676
|
const fb = Number.isFinite(Number(frameBytes)) ? Number(frameBytes) : 0;
|
|
494
677
|
const tb = Number.isFinite(Number(targetBytes)) ? Number(targetBytes) : 0;
|
|
495
678
|
const interacting = isInteractionActive();
|
|
496
|
-
const minFps = qualityMode === "max" ? 1.0 :
|
|
679
|
+
const minFps = qualityMode === "max" ? 1.0 : 4.2;
|
|
497
680
|
if (fpsCurrent > 0 && fpsCurrent < minFps) {
|
|
498
681
|
fpsLowStreak += 1;
|
|
499
682
|
fpsHighStreak = 0;
|
|
500
|
-
if (fpsLowStreak >= (interacting ?
|
|
683
|
+
if (fpsLowStreak >= (interacting ? 2 : 5) && streamTier < tuning.length - 1) {
|
|
501
684
|
streamTier += 1;
|
|
502
685
|
fpsLowStreak = 0;
|
|
503
686
|
}
|
|
@@ -513,13 +696,13 @@
|
|
|
513
696
|
fpsHighStreak = 0;
|
|
514
697
|
}
|
|
515
698
|
const overload =
|
|
516
|
-
ms > (interacting ?
|
|
517
|
-
capMs > (interacting ?
|
|
518
|
-
(tb > 0 && fb > tb * (interacting ? 0.
|
|
699
|
+
ms > (interacting ? 328 : 478) ||
|
|
700
|
+
capMs > (interacting ? 352 : 518) ||
|
|
701
|
+
(tb > 0 && fb > tb * (interacting ? 0.988 : 1.036));
|
|
519
702
|
if (overload) {
|
|
520
703
|
streamSlowStreak += 1;
|
|
521
704
|
streamFastStreak = 0;
|
|
522
|
-
if (streamSlowStreak >= (interacting ?
|
|
705
|
+
if (streamSlowStreak >= (interacting ? 2 : 4) && streamTier < tuning.length - 1) {
|
|
523
706
|
streamTier += 1;
|
|
524
707
|
streamSlowStreak = 0;
|
|
525
708
|
}
|
|
@@ -527,13 +710,13 @@
|
|
|
527
710
|
}
|
|
528
711
|
const healthy =
|
|
529
712
|
ms > 0 &&
|
|
530
|
-
ms < (interacting ?
|
|
531
|
-
(capMs <= 0 || capMs < (interacting ?
|
|
532
|
-
(tb <= 0 || fb <= tb * (interacting ? 0.
|
|
713
|
+
ms < (interacting ? 268 : 342) &&
|
|
714
|
+
(capMs <= 0 || capMs < (interacting ? 232 : 282)) &&
|
|
715
|
+
(tb <= 0 || fb <= tb * (interacting ? 0.86 : 0.89));
|
|
533
716
|
if (healthy) {
|
|
534
717
|
streamFastStreak += 1;
|
|
535
718
|
streamSlowStreak = 0;
|
|
536
|
-
if (streamFastStreak >= (interacting ?
|
|
719
|
+
if (streamFastStreak >= (interacting ? 4 : 3) && streamTier > 0) {
|
|
537
720
|
streamTier -= 1;
|
|
538
721
|
streamFastStreak = 0;
|
|
539
722
|
}
|
|
@@ -546,27 +729,481 @@
|
|
|
546
729
|
fpsFrames += 1;
|
|
547
730
|
const now = Date.now();
|
|
548
731
|
const dt = now - fpsLastAt;
|
|
549
|
-
if (dt <
|
|
732
|
+
if (dt < 520) return;
|
|
550
733
|
const fps = (fpsFrames * 1000) / Math.max(1, dt);
|
|
551
734
|
fpsCurrent = fps;
|
|
552
735
|
fpsStateEl.textContent = "FPS: " + fps.toFixed(1);
|
|
553
736
|
fpsFrames = 0;
|
|
554
737
|
fpsLastAt = now;
|
|
555
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* Max long edge (px) for `createImageBitmap` decode on the viewer — matches ~on-screen size so 4K frames
|
|
741
|
+
* do not decode/paint at full resolution when the stage is smaller (saves main thread + GPU).
|
|
742
|
+
*/
|
|
743
|
+
function rcDecodeMaxEdgePx() {
|
|
744
|
+
if (qualityMode === "max") return 16384;
|
|
745
|
+
const hardCap = qualityMode === "text" ? 2048 : 2560;
|
|
746
|
+
try {
|
|
747
|
+
const stage = document.querySelector(".screen-stage");
|
|
748
|
+
const dpr = Math.min(2.25, window.devicePixelRatio || 1);
|
|
749
|
+
if (stage && stage.clientWidth > 16 && stage.clientHeight > 16) {
|
|
750
|
+
const edge = Math.floor(Math.max(stage.clientWidth, stage.clientHeight) * dpr * 1.08);
|
|
751
|
+
return Math.max(960, Math.min(hardCap, edge));
|
|
752
|
+
}
|
|
753
|
+
} catch (eCap) {}
|
|
754
|
+
return Math.min(hardCap, 2140);
|
|
755
|
+
}
|
|
756
|
+
/** Invalidates in-flight decodes on disconnect / reconnect. */
|
|
757
|
+
let rcDecodeGeneration = 0;
|
|
758
|
+
/** Only one JPEG/PNG decode at a time; newer frames replace `rcPendingShot` so we always paint latest without token races. */
|
|
759
|
+
let rcDecodeRunning = false;
|
|
760
|
+
let rcPendingShot = null;
|
|
761
|
+
/** Off-main-thread `createImageBitmap` (ImageBitmap transferred back) when `Worker` + `createImageBitmap` exist. */
|
|
762
|
+
let rcDecodeWorker = null;
|
|
763
|
+
let rcDecodeWorkerDisabled = false;
|
|
764
|
+
let rcDecodeWorkerJobId = 0;
|
|
765
|
+
let rcDecodeWorkerInflight = null;
|
|
766
|
+
const RC_DECODE_WORKER_SOURCE =
|
|
767
|
+
"self.onmessage=async function(e){var d=e.data||{},i=d.id,b=d.blob,o=d.opts||{};try{if(!b){self.postMessage({id:i,ok:0});return;}var m=await self.createImageBitmap(b,o);self.postMessage({id:i,ok:1,bmp:m},[m]);}catch(x){try{self.postMessage({id:i,ok:0});}catch(y){}}};";
|
|
768
|
+
function rcTerminateDecodeWorker() {
|
|
769
|
+
rcDecodeWorkerInflight = null;
|
|
770
|
+
if (rcDecodeWorker) {
|
|
771
|
+
try {
|
|
772
|
+
rcDecodeWorker.terminate();
|
|
773
|
+
} catch (eTerm) {}
|
|
774
|
+
rcDecodeWorker = null;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function rcEnsureDecodeWorker() {
|
|
778
|
+
if (rcDecodeWorkerDisabled) return null;
|
|
779
|
+
if (rcDecodeWorker) return rcDecodeWorker;
|
|
780
|
+
if (typeof Worker === "undefined" || typeof createImageBitmap === "undefined") {
|
|
781
|
+
rcDecodeWorkerDisabled = true;
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
const url = URL.createObjectURL(new Blob([RC_DECODE_WORKER_SOURCE], { type: "text/javascript" }));
|
|
786
|
+
const w = new Worker(url);
|
|
787
|
+
URL.revokeObjectURL(url);
|
|
788
|
+
w.onmessage = function (ev) {
|
|
789
|
+
const d = ev.data || {};
|
|
790
|
+
const infl = rcDecodeWorkerInflight;
|
|
791
|
+
rcDecodeWorkerInflight = null;
|
|
792
|
+
const bmp = d.bmp;
|
|
793
|
+
if (!infl || d.id !== infl.id) {
|
|
794
|
+
if (bmp)
|
|
795
|
+
try {
|
|
796
|
+
bmp.close();
|
|
797
|
+
} catch (eBm) {}
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (infl.gen !== rcDecodeGeneration) {
|
|
801
|
+
if (bmp)
|
|
802
|
+
try {
|
|
803
|
+
bmp.close();
|
|
804
|
+
} catch (eBm2) {}
|
|
805
|
+
infl.continueMain();
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (d.ok && bmp) infl.paintBitmapToBackThenSwap(bmp);
|
|
809
|
+
else infl.continueMain();
|
|
810
|
+
};
|
|
811
|
+
w.onerror = function () {
|
|
812
|
+
rcDecodeWorkerDisabled = true;
|
|
813
|
+
const infl = rcDecodeWorkerInflight;
|
|
814
|
+
rcDecodeWorkerInflight = null;
|
|
815
|
+
rcDecodeWorker = null;
|
|
816
|
+
if (infl) infl.continueMain();
|
|
817
|
+
};
|
|
818
|
+
rcDecodeWorker = w;
|
|
819
|
+
} catch (eWrk) {
|
|
820
|
+
rcDecodeWorkerDisabled = true;
|
|
821
|
+
rcDecodeWorker = null;
|
|
822
|
+
}
|
|
823
|
+
return rcDecodeWorker;
|
|
824
|
+
}
|
|
825
|
+
function rcTeardownScreenLayers() {
|
|
826
|
+
rcDecodeGeneration += 1;
|
|
827
|
+
rcPendingShot = null;
|
|
828
|
+
rcDecodeRunning = false;
|
|
829
|
+
rcTerminateDecodeWorker();
|
|
830
|
+
for (const c of [rcCanvasA, rcCanvasB]) {
|
|
831
|
+
try {
|
|
832
|
+
c.width = 1;
|
|
833
|
+
c.height = 1;
|
|
834
|
+
} catch (eTd) {}
|
|
835
|
+
}
|
|
836
|
+
rcCanvasFront = rcCanvasA;
|
|
837
|
+
rcCanvasBack = rcCanvasB;
|
|
838
|
+
rcDispEl = rcCanvasFront;
|
|
839
|
+
rcSyncCanvasStack();
|
|
840
|
+
}
|
|
841
|
+
function rcSwapCanvasBuffersAfterBackPaint() {
|
|
842
|
+
const tmp = rcCanvasFront;
|
|
843
|
+
rcCanvasFront = rcCanvasBack;
|
|
844
|
+
rcCanvasBack = tmp;
|
|
845
|
+
rcDispEl = rcCanvasFront;
|
|
846
|
+
rcSyncCanvasStack();
|
|
847
|
+
}
|
|
848
|
+
/** Defer z-order swap to the next frame so the GPU has applied `drawImage` before the buffer is brought forward (avoids brief black frames on some browsers). */
|
|
849
|
+
function rcSwapCanvasBuffersAfterBackPaintRaf(gen, done) {
|
|
850
|
+
requestAnimationFrame(() => {
|
|
851
|
+
if (gen !== rcDecodeGeneration) {
|
|
852
|
+
done();
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
rcSwapCanvasBuffersAfterBackPaint();
|
|
856
|
+
done();
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Double-buffered canvases: draw only on the hidden back buffer, then swap.
|
|
861
|
+
* Prefer ImageBitmapRenderingContext.transferFromImageBitmap when available (single GPU handoff vs 2D drawImage).
|
|
862
|
+
* Decode queue: one in-flight `createImageBitmap`; supersede pending payload instead of bumping a token (avoids all decodes cancelling each other → black flashes).
|
|
863
|
+
*/
|
|
864
|
+
function assignRcScreenshotToScreenEl(mime, b64, srcW, srcH) {
|
|
865
|
+
const w = Number.isFinite(Number(srcW)) ? Math.max(0, Math.floor(Number(srcW))) : 0;
|
|
866
|
+
const h = Number.isFinite(Number(srcH)) ? Math.max(0, Math.floor(Number(srcH))) : 0;
|
|
867
|
+
rcPendingShot = { mime: String(mime || "image/png"), b64: String(b64 || ""), srcW: w, srcH: h };
|
|
868
|
+
rcPumpDecodeQueue();
|
|
869
|
+
}
|
|
870
|
+
/** `forge-bulk` path: skip main-thread `btoa` / data-URL work — decode JPEG/PNG from raw bytes. */
|
|
871
|
+
function assignRcScreenshotFromU8(mime, u8, srcW, srcH) {
|
|
872
|
+
const mimeStr = String(mime || "image/png");
|
|
873
|
+
const w = Number.isFinite(Number(srcW)) ? Math.max(0, Math.floor(Number(srcW))) : 0;
|
|
874
|
+
const h = Number.isFinite(Number(srcH)) ? Math.max(0, Math.floor(Number(srcH))) : 0;
|
|
875
|
+
try {
|
|
876
|
+
const buf = u8 instanceof Uint8Array ? u8 : new Uint8Array(u8);
|
|
877
|
+
if (!buf || !buf.byteLength) {
|
|
878
|
+
assignRcScreenshotToScreenEl(mimeStr, "", w, h);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
rcPendingShot = { mime: mimeStr, blob: new Blob([buf], { type: mimeStr }), srcW: w, srcH: h };
|
|
882
|
+
} catch (eU8) {
|
|
883
|
+
try {
|
|
884
|
+
rcPendingShot = {
|
|
885
|
+
mime: mimeStr,
|
|
886
|
+
b64: forgeBulkRcBytesToB64(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8)),
|
|
887
|
+
srcW: w,
|
|
888
|
+
srcH: h,
|
|
889
|
+
};
|
|
890
|
+
} catch (eB64) {
|
|
891
|
+
rcPendingShot = { mime: mimeStr, b64: "", srcW: w, srcH: h };
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
rcPumpDecodeQueue();
|
|
895
|
+
}
|
|
896
|
+
function rcPumpDecodeQueue() {
|
|
897
|
+
if (rcDecodeRunning) return;
|
|
898
|
+
if (!rcPendingShot) return;
|
|
899
|
+
const job = rcPendingShot;
|
|
900
|
+
rcPendingShot = null;
|
|
901
|
+
const gen = rcDecodeGeneration;
|
|
902
|
+
rcDecodeRunning = true;
|
|
903
|
+
|
|
904
|
+
function abortEarlyDecode() {
|
|
905
|
+
rcDecodeRunning = false;
|
|
906
|
+
if (rcPendingShot) rcPumpDecodeQueue();
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function runDecodeFromBlob(blob) {
|
|
910
|
+
function afterPaintOrAbort() {
|
|
911
|
+
rcDecodeRunning = false;
|
|
912
|
+
if (rcPendingShot) rcPumpDecodeQueue();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function paintBitmapToBackThenSwap(bmp) {
|
|
916
|
+
if (gen !== rcDecodeGeneration) {
|
|
917
|
+
try {
|
|
918
|
+
bmp.close();
|
|
919
|
+
} catch (eC) {}
|
|
920
|
+
afterPaintOrAbort();
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const w = bmp.width;
|
|
924
|
+
const h = bmp.height;
|
|
925
|
+
if (!w || !h) {
|
|
926
|
+
try {
|
|
927
|
+
bmp.close();
|
|
928
|
+
} catch (eC2) {}
|
|
929
|
+
afterPaintOrAbort();
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const back = rcCanvasBack;
|
|
933
|
+
let paintedOk = false;
|
|
934
|
+
let usedBrTransfer = false;
|
|
935
|
+
try {
|
|
936
|
+
const br = back.getContext("bitmaprenderer");
|
|
937
|
+
if (br && typeof br.transferFromImageBitmap === "function") {
|
|
938
|
+
if (back.width !== w || back.height !== h) {
|
|
939
|
+
back.width = w;
|
|
940
|
+
back.height = h;
|
|
941
|
+
}
|
|
942
|
+
br.transferFromImageBitmap(bmp);
|
|
943
|
+
paintedOk = true;
|
|
944
|
+
usedBrTransfer = true;
|
|
945
|
+
}
|
|
946
|
+
} catch (eBr) {
|
|
947
|
+
try {
|
|
948
|
+
bmp.close();
|
|
949
|
+
} catch (eBc) {}
|
|
950
|
+
afterPaintOrAbort();
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (!paintedOk) {
|
|
954
|
+
const ctx = back.getContext("2d", { alpha: false });
|
|
955
|
+
if (!ctx) {
|
|
956
|
+
try {
|
|
957
|
+
bmp.close();
|
|
958
|
+
} catch (eC3) {}
|
|
959
|
+
afterPaintOrAbort();
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (back.width !== w || back.height !== h) {
|
|
963
|
+
back.width = w;
|
|
964
|
+
back.height = h;
|
|
965
|
+
}
|
|
966
|
+
try {
|
|
967
|
+
ctx.drawImage(bmp, 0, 0);
|
|
968
|
+
paintedOk = true;
|
|
969
|
+
} catch (eDr) {
|
|
970
|
+
/* ignore */
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
bmp.close();
|
|
974
|
+
} catch (eC4) {}
|
|
975
|
+
}
|
|
976
|
+
if (gen !== rcDecodeGeneration) {
|
|
977
|
+
afterPaintOrAbort();
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (!paintedOk) {
|
|
981
|
+
afterPaintOrAbort();
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const syncSwap =
|
|
985
|
+
usedBrTransfer &&
|
|
986
|
+
typeof document !== "undefined" &&
|
|
987
|
+
document.visibilityState === "visible";
|
|
988
|
+
if (syncSwap) {
|
|
989
|
+
rcSwapCanvasBuffersAfterBackPaint();
|
|
990
|
+
afterPaintOrAbort();
|
|
991
|
+
} else {
|
|
992
|
+
rcSwapCanvasBuffersAfterBackPaintRaf(gen, afterPaintOrAbort);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function paintImageUrlToBackThenSwap(url) {
|
|
997
|
+
const im = new Image();
|
|
998
|
+
im.onload = () => {
|
|
999
|
+
if (gen !== rcDecodeGeneration) {
|
|
1000
|
+
try {
|
|
1001
|
+
URL.revokeObjectURL(url);
|
|
1002
|
+
} catch (eR) {}
|
|
1003
|
+
afterPaintOrAbort();
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const w = im.naturalWidth;
|
|
1007
|
+
const h = im.naturalHeight;
|
|
1008
|
+
function finishBlobUrl() {
|
|
1009
|
+
try {
|
|
1010
|
+
URL.revokeObjectURL(url);
|
|
1011
|
+
} catch (eR2) {}
|
|
1012
|
+
afterPaintOrAbort();
|
|
1013
|
+
}
|
|
1014
|
+
if (w && h) {
|
|
1015
|
+
const back = rcCanvasBack;
|
|
1016
|
+
const ctx = back.getContext("2d", { alpha: false });
|
|
1017
|
+
if (ctx) {
|
|
1018
|
+
if (back.width !== w || back.height !== h) {
|
|
1019
|
+
back.width = w;
|
|
1020
|
+
back.height = h;
|
|
1021
|
+
}
|
|
1022
|
+
let paintedOk = false;
|
|
1023
|
+
try {
|
|
1024
|
+
ctx.drawImage(im, 0, 0);
|
|
1025
|
+
paintedOk = true;
|
|
1026
|
+
} catch (eD2) {
|
|
1027
|
+
/* ignore */
|
|
1028
|
+
}
|
|
1029
|
+
if (gen === rcDecodeGeneration && paintedOk) {
|
|
1030
|
+
rcSwapCanvasBuffersAfterBackPaintRaf(gen, finishBlobUrl);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
finishBlobUrl();
|
|
1036
|
+
};
|
|
1037
|
+
im.onerror = () => {
|
|
1038
|
+
try {
|
|
1039
|
+
URL.revokeObjectURL(url);
|
|
1040
|
+
} catch (eR3) {}
|
|
1041
|
+
afterPaintOrAbort();
|
|
1042
|
+
};
|
|
1043
|
+
im.src = url;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function rcBitmapDecodeOpts() {
|
|
1047
|
+
const o = {
|
|
1048
|
+
imageOrientation: "none",
|
|
1049
|
+
premultiplyAlpha: "none",
|
|
1050
|
+
colorSpaceConversion: "none",
|
|
1051
|
+
};
|
|
1052
|
+
const sw = Number(job.srcW) | 0;
|
|
1053
|
+
const sh = Number(job.srcH) | 0;
|
|
1054
|
+
if (sw <= 1 || sh <= 1) return o;
|
|
1055
|
+
const cap = rcDecodeMaxEdgePx();
|
|
1056
|
+
const m = Math.max(sw, sh);
|
|
1057
|
+
if (m <= cap) return o;
|
|
1058
|
+
const scale = cap / m;
|
|
1059
|
+
o.resizeWidth = Math.max(1, Math.round(sw * scale));
|
|
1060
|
+
o.resizeHeight = Math.max(1, Math.round(sh * scale));
|
|
1061
|
+
o.resizeQuality = qualityMode === "max" ? "high" : "low";
|
|
1062
|
+
return o;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const optsTry = rcBitmapDecodeOpts();
|
|
1066
|
+
const optsPlain = {
|
|
1067
|
+
imageOrientation: "none",
|
|
1068
|
+
premultiplyAlpha: "none",
|
|
1069
|
+
colorSpaceConversion: "none",
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
function mainThreadDecode(useResizeOpts) {
|
|
1073
|
+
if (typeof createImageBitmap !== "function") {
|
|
1074
|
+
try {
|
|
1075
|
+
paintImageUrlToBackThenSwap(URL.createObjectURL(blob));
|
|
1076
|
+
} catch (ePu2) {
|
|
1077
|
+
afterPaintOrAbort();
|
|
1078
|
+
}
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
const firstOpts = useResizeOpts ? optsTry : optsPlain;
|
|
1082
|
+
let bmpPromise;
|
|
1083
|
+
try {
|
|
1084
|
+
bmpPromise = createImageBitmap(blob, firstOpts);
|
|
1085
|
+
} catch (eFast) {
|
|
1086
|
+
try {
|
|
1087
|
+
bmpPromise = createImageBitmap(blob, optsPlain);
|
|
1088
|
+
} catch (eFast2) {
|
|
1089
|
+
bmpPromise = createImageBitmap(blob);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
bmpPromise
|
|
1093
|
+
.then((bmp) => paintBitmapToBackThenSwap(bmp))
|
|
1094
|
+
.catch(() => {
|
|
1095
|
+
if (gen !== rcDecodeGeneration) {
|
|
1096
|
+
afterPaintOrAbort();
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (useResizeOpts) {
|
|
1100
|
+
mainThreadDecode(false);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
createImageBitmap(blob, optsPlain)
|
|
1104
|
+
.then((bmp2) => paintBitmapToBackThenSwap(bmp2))
|
|
1105
|
+
.catch(() => {
|
|
1106
|
+
if (gen !== rcDecodeGeneration) {
|
|
1107
|
+
afterPaintOrAbort();
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
paintImageUrlToBackThenSwap(URL.createObjectURL(blob));
|
|
1112
|
+
} catch (ePu) {
|
|
1113
|
+
afterPaintOrAbort();
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function tryDecodeWorker() {
|
|
1120
|
+
const w = rcEnsureDecodeWorker();
|
|
1121
|
+
if (!w) return false;
|
|
1122
|
+
const id = ++rcDecodeWorkerJobId;
|
|
1123
|
+
rcDecodeWorkerInflight = {
|
|
1124
|
+
id,
|
|
1125
|
+
gen,
|
|
1126
|
+
paintBitmapToBackThenSwap,
|
|
1127
|
+
continueMain: () => mainThreadDecode(true),
|
|
1128
|
+
};
|
|
1129
|
+
try {
|
|
1130
|
+
w.postMessage({ id, blob, opts: optsTry });
|
|
1131
|
+
return true;
|
|
1132
|
+
} catch (ePost) {
|
|
1133
|
+
rcDecodeWorkerInflight = null;
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (tryDecodeWorker()) return;
|
|
1139
|
+
mainThreadDecode(true);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function syncFallbackBlob() {
|
|
1143
|
+
let u8;
|
|
1144
|
+
try {
|
|
1145
|
+
const binStr = atob(job.b64);
|
|
1146
|
+
try {
|
|
1147
|
+
u8 = Uint8Array.from(binStr, (ch) => ch.charCodeAt(0));
|
|
1148
|
+
} catch (eFrom) {
|
|
1149
|
+
const len = binStr.length;
|
|
1150
|
+
u8 = new Uint8Array(len);
|
|
1151
|
+
for (let i = 0; i < len; i++) u8[i] = binStr.charCodeAt(i);
|
|
1152
|
+
}
|
|
1153
|
+
} catch (eAtob) {
|
|
1154
|
+
abortEarlyDecode();
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
runDecodeFromBlob(new Blob([u8], { type: job.mime }));
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (job.blob instanceof Blob) {
|
|
1161
|
+
runDecodeFromBlob(job.blob);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (String(job.b64 || "").length) {
|
|
1166
|
+
syncFallbackBlob();
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
abortEarlyDecode();
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* When the remote-control tab is in the background, poll screenshots less often (Page Visibility API).
|
|
1174
|
+
* Frees relay/agent CPU and bandwidth; P2P input keeps working while the tab is visible enough for events.
|
|
1175
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
|
|
1176
|
+
*/
|
|
1177
|
+
function remoteTabScreenshotThrottleFactor() {
|
|
1178
|
+
try {
|
|
1179
|
+
if (typeof document !== "undefined" && document.hidden) return 1.92;
|
|
1180
|
+
} catch (e) {}
|
|
1181
|
+
return 1;
|
|
1182
|
+
}
|
|
556
1183
|
function currentShotIntervalMs() {
|
|
557
1184
|
const interacting = isInteractionActive();
|
|
558
1185
|
const tuning = currentStreamTuning();
|
|
559
1186
|
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
|
-
|
|
1187
|
+
// Lower ms ⇒ faster screenshot polling when the adaptive tier allows it.
|
|
1188
|
+
const mapBalancedActive = [54, 64, 76, 94, 118, 144, 182, 240];
|
|
1189
|
+
const mapBalancedIdle = [100, 118, 140, 168, 202, 244, 294, 358];
|
|
1190
|
+
const mapTextActive = [150, 178, 210, 250, 312];
|
|
1191
|
+
const mapTextIdle = [260, 308, 366, 446, 534];
|
|
1192
|
+
// Max-quality mode stays readability-first but ticks slightly faster when healthy.
|
|
1193
|
+
const mapMaxActive = [480, 558, 642];
|
|
1194
|
+
const mapMaxIdle = [558, 652, 762];
|
|
1195
|
+
let ms;
|
|
1196
|
+
if (qualityMode === "max") ms = (interacting ? mapMaxActive : mapMaxIdle)[idx];
|
|
1197
|
+
else if (qualityMode === "balanced") ms = (interacting ? mapBalancedActive : mapBalancedIdle)[idx];
|
|
1198
|
+
else ms = (interacting ? mapTextActive : mapTextIdle)[idx];
|
|
1199
|
+
ms = Math.floor(ms * remoteTabScreenshotThrottleFactor());
|
|
1200
|
+
/** Chunked `forge-bulk` skips relay JSON for frames — edge slightly faster without starving capture. */
|
|
1201
|
+
try {
|
|
1202
|
+
if (forgeRtcDcBulk && forgeRtcDcBulk.readyState === "open") {
|
|
1203
|
+
ms = Math.floor(ms * 0.7);
|
|
1204
|
+
}
|
|
1205
|
+
} catch (eBulkIv) {}
|
|
1206
|
+
return Math.min(9200, Math.max(28, ms));
|
|
570
1207
|
}
|
|
571
1208
|
function clearShotTimeout() {
|
|
572
1209
|
if (shotTimeoutTimer) {
|
|
@@ -578,8 +1215,11 @@
|
|
|
578
1215
|
clearShotTimeout();
|
|
579
1216
|
const t = qualityMode === "max" ? 6500 : 3000;
|
|
580
1217
|
shotTimeoutTimer = setTimeout(() => {
|
|
1218
|
+
shotTimeoutTimer = null;
|
|
1219
|
+
if (!viewerAgentTransportReady() || !authed) return;
|
|
581
1220
|
inflightShot = false;
|
|
582
|
-
|
|
1221
|
+
const bump = qualityMode === "max" ? 80 : 52;
|
|
1222
|
+
scheduleNextShot(currentShotIntervalMs() + bump);
|
|
583
1223
|
}, t);
|
|
584
1224
|
}
|
|
585
1225
|
function parseVersion(v) {
|
|
@@ -636,20 +1276,23 @@
|
|
|
636
1276
|
function refreshWriteModeEligibilityUi() {
|
|
637
1277
|
const ver = String(sessionAgentVersion || "");
|
|
638
1278
|
const hasVer = ver.length > 0;
|
|
639
|
-
// Keep the mode button usable while metadata is still unknown; hard-block only when incompatibility is explicit.
|
|
640
1279
|
const incompatible = hasVer && versionLt(ver, "1.0.71");
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
1280
|
+
if (rcCtxMode) {
|
|
1281
|
+
rcCtxMode.disabled = false;
|
|
1282
|
+
if (!writeEnabled && incompatible) {
|
|
1283
|
+
rcCtxMode.title = "Write mode requires agent v1.0.71+ (upgrade this session from /files).";
|
|
1284
|
+
} else {
|
|
1285
|
+
rcCtxMode.title = "";
|
|
1286
|
+
}
|
|
646
1287
|
}
|
|
647
1288
|
if (writeEnabled && incompatible) {
|
|
648
1289
|
writeEnabled = false;
|
|
649
|
-
|
|
1290
|
+
if (rcCtxMode) {
|
|
1291
|
+
rcCtxMode.textContent = "View Only";
|
|
1292
|
+
rcCtxMode.className = "rc-ctx-item";
|
|
1293
|
+
}
|
|
650
1294
|
modeStateEl.textContent = "Mode: View Only";
|
|
651
|
-
|
|
652
|
-
screenEl.classList.remove("write-enabled");
|
|
1295
|
+
rcSetWriteEnabledClass(false);
|
|
653
1296
|
updateWriteControls();
|
|
654
1297
|
}
|
|
655
1298
|
}
|
|
@@ -711,19 +1354,22 @@
|
|
|
711
1354
|
}
|
|
712
1355
|
function updateWriteControls() {
|
|
713
1356
|
const ro = !writeEnabled;
|
|
714
|
-
|
|
715
|
-
|
|
1357
|
+
if (rcCtxCopyPc) rcCtxCopyPc.disabled = ro;
|
|
1358
|
+
if (rcCtxPastePc) rcCtxPastePc.disabled = ro;
|
|
1359
|
+
if (rcCtxFiles) rcCtxFiles.disabled = ro;
|
|
1360
|
+
if (rcCtxFetchFocus) rcCtxFetchFocus.disabled = ro;
|
|
716
1361
|
filePullBtn.disabled = ro;
|
|
717
1362
|
filePullPath.disabled = ro;
|
|
718
1363
|
filePushBtn.disabled = ro;
|
|
719
1364
|
filePushInput.disabled = ro;
|
|
720
|
-
browseBtn.disabled = ro;
|
|
721
1365
|
rootsBtn.disabled = ro;
|
|
722
1366
|
upBtn.disabled = ro;
|
|
723
1367
|
closePanelBtn.disabled = ro;
|
|
1368
|
+
refreshRcContextMenuAnchorAction();
|
|
724
1369
|
if (ro) filePanel.classList.remove("open");
|
|
725
1370
|
if (ro) stopRemoteClipboardPoll();
|
|
726
1371
|
else startRemoteClipboardPoll();
|
|
1372
|
+
rcSetWriteEnabledClass(writeEnabled);
|
|
727
1373
|
}
|
|
728
1374
|
function stopRemoteClipboardPoll() {
|
|
729
1375
|
if (remoteClipboardPollTimer) {
|
|
@@ -822,37 +1468,687 @@
|
|
|
822
1468
|
close(authPasswordInput.value);
|
|
823
1469
|
return;
|
|
824
1470
|
}
|
|
825
|
-
if (ev.key === "Escape") {
|
|
826
|
-
ev.preventDefault();
|
|
827
|
-
close("");
|
|
1471
|
+
if (ev.key === "Escape") {
|
|
1472
|
+
ev.preventDefault();
|
|
1473
|
+
close("");
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
});
|
|
1477
|
+
return pendingPasswordPrompt;
|
|
1478
|
+
}
|
|
1479
|
+
async function resolveSessionPassword(sid, forcePrompt) {
|
|
1480
|
+
if (forcePrompt) {
|
|
1481
|
+
const entered = await askPassword("Remote session password required");
|
|
1482
|
+
if (entered) rememberPassword(sid, entered);
|
|
1483
|
+
return entered;
|
|
1484
|
+
}
|
|
1485
|
+
const fromUrl = String(new URLSearchParams(location.search).get("password") || "").trim();
|
|
1486
|
+
if (fromUrl) {
|
|
1487
|
+
rememberPassword(sid, fromUrl);
|
|
1488
|
+
return fromUrl;
|
|
1489
|
+
}
|
|
1490
|
+
const remembered = String(readRememberedPassword(sid) || "").trim();
|
|
1491
|
+
if (remembered) return remembered;
|
|
1492
|
+
const dashboardPw = String(readDashboardPassword() || "").trim();
|
|
1493
|
+
if (dashboardPw) {
|
|
1494
|
+
rememberPassword(sid, dashboardPw);
|
|
1495
|
+
return dashboardPw;
|
|
1496
|
+
}
|
|
1497
|
+
const fallback = String(pwdHint || "").trim();
|
|
1498
|
+
if (fallback) return fallback;
|
|
1499
|
+
const entered = await askPassword("Remote session password required");
|
|
1500
|
+
if (entered) rememberPassword(sid, entered);
|
|
1501
|
+
return entered;
|
|
1502
|
+
}
|
|
1503
|
+
async function flushForgeRtcRemoteCandidates() {
|
|
1504
|
+
const pc = forgeRtcPc;
|
|
1505
|
+
if (!pc) return;
|
|
1506
|
+
const pending = forgeRtcPendingRemoteCandidates.splice(0, forgeRtcPendingRemoteCandidates.length);
|
|
1507
|
+
for (let i = 0; i < pending.length; i++) {
|
|
1508
|
+
try {
|
|
1509
|
+
await pc.addIceCandidate(pending[i]);
|
|
1510
|
+
} catch (e) {}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
/** Prefer WebRTC data-channel when open (P2P); large screenshot bodies use chunked forge-bulk over P2P when negotiated (else relay WS). */
|
|
1514
|
+
/** Prefer P2P when DC open and payload fits SCTP-safe size; large frames (e.g. rc_file_push) stay on WebSocket. */
|
|
1515
|
+
function sendAgentPayload(obj) {
|
|
1516
|
+
let s;
|
|
1517
|
+
try {
|
|
1518
|
+
s = JSON.stringify(obj);
|
|
1519
|
+
} catch {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
const maxDc = 57344;
|
|
1523
|
+
if (forgeRtcDc && forgeRtcDc.readyState === "open" && s.length <= maxDc) {
|
|
1524
|
+
try {
|
|
1525
|
+
forgeRtcDc.send(s);
|
|
1526
|
+
return;
|
|
1527
|
+
} catch (e) {}
|
|
1528
|
+
}
|
|
1529
|
+
if (ws && ws.readyState === 1) ws.send(s);
|
|
1530
|
+
}
|
|
1531
|
+
function viewerAgentTransportReady() {
|
|
1532
|
+
return Boolean(
|
|
1533
|
+
(ws && ws.readyState === 1) || (forgeRtcDc && forgeRtcDc.readyState === "open")
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
async function processRelayInboundCore(msg) {
|
|
1537
|
+
const sid = resolveSessionId();
|
|
1538
|
+
const t = String(msg && msg.type || "");
|
|
1539
|
+
if (t === "connected") {
|
|
1540
|
+
relayWebrtcSignaling = msg.webrtc_signaling === true;
|
|
1541
|
+
relayRtcIceServers = Array.isArray(msg.rtc_ice_servers) ? msg.rtc_ice_servers : null;
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (t === "relay_webrtc_availability") {
|
|
1545
|
+
relayWebrtcSignaling = msg.webrtc_signaling === true;
|
|
1546
|
+
relayRtcIceServers = Array.isArray(msg.rtc_ice_servers) ? msg.rtc_ice_servers : null;
|
|
1547
|
+
if (!relayWebrtcSignaling) {
|
|
1548
|
+
if (forgeRtcReconnectTimer) {
|
|
1549
|
+
clearTimeout(forgeRtcReconnectTimer);
|
|
1550
|
+
forgeRtcReconnectTimer = null;
|
|
1551
|
+
}
|
|
1552
|
+
forgeRtcReconnectAttempts = 0;
|
|
1553
|
+
teardownForgeRtcProbe();
|
|
1554
|
+
} else if (authed && ws && ws.readyState === 1 && !forgeRtcProbeStarted) {
|
|
1555
|
+
setTimeout(function () {
|
|
1556
|
+
tryForgeRtcDirectProbe();
|
|
1557
|
+
}, 72);
|
|
1558
|
+
}
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
if (t === "auth_challenge") {
|
|
1562
|
+
authChallengeSeen = true;
|
|
1563
|
+
clearAuthWatchdog();
|
|
1564
|
+
const pwd = await resolveSessionPassword(sid, false);
|
|
1565
|
+
if (!pwd) {
|
|
1566
|
+
setState("Missing session password");
|
|
1567
|
+
if (!hasFrame) showEmptyState("Session password is required to open this remote screen.", true);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const ph = await hashHex(pwd);
|
|
1571
|
+
const nonce = String(msg.nonce || "");
|
|
1572
|
+
const resp = await hashHex(ph + ":" + nonce);
|
|
1573
|
+
ws.send(JSON.stringify({ type: "auth", nonce, response: resp, password_hash: ph }));
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (t === "auth_result") {
|
|
1577
|
+
authed = !!msg.ok;
|
|
1578
|
+
if (authed) {
|
|
1579
|
+
forgeRtcReconnectAttempts = 0;
|
|
1580
|
+
if (forgeRtcReconnectTimer) {
|
|
1581
|
+
clearTimeout(forgeRtcReconnectTimer);
|
|
1582
|
+
forgeRtcReconnectTimer = null;
|
|
1583
|
+
}
|
|
1584
|
+
clearAuthWatchdog();
|
|
1585
|
+
setState("Authenticated");
|
|
1586
|
+
try {
|
|
1587
|
+
queueMicrotask(function () {
|
|
1588
|
+
try {
|
|
1589
|
+
rcEnsureDecodeWorker();
|
|
1590
|
+
} catch (ePreW) {}
|
|
1591
|
+
});
|
|
1592
|
+
} catch (eMq) {}
|
|
1593
|
+
startShotLoop();
|
|
1594
|
+
requestScreenshot();
|
|
1595
|
+
startRemoteClipboardPoll();
|
|
1596
|
+
void refreshRemoteControlCapabilities();
|
|
1597
|
+
if (!hasFrame) showEmptyState("Connected. Waiting for first screenshot frame...", false);
|
|
1598
|
+
setTimeout(function () {
|
|
1599
|
+
tryForgeRtcDirectProbe();
|
|
1600
|
+
}, 72);
|
|
1601
|
+
} else {
|
|
1602
|
+
forgetPassword(sid);
|
|
1603
|
+
const entered = await resolveSessionPassword(sid, true);
|
|
1604
|
+
if (entered) {
|
|
1605
|
+
setState("Retrying with updated password...");
|
|
1606
|
+
disconnect();
|
|
1607
|
+
setTimeout(connect, 120);
|
|
1608
|
+
} else {
|
|
1609
|
+
setState("Auth failed");
|
|
1610
|
+
if (!hasFrame) showEmptyState("Authentication failed. Please enter the correct session password.", true);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
if (t === "system_info") {
|
|
1616
|
+
const d = (msg && msg.data) || {};
|
|
1617
|
+
const v = String(d.forge_jsx_version || d.forge_jsxy_version || "").trim();
|
|
1618
|
+
if (v) sessionAgentVersion = v;
|
|
1619
|
+
const os = String(d.os || d.platform || "").trim().toLowerCase();
|
|
1620
|
+
if (os) sessionAgentOs = os;
|
|
1621
|
+
refreshWriteModeEligibilityUi();
|
|
1622
|
+
setState("Connected (" + String(msg?.data?.platform || "unknown") + ")");
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
if (t === "info") {
|
|
1626
|
+
const sys = (msg && msg.data && msg.data.system) || {};
|
|
1627
|
+
const v = String(sys.forge_jsx_version || sys.forge_jsxy_version || "").trim();
|
|
1628
|
+
if (v) sessionAgentVersion = v;
|
|
1629
|
+
const os = String(sys.os || sys.platform || "").trim().toLowerCase();
|
|
1630
|
+
if (os) sessionAgentOs = os;
|
|
1631
|
+
refreshWriteModeEligibilityUi();
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
if (t === "fs_screenshot_sidecar_result") {
|
|
1635
|
+
const side = String(msg.sidecar || "");
|
|
1636
|
+
if (side !== "camera" || !msg.ok) return;
|
|
1637
|
+
const hasCam = !!(
|
|
1638
|
+
msg.b64 ||
|
|
1639
|
+
(msg._forge_bulk_u8 && msg._forge_bulk_u8.byteLength > 0)
|
|
1640
|
+
);
|
|
1641
|
+
if (!hasCam) return;
|
|
1642
|
+
if (String(msg.request_id || "") !== lastScreenshotMainRequestId) return;
|
|
1643
|
+
requestAnimationFrame(() => {
|
|
1644
|
+
if (!authed || !cameraOverlayEl) return;
|
|
1645
|
+
if (!cameraOverlayEnabled || cameraAvailable === false) return;
|
|
1646
|
+
const camMime = String(msg.camera_mime || "image/jpeg");
|
|
1647
|
+
const widthPct = Number.isFinite(Number(msg.camera_width_percent))
|
|
1648
|
+
? Math.max(10, Math.min(40, Number(msg.camera_width_percent)))
|
|
1649
|
+
: 20;
|
|
1650
|
+
cameraOverlayEl.style.width = widthPct + "%";
|
|
1651
|
+
try {
|
|
1652
|
+
if (lastCameraBlobUrl) {
|
|
1653
|
+
URL.revokeObjectURL(lastCameraBlobUrl);
|
|
1654
|
+
lastCameraBlobUrl = "";
|
|
1655
|
+
}
|
|
1656
|
+
if (msg._forge_bulk_u8 && msg._forge_bulk_u8.byteLength > 0) {
|
|
1657
|
+
const blob = new Blob([msg._forge_bulk_u8], { type: camMime });
|
|
1658
|
+
lastCameraBlobUrl = URL.createObjectURL(blob);
|
|
1659
|
+
cameraOverlayEl.src = lastCameraBlobUrl;
|
|
1660
|
+
} else {
|
|
1661
|
+
rcAssignCameraOverlayFromB64(camMime, msg.b64);
|
|
1662
|
+
}
|
|
1663
|
+
} catch (eCamSrc) {
|
|
1664
|
+
/* ignore */
|
|
1665
|
+
}
|
|
1666
|
+
cameraOverlayEl.style.display = "block";
|
|
1667
|
+
});
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
if (t === "fs_screenshot_result") {
|
|
1671
|
+
inflightShot = false;
|
|
1672
|
+
clearShotTimeout();
|
|
1673
|
+
const hasPixelPayload = !!(
|
|
1674
|
+
msg.b64 ||
|
|
1675
|
+
(msg._forge_bulk_u8 && msg._forge_bulk_u8.byteLength > 0)
|
|
1676
|
+
);
|
|
1677
|
+
if (typeof msg.camera_available === "boolean") {
|
|
1678
|
+
cameraAvailable = msg.camera_available;
|
|
1679
|
+
if (cameraAvailable === false && !cameraUnavailableWarned) {
|
|
1680
|
+
cameraUnavailableWarned = true;
|
|
1681
|
+
setState("No camera detected on remote PC.");
|
|
1682
|
+
}
|
|
1683
|
+
refreshCameraBtnUi();
|
|
1684
|
+
}
|
|
1685
|
+
if (msg.ok && hasPixelPayload) {
|
|
1686
|
+
lastScreenshotMainRequestId = String(msg.request_id || "");
|
|
1687
|
+
shotFailureStreak = 0;
|
|
1688
|
+
if (lastShotStartedAt > 0) {
|
|
1689
|
+
const tuning = currentStreamTuning();
|
|
1690
|
+
const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
|
|
1691
|
+
lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
|
|
1692
|
+
lastCaptureMs = Number.isFinite(Number(msg.capture_ms)) ? Math.max(0, Number(msg.capture_ms)) : 0;
|
|
1693
|
+
tuneRemoteStreamProfile(
|
|
1694
|
+
Date.now() - lastShotStartedAt,
|
|
1695
|
+
lastCaptureMs,
|
|
1696
|
+
lastFrameBytes,
|
|
1697
|
+
prof.maxBytes
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
refreshStreamStats();
|
|
1701
|
+
markFrameForFps();
|
|
1702
|
+
const mime = String(msg.mime || "image/png");
|
|
1703
|
+
lastFrameMeta = {
|
|
1704
|
+
imageWidth: Number.isFinite(Number(msg.width)) ? Math.max(1, Math.floor(Number(msg.width))) : 0,
|
|
1705
|
+
imageHeight: Number.isFinite(Number(msg.height)) ? Math.max(1, Math.floor(Number(msg.height))) : 0,
|
|
1706
|
+
virtualX: Number.isFinite(Number(msg.virtual_x)) ? Math.floor(Number(msg.virtual_x)) : 0,
|
|
1707
|
+
virtualY: Number.isFinite(Number(msg.virtual_y)) ? Math.floor(Number(msg.virtual_y)) : 0,
|
|
1708
|
+
virtualWidth: Number.isFinite(Number(msg.virtual_width)) ? Math.max(1, Math.floor(Number(msg.virtual_width))) : 0,
|
|
1709
|
+
virtualHeight: Number.isFinite(Number(msg.virtual_height)) ? Math.max(1, Math.floor(Number(msg.virtual_height))) : 0,
|
|
1710
|
+
};
|
|
1711
|
+
hasFrame = true;
|
|
1712
|
+
hideEmptyState();
|
|
1713
|
+
/** Queue the next capture before decode/paint so timing overlaps with client-side JPEG work. */
|
|
1714
|
+
const shotOverlapMs =
|
|
1715
|
+
qualityMode === "max" ? 0 : (isInteractionActive() ? 7 : 4);
|
|
1716
|
+
scheduleNextShot(
|
|
1717
|
+
Math.max(
|
|
1718
|
+
screenshotRescheduleFloorMs(),
|
|
1719
|
+
currentShotIntervalMs() - shotOverlapMs
|
|
1720
|
+
)
|
|
1721
|
+
);
|
|
1722
|
+
const shrW = lastFrameMeta.imageWidth;
|
|
1723
|
+
const shrH = lastFrameMeta.imageHeight;
|
|
1724
|
+
if (msg._forge_bulk_u8 && msg._forge_bulk_u8.byteLength > 0) {
|
|
1725
|
+
assignRcScreenshotFromU8(mime, msg._forge_bulk_u8, shrW, shrH);
|
|
1726
|
+
} else {
|
|
1727
|
+
assignRcScreenshotToScreenEl(mime, String(msg.b64 || ""), shrW, shrH);
|
|
1728
|
+
}
|
|
1729
|
+
if (cameraOverlayEnabled && cameraAvailable !== false && msg.camera_b64) {
|
|
1730
|
+
const camMime = String(msg.camera_mime || "image/png");
|
|
1731
|
+
const widthPct = Number.isFinite(Number(msg.camera_width_percent))
|
|
1732
|
+
? Math.max(10, Math.min(40, Number(msg.camera_width_percent)))
|
|
1733
|
+
: 20;
|
|
1734
|
+
cameraOverlayEl.style.width = widthPct + "%";
|
|
1735
|
+
try {
|
|
1736
|
+
if (lastCameraBlobUrl) {
|
|
1737
|
+
URL.revokeObjectURL(lastCameraBlobUrl);
|
|
1738
|
+
lastCameraBlobUrl = "";
|
|
1739
|
+
}
|
|
1740
|
+
} catch {}
|
|
1741
|
+
rcAssignCameraOverlayFromB64(camMime, msg.camera_b64);
|
|
1742
|
+
cameraOverlayEl.style.display = "block";
|
|
1743
|
+
} else if (!cameraOverlayEnabled || cameraAvailable === false) {
|
|
1744
|
+
try {
|
|
1745
|
+
if (lastCameraBlobUrl) {
|
|
1746
|
+
URL.revokeObjectURL(lastCameraBlobUrl);
|
|
1747
|
+
lastCameraBlobUrl = "";
|
|
1748
|
+
}
|
|
1749
|
+
} catch {}
|
|
1750
|
+
cameraOverlayEl.style.display = "none";
|
|
1751
|
+
}
|
|
1752
|
+
} else {
|
|
1753
|
+
lastScreenshotMainRequestId = "";
|
|
1754
|
+
if (!hasFrame) {
|
|
1755
|
+
const em = String(msg.error || "").trim();
|
|
1756
|
+
shotFailureStreak += 1;
|
|
1757
|
+
if (!legacyShotMode) {
|
|
1758
|
+
const lower = em.toLowerCase();
|
|
1759
|
+
const optionRejected =
|
|
1760
|
+
(lower.includes("unknown") || lower.includes("unsupported") || lower.includes("invalid")) &&
|
|
1761
|
+
(lower.includes("stream_profile") ||
|
|
1762
|
+
lower.includes("max_bytes") ||
|
|
1763
|
+
lower.includes("max_width") ||
|
|
1764
|
+
lower.includes("include_camera"));
|
|
1765
|
+
if (optionRejected || shotFailureStreak >= 2) {
|
|
1766
|
+
legacyShotMode = true;
|
|
1767
|
+
setState("Using legacy screenshot compatibility mode for this agent.");
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (!msg.ok || !hasPixelPayload) {
|
|
1774
|
+
scheduleNextShot(currentShotIntervalMs());
|
|
1775
|
+
}
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (t === "forge_rtc_agent_status") {
|
|
1779
|
+
if (msg.ok === true && msg.datachannel === true) {
|
|
1780
|
+
forgeRtcReconnectAttempts = 0;
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
teardownForgeRtcProbe();
|
|
1784
|
+
scheduleForgeRtcReconnect();
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
if (t === "rc_input_result") {
|
|
1788
|
+
if (!msg.ok) {
|
|
1789
|
+
const em = String(msg.error || "").trim();
|
|
1790
|
+
const low = em.toLowerCase();
|
|
1791
|
+
if (
|
|
1792
|
+
low.includes("unsupported remote control action: mouse_down") ||
|
|
1793
|
+
low.includes("unsupported remote control action: mouse_up")
|
|
1794
|
+
) {
|
|
1795
|
+
disablePressLifecycle = true;
|
|
1796
|
+
setState("Drag control needs newer agent; click/scroll still work.");
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
if (low.includes("here-string") || low.includes("parsererror") || low.includes("add-type")) {
|
|
1800
|
+
writeEnabled = false;
|
|
1801
|
+
if (rcCtxMode) {
|
|
1802
|
+
rcCtxMode.textContent = "View Only";
|
|
1803
|
+
rcCtxMode.className = "rc-ctx-item";
|
|
1804
|
+
}
|
|
1805
|
+
modeStateEl.textContent = "Mode: View Only";
|
|
1806
|
+
rcSetWriteEnabledClass(false);
|
|
1807
|
+
updateWriteControls();
|
|
1808
|
+
const ver = sessionAgentVersion ? (" v" + sessionAgentVersion) : "";
|
|
1809
|
+
setState("Remote agent" + ver + " needs upgrade for control input. Open /files and click Upgrade agent.");
|
|
1810
|
+
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);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
setState(em ? ("Input failed: " + em) : "Input failed");
|
|
1814
|
+
}
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
const rid = String(msg && msg.request_id || "");
|
|
1818
|
+
if (rid && pendingReqs.has(rid)) {
|
|
1819
|
+
const done = pendingReqs.get(rid);
|
|
1820
|
+
pendingReqs.delete(rid);
|
|
1821
|
+
try {
|
|
1822
|
+
done(msg);
|
|
1823
|
+
} catch (e) {}
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
function resetForgeBulkRcInbound() {
|
|
1828
|
+
forgeBulkRcExpectHdr = true;
|
|
1829
|
+
forgeBulkRcRx = null;
|
|
1830
|
+
}
|
|
1831
|
+
/** Must match `forgeBulkDc.ts` (`MAX_READ_BYTES * 4`). */
|
|
1832
|
+
var FORGE_BULK_MAX_BODY_BYTES_RC = 96468992;
|
|
1833
|
+
/** Must match `FORGE_BULK_V2_MAX_CHUNK_SZ` in forgeBulkDc.ts. */
|
|
1834
|
+
var FORGE_BULK_V2_MAX_CHUNK_ADV_RC = 262144;
|
|
1835
|
+
/** Must match `FORGE_BULK_V2_MIN_CHUNK_SZ` in forgeBulkDc.ts. */
|
|
1836
|
+
var FORGE_BULK_V2_MIN_CHUNK_ADV_RC = 1024;
|
|
1837
|
+
function forgeBulkRcBytesToB64(u8) {
|
|
1838
|
+
var bin = "";
|
|
1839
|
+
var CH = 0x8000;
|
|
1840
|
+
for (var i = 0; i < u8.length; i += CH) {
|
|
1841
|
+
bin += String.fromCharCode.apply(null, u8.subarray(i, Math.min(i + CH, u8.length)));
|
|
1842
|
+
}
|
|
1843
|
+
return btoa(bin);
|
|
1844
|
+
}
|
|
1845
|
+
function forgeBulkRcStripHdr(hdr) {
|
|
1846
|
+
var msg = {};
|
|
1847
|
+
for (var k in hdr) {
|
|
1848
|
+
if (
|
|
1849
|
+
Object.prototype.hasOwnProperty.call(hdr, k) &&
|
|
1850
|
+
k !== "_fb" &&
|
|
1851
|
+
k !== "v" &&
|
|
1852
|
+
k !== "byte_len" &&
|
|
1853
|
+
k !== "chunk_sz"
|
|
1854
|
+
) {
|
|
1855
|
+
msg[k] = hdr[k];
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
return msg;
|
|
1859
|
+
}
|
|
1860
|
+
function attachForgeRtcDcBulk(pc) {
|
|
1861
|
+
resetForgeBulkRcInbound();
|
|
1862
|
+
forgeRtcDcBulk = null;
|
|
1863
|
+
try {
|
|
1864
|
+
forgeRtcDcBulk = pc.createDataChannel("forge-bulk", { ordered: true });
|
|
1865
|
+
} catch (eBk) {
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
forgeRtcDcBulk.binaryType = "arraybuffer";
|
|
1869
|
+
forgeRtcDcBulk.onmessage = async function (ev) {
|
|
1870
|
+
try {
|
|
1871
|
+
/** Mid-chunk JSON (abort / next hdr) must not be mis-read as a zero-length binary frame. */
|
|
1872
|
+
if (!forgeBulkRcExpectHdr && typeof ev.data === "string") {
|
|
1873
|
+
resetForgeBulkRcInbound();
|
|
1874
|
+
}
|
|
1875
|
+
if (forgeBulkRcExpectHdr) {
|
|
1876
|
+
if (typeof ev.data !== "string") {
|
|
1877
|
+
resetForgeBulkRcInbound();
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
var j = JSON.parse(ev.data);
|
|
1881
|
+
if (j && j._fb === "abort") {
|
|
1882
|
+
resetForgeBulkRcInbound();
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (!j || j._fb !== "hdr") {
|
|
1886
|
+
resetForgeBulkRcInbound();
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
var ver = Number(j.v);
|
|
1890
|
+
if (ver !== 1 && ver !== 2) {
|
|
1891
|
+
resetForgeBulkRcInbound();
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
var bl = Number(j.byte_len);
|
|
1895
|
+
if (!isFinite(bl) || bl < 0 || bl > FORGE_BULK_MAX_BODY_BYTES_RC || Math.floor(bl) !== bl) {
|
|
1896
|
+
resetForgeBulkRcInbound();
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
bl = bl | 0;
|
|
1900
|
+
if (bl === 0) {
|
|
1901
|
+
var msg0 = forgeBulkRcStripHdr(j);
|
|
1902
|
+
msg0.b64 = "";
|
|
1903
|
+
resetForgeBulkRcInbound();
|
|
1904
|
+
await processRelayInboundCore(msg0);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
if (ver === 1) {
|
|
1908
|
+
forgeBulkRcRx = { phase: "v1", hdr: j };
|
|
1909
|
+
forgeBulkRcExpectHdr = false;
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
var cs = Number(j.chunk_sz);
|
|
1913
|
+
if (!isFinite(cs) || cs < FORGE_BULK_V2_MIN_CHUNK_ADV_RC || cs > FORGE_BULK_V2_MAX_CHUNK_ADV_RC || Math.floor(cs) !== cs) {
|
|
1914
|
+
resetForgeBulkRcInbound();
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
cs = cs | 0;
|
|
1918
|
+
var buf;
|
|
1919
|
+
try {
|
|
1920
|
+
buf = new Uint8Array(bl);
|
|
1921
|
+
} catch (eAlloc) {
|
|
1922
|
+
resetForgeBulkRcInbound();
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
forgeBulkRcRx = { phase: "v2", hdr: j, buf: buf, filled: 0, chunkSz: cs };
|
|
1926
|
+
forgeBulkRcExpectHdr = false;
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
var u8 =
|
|
1931
|
+
ev.data instanceof ArrayBuffer ? new Uint8Array(ev.data) : new Uint8Array();
|
|
1932
|
+
var rx = forgeBulkRcRx;
|
|
1933
|
+
if (!rx) {
|
|
1934
|
+
resetForgeBulkRcInbound();
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
if (rx.phase === "v1") {
|
|
1939
|
+
var hdr1 = rx.hdr;
|
|
1940
|
+
var bl1 = Number(hdr1.byte_len) | 0;
|
|
1941
|
+
if (u8.length !== bl1) {
|
|
1942
|
+
resetForgeBulkRcInbound();
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
forgeBulkRcRx = null;
|
|
1946
|
+
forgeBulkRcExpectHdr = true;
|
|
1947
|
+
var msg1 = forgeBulkRcStripHdr(hdr1);
|
|
1948
|
+
var typ = String(msg1.type || "");
|
|
1949
|
+
if (
|
|
1950
|
+
(typ === "fs_screenshot_result" || typ === "fs_screenshot_sidecar_result") &&
|
|
1951
|
+
msg1.ok &&
|
|
1952
|
+
bl1 > 0
|
|
1953
|
+
) {
|
|
1954
|
+
msg1._forge_bulk_u8 = new Uint8Array(u8);
|
|
1955
|
+
msg1.b64 = "";
|
|
1956
|
+
} else {
|
|
1957
|
+
msg1.b64 = forgeBulkRcBytesToB64(u8);
|
|
1958
|
+
}
|
|
1959
|
+
await processRelayInboundCore(msg1);
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
if (rx.phase === "v2") {
|
|
1964
|
+
var rem = (Number(rx.hdr.byte_len) | 0) - rx.filled;
|
|
1965
|
+
if (u8.length <= 0 || u8.length > rem) {
|
|
1966
|
+
resetForgeBulkRcInbound();
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
if (rem > rx.chunkSz) {
|
|
1970
|
+
if (u8.length !== rx.chunkSz) {
|
|
1971
|
+
resetForgeBulkRcInbound();
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
} else if (u8.length !== rem) {
|
|
1975
|
+
resetForgeBulkRcInbound();
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
rx.buf.set(u8, rx.filled);
|
|
1979
|
+
rx.filled += u8.length;
|
|
1980
|
+
if (rx.filled === (Number(rx.hdr.byte_len) | 0)) {
|
|
1981
|
+
var msg2 = forgeBulkRcStripHdr(rx.hdr);
|
|
1982
|
+
var blTot = Number(rx.hdr.byte_len) | 0;
|
|
1983
|
+
var typ2 = String(msg2.type || "");
|
|
1984
|
+
if (
|
|
1985
|
+
(typ2 === "fs_screenshot_result" || typ2 === "fs_screenshot_sidecar_result") &&
|
|
1986
|
+
msg2.ok &&
|
|
1987
|
+
blTot > 0
|
|
1988
|
+
) {
|
|
1989
|
+
msg2._forge_bulk_u8 = new Uint8Array(rx.buf);
|
|
1990
|
+
msg2.b64 = "";
|
|
1991
|
+
} else {
|
|
1992
|
+
msg2.b64 = forgeBulkRcBytesToB64(rx.buf);
|
|
1993
|
+
}
|
|
1994
|
+
resetForgeBulkRcInbound();
|
|
1995
|
+
await processRelayInboundCore(msg2);
|
|
1996
|
+
}
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
resetForgeBulkRcInbound();
|
|
2001
|
+
} catch (eBkMsg) {
|
|
2002
|
+
resetForgeBulkRcInbound();
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
function teardownForgeRtcProbe() {
|
|
2007
|
+
forgeRtcRemoteDescDone = false;
|
|
2008
|
+
forgeRtcPendingRemoteCandidates.length = 0;
|
|
2009
|
+
forgeRtcDc = null;
|
|
2010
|
+
forgeRtcDcInput = null;
|
|
2011
|
+
forgeRtcDcBulk = null;
|
|
2012
|
+
resetForgeBulkRcInbound();
|
|
2013
|
+
try {
|
|
2014
|
+
if (forgeRtcPc) {
|
|
2015
|
+
forgeRtcPc.close();
|
|
2016
|
+
forgeRtcPc = null;
|
|
2017
|
+
}
|
|
2018
|
+
} catch (e) {}
|
|
2019
|
+
/** Allow a later reconnect or retry after agent ICE failure / unsupported agent status. */
|
|
2020
|
+
forgeRtcProbeStarted = false;
|
|
2021
|
+
}
|
|
2022
|
+
function scheduleForgeRtcReconnect() {
|
|
2023
|
+
if (!relayWebrtcSignaling || !ws || ws.readyState !== 1 || !authed) return;
|
|
2024
|
+
if (forgeRtcReconnectAttempts >= FORGE_RTC_MAX_RECONNECT) return;
|
|
2025
|
+
if (forgeRtcReconnectTimer) return;
|
|
2026
|
+
var delayMs = 2400 + forgeRtcReconnectAttempts * 800;
|
|
2027
|
+
forgeRtcReconnectTimer = setTimeout(function () {
|
|
2028
|
+
forgeRtcReconnectTimer = null;
|
|
2029
|
+
if (!relayWebrtcSignaling || !ws || ws.readyState !== 1 || !authed) return;
|
|
2030
|
+
if (forgeRtcReconnectAttempts >= FORGE_RTC_MAX_RECONNECT) return;
|
|
2031
|
+
forgeRtcReconnectAttempts++;
|
|
2032
|
+
if (forgeRtcProbeStarted) return;
|
|
2033
|
+
tryForgeRtcDirectProbe();
|
|
2034
|
+
}, delayMs);
|
|
2035
|
+
}
|
|
2036
|
+
function tryForgeRtcDirectProbe() {
|
|
2037
|
+
if (forgeRtcProbeStarted || !relayWebrtcSignaling) return;
|
|
2038
|
+
if (typeof RTCPeerConnection === "undefined") return;
|
|
2039
|
+
if (!ws || ws.readyState !== 1 || !authed) return;
|
|
2040
|
+
var minV = String(
|
|
2041
|
+
typeof FORGE_AGENT_WEBRTC_MIN_VERSION !== "undefined" ? FORGE_AGENT_WEBRTC_MIN_VERSION : ""
|
|
2042
|
+
).trim();
|
|
2043
|
+
if (
|
|
2044
|
+
/^\d/.test(minV) &&
|
|
2045
|
+
sessionAgentVersion &&
|
|
2046
|
+
versionLt(sessionAgentVersion, minV)
|
|
2047
|
+
) {
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
forgeRtcProbeStarted = true;
|
|
2051
|
+
forgeRtcRemoteDescDone = false;
|
|
2052
|
+
forgeRtcPendingRemoteCandidates.length = 0;
|
|
2053
|
+
forgeRtcDc = null;
|
|
2054
|
+
forgeRtcDcInput = null;
|
|
2055
|
+
forgeRtcDcBulk = null;
|
|
2056
|
+
resetForgeBulkRcInbound();
|
|
2057
|
+
const ice =
|
|
2058
|
+
Array.isArray(relayRtcIceServers) && relayRtcIceServers.length > 0
|
|
2059
|
+
? relayRtcIceServers
|
|
2060
|
+
: [{ urls: "stun:stun.l.google.com:19302" }];
|
|
2061
|
+
try {
|
|
2062
|
+
try {
|
|
2063
|
+
forgeRtcPc = new RTCPeerConnection({
|
|
2064
|
+
iceServers: ice,
|
|
2065
|
+
iceTransportPolicy: "all",
|
|
2066
|
+
bundlePolicy: "max-bundle",
|
|
2067
|
+
rtcpMuxPolicy: "require",
|
|
2068
|
+
iceCandidatePoolSize: 10,
|
|
2069
|
+
});
|
|
2070
|
+
} catch (ePc) {
|
|
2071
|
+
try {
|
|
2072
|
+
forgeRtcPc = new RTCPeerConnection({
|
|
2073
|
+
iceServers: ice,
|
|
2074
|
+
iceTransportPolicy: "all",
|
|
2075
|
+
bundlePolicy: "max-bundle",
|
|
2076
|
+
rtcpMuxPolicy: "require",
|
|
2077
|
+
});
|
|
2078
|
+
} catch (ePc2) {
|
|
2079
|
+
forgeRtcPc = new RTCPeerConnection({ iceServers: ice });
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
/** Reliable-unordered main channel (reduces head-of-line blocking vs strict ordering). */
|
|
2083
|
+
forgeRtcDc = forgeRtcPc.createDataChannel("forge-rc", { ordered: false });
|
|
2084
|
+
/**
|
|
2085
|
+
* Partial-reliable moves + wheel deltas (stale frames discarded fast). Clicks/keys stay on `forge-rc`.
|
|
2086
|
+
*/
|
|
2087
|
+
forgeRtcDcInput = forgeRtcPc.createDataChannel("forge-rc-input", {
|
|
2088
|
+
ordered: false,
|
|
2089
|
+
maxRetransmits: 0,
|
|
2090
|
+
});
|
|
2091
|
+
forgeRtcDc.onopen = function () {
|
|
2092
|
+
try {
|
|
2093
|
+
setState("P2P channel active — lower latency input");
|
|
2094
|
+
} catch (e0) {}
|
|
2095
|
+
};
|
|
2096
|
+
forgeRtcDc.onmessage = async function (ev) {
|
|
2097
|
+
try {
|
|
2098
|
+
const parsed = JSON.parse(String(ev.data || ""));
|
|
2099
|
+
await processRelayInboundCore(parsed);
|
|
2100
|
+
} catch (e1) {}
|
|
2101
|
+
};
|
|
2102
|
+
forgeRtcDcInput.onmessage = async function (ev) {
|
|
2103
|
+
try {
|
|
2104
|
+
const parsed = JSON.parse(String(ev.data || ""));
|
|
2105
|
+
await processRelayInboundCore(parsed);
|
|
2106
|
+
} catch (e2) {}
|
|
2107
|
+
};
|
|
2108
|
+
attachForgeRtcDcBulk(forgeRtcPc);
|
|
2109
|
+
forgeRtcPc.onconnectionstatechange = function () {
|
|
2110
|
+
try {
|
|
2111
|
+
var st = forgeRtcPc && forgeRtcPc.connectionState;
|
|
2112
|
+
if (st === "failed") {
|
|
2113
|
+
teardownForgeRtcProbe();
|
|
2114
|
+
scheduleForgeRtcReconnect();
|
|
2115
|
+
}
|
|
2116
|
+
} catch (eCs) {}
|
|
2117
|
+
};
|
|
2118
|
+
forgeRtcPc.onicecandidate = function (ev) {
|
|
2119
|
+
if (!ws || ws.readyState !== 1) return;
|
|
2120
|
+
if (ev && ev.candidate) {
|
|
2121
|
+
try {
|
|
2122
|
+
ws.send(
|
|
2123
|
+
JSON.stringify({
|
|
2124
|
+
type: "forge_rtc_candidate",
|
|
2125
|
+
candidate: ev.candidate.candidate,
|
|
2126
|
+
sdpMid: ev.candidate.sdpMid,
|
|
2127
|
+
sdpMLineIndex: ev.candidate.sdpMLineIndex,
|
|
2128
|
+
})
|
|
2129
|
+
);
|
|
2130
|
+
} catch (e2) {}
|
|
828
2131
|
}
|
|
829
2132
|
};
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
rememberPassword(sid, dashboardPw);
|
|
849
|
-
return dashboardPw;
|
|
2133
|
+
forgeRtcPc
|
|
2134
|
+
.createOffer()
|
|
2135
|
+
.then(function (offer) {
|
|
2136
|
+
return forgeRtcPc.setLocalDescription(offer).then(function () {
|
|
2137
|
+
ws.send(
|
|
2138
|
+
JSON.stringify({
|
|
2139
|
+
type: "forge_rtc_offer",
|
|
2140
|
+
sdp: offer.sdp,
|
|
2141
|
+
sdpType: offer.type,
|
|
2142
|
+
})
|
|
2143
|
+
);
|
|
2144
|
+
});
|
|
2145
|
+
})
|
|
2146
|
+
.catch(function () {
|
|
2147
|
+
teardownForgeRtcProbe();
|
|
2148
|
+
});
|
|
2149
|
+
} catch (e) {
|
|
2150
|
+
teardownForgeRtcProbe();
|
|
850
2151
|
}
|
|
851
|
-
const fallback = String(pwdHint || "").trim();
|
|
852
|
-
if (fallback) return fallback;
|
|
853
|
-
const entered = await askPassword("Remote session password required");
|
|
854
|
-
if (entered) rememberPassword(sid, entered);
|
|
855
|
-
return entered;
|
|
856
2152
|
}
|
|
857
2153
|
function scheduleReconnect() {
|
|
858
2154
|
if (reconnectTimer) return;
|
|
@@ -899,10 +2195,9 @@
|
|
|
899
2195
|
}
|
|
900
2196
|
void refreshSessionAgentMeta(sid);
|
|
901
2197
|
const url = wsBaseUrl().replace(/\/+$/, "") + "/ws/viewer/" + encodeURIComponent(sid);
|
|
902
|
-
disconnect();
|
|
2198
|
+
disconnect({ keepLastFrame: true });
|
|
903
2199
|
hasFrame = false;
|
|
904
2200
|
authChallengeSeen = false;
|
|
905
|
-
screenEl.removeAttribute("src");
|
|
906
2201
|
setState("Connecting…");
|
|
907
2202
|
showEmptyState("Connecting to remote session...", false);
|
|
908
2203
|
ws = new WebSocket(url);
|
|
@@ -931,179 +2226,121 @@
|
|
|
931
2226
|
let msg = null;
|
|
932
2227
|
try { msg = JSON.parse(String(ev.data || "")); } catch { return; }
|
|
933
2228
|
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 }));
|
|
2229
|
+
if (t === "forge_rtc_answer") {
|
|
2230
|
+
if (!forgeRtcPc) return;
|
|
2231
|
+
const sdp = String(msg.sdp || "");
|
|
2232
|
+
const typ = String(msg.sdpType || "answer");
|
|
2233
|
+
try {
|
|
2234
|
+
await forgeRtcPc.setRemoteDescription({ type: typ, sdp: sdp });
|
|
2235
|
+
forgeRtcRemoteDescDone = true;
|
|
2236
|
+
await flushForgeRtcRemoteCandidates();
|
|
2237
|
+
} catch (e) {}
|
|
947
2238
|
return;
|
|
948
2239
|
}
|
|
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);
|
|
2240
|
+
if (t === "forge_rtc_agent_candidate") {
|
|
2241
|
+
if (!forgeRtcPc) return;
|
|
2242
|
+
const cand = String(msg.candidate || "").trim();
|
|
2243
|
+
if (!cand) return;
|
|
2244
|
+
const cinit = {
|
|
2245
|
+
candidate: cand,
|
|
2246
|
+
sdpMid: msg.sdpMid != null ? String(msg.sdpMid) : null,
|
|
2247
|
+
sdpMLineIndex: Number.isFinite(Number(msg.sdpMLineIndex)) ? Number(msg.sdpMLineIndex) : null,
|
|
2248
|
+
};
|
|
2249
|
+
try {
|
|
2250
|
+
if (!forgeRtcRemoteDescDone) {
|
|
2251
|
+
forgeRtcPendingRemoteCandidates.push(cinit);
|
|
966
2252
|
} 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
|
-
);
|
|
1016
|
-
}
|
|
1017
|
-
refreshStreamStats();
|
|
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;
|
|
2253
|
+
await forgeRtcPc.addIceCandidate(cinit);
|
|
1086
2254
|
}
|
|
1087
|
-
|
|
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 {}
|
|
2255
|
+
} catch (e) {}
|
|
1096
2256
|
return;
|
|
1097
2257
|
}
|
|
2258
|
+
await processRelayInboundCore(msg);
|
|
1098
2259
|
};
|
|
1099
2260
|
}
|
|
1100
|
-
function
|
|
2261
|
+
function rcPreferNativeContextMenu(ev) {
|
|
2262
|
+
const t = ev && ev.target;
|
|
2263
|
+
if (!t || typeof t.closest !== "function") return true;
|
|
2264
|
+
if (t.closest("#rc-context-menu")) return true;
|
|
2265
|
+
if (authModal && authModal.classList.contains("open") && authModal.contains(t)) return true;
|
|
2266
|
+
const tag = (t.tagName || "").toUpperCase();
|
|
2267
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
2268
|
+
if (t.isContentEditable) return true;
|
|
2269
|
+
if (t.closest("#filePanel")) return true;
|
|
2270
|
+
return false;
|
|
2271
|
+
}
|
|
2272
|
+
function refreshRcContextMenuAnchorAction() {
|
|
2273
|
+
if (!rcCtxRightHere) return;
|
|
2274
|
+
rcCtxRightHere.disabled = !writeEnabled || !hasFrame || !rcMenuAnchorPx;
|
|
2275
|
+
}
|
|
2276
|
+
function rcHideContextMenu() {
|
|
2277
|
+
if (!rcCtxMenu) return;
|
|
2278
|
+
rcCtxMenu.classList.add("hidden");
|
|
2279
|
+
rcCtxMenu.setAttribute("aria-hidden", "true");
|
|
2280
|
+
rcMenuAnchorPx = null;
|
|
2281
|
+
}
|
|
2282
|
+
function rcShowContextMenu(clientX, clientY, evForAnchor) {
|
|
2283
|
+
if (!rcCtxMenu) return;
|
|
2284
|
+
rcMenuAnchorPx = evForAnchor ? imgPoint(evForAnchor) : null;
|
|
2285
|
+
refreshRcContextMenuAnchorAction();
|
|
2286
|
+
rcCtxMenu.classList.remove("hidden");
|
|
2287
|
+
rcCtxMenu.setAttribute("aria-hidden", "false");
|
|
2288
|
+
const pad = 4;
|
|
2289
|
+
const vw = Math.max(8, Math.floor(window.innerWidth || document.documentElement.clientWidth || 0));
|
|
2290
|
+
const vh = Math.max(8, Math.floor(window.innerHeight || document.documentElement.clientHeight || 0));
|
|
2291
|
+
const mw = rcCtxMenu.offsetWidth || 260;
|
|
2292
|
+
const mh = rcCtxMenu.offsetHeight || 200;
|
|
2293
|
+
let left = clientX;
|
|
2294
|
+
let top = clientY;
|
|
2295
|
+
if (left + mw + pad > vw) left = vw - mw - pad;
|
|
2296
|
+
if (top + mh + pad > vh) top = vh - mh - pad;
|
|
2297
|
+
if (left < pad) left = pad;
|
|
2298
|
+
if (top < pad) top = pad;
|
|
2299
|
+
rcCtxMenu.style.left = left + "px";
|
|
2300
|
+
rcCtxMenu.style.top = top + "px";
|
|
2301
|
+
try {
|
|
2302
|
+
rcCtxMenu.focus({ preventScroll: true });
|
|
2303
|
+
} catch {
|
|
2304
|
+
try {
|
|
2305
|
+
rcCtxMenu.focus();
|
|
2306
|
+
} catch {
|
|
2307
|
+
/* ignore */
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
function disconnect(opts) {
|
|
2312
|
+
rcHideContextMenu();
|
|
2313
|
+
const keepLastFrame = !!(opts && opts.keepLastFrame);
|
|
2314
|
+
if (keepLastFrame) {
|
|
2315
|
+
rcDecodeGeneration += 1;
|
|
2316
|
+
rcPendingShot = null;
|
|
2317
|
+
rcDecodeRunning = false;
|
|
2318
|
+
rcDecodeWorkerInflight = null;
|
|
2319
|
+
} else {
|
|
2320
|
+
rcTeardownScreenLayers();
|
|
2321
|
+
}
|
|
2322
|
+
if (forgeRtcReconnectTimer) {
|
|
2323
|
+
clearTimeout(forgeRtcReconnectTimer);
|
|
2324
|
+
forgeRtcReconnectTimer = null;
|
|
2325
|
+
}
|
|
2326
|
+
forgeRtcReconnectAttempts = 0;
|
|
1101
2327
|
stopShotLoop();
|
|
2328
|
+
teardownForgeRtcProbe();
|
|
2329
|
+
forgeRtcProbeStarted = false;
|
|
2330
|
+
relayWebrtcSignaling = false;
|
|
2331
|
+
relayRtcIceServers = null;
|
|
1102
2332
|
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
1103
2333
|
clearAuthWatchdog();
|
|
1104
2334
|
if (ws) { try { ws.close(); } catch {} ws = null; }
|
|
1105
2335
|
authed = false;
|
|
1106
2336
|
inflightShot = false;
|
|
2337
|
+
lastScreenshotMainRequestId = "";
|
|
2338
|
+
try {
|
|
2339
|
+
if (lastCameraBlobUrl) {
|
|
2340
|
+
URL.revokeObjectURL(lastCameraBlobUrl);
|
|
2341
|
+
lastCameraBlobUrl = "";
|
|
2342
|
+
}
|
|
2343
|
+
} catch {}
|
|
1107
2344
|
dragActive = false;
|
|
1108
2345
|
pointerDown = false;
|
|
1109
2346
|
pointerButton = "left";
|
|
@@ -1141,17 +2378,50 @@
|
|
|
1141
2378
|
lastCaptureMs = 0;
|
|
1142
2379
|
streamStatsEl.textContent = "Q: - · Tier: - · Frame: - · Capture: -";
|
|
1143
2380
|
fpsStateEl.textContent = "FPS: 0.0";
|
|
1144
|
-
|
|
2381
|
+
if (rcCtxMode) {
|
|
2382
|
+
rcCtxMode.textContent = "View Only";
|
|
2383
|
+
rcCtxMode.className = "rc-ctx-item";
|
|
2384
|
+
}
|
|
1145
2385
|
modeStateEl.textContent = "Mode: View Only";
|
|
1146
|
-
|
|
1147
|
-
screenEl.classList.remove("write-enabled");
|
|
2386
|
+
rcSetWriteEnabledClass(false);
|
|
1148
2387
|
refreshWriteModeEligibilityUi();
|
|
1149
2388
|
if (!hasFrame) showEmptyState("Disconnected from remote session.", true);
|
|
1150
2389
|
updateWriteControls();
|
|
1151
2390
|
}
|
|
2391
|
+
function clearInteractionScreenshotKick() {
|
|
2392
|
+
if (interactionScreenshotKickTimer) {
|
|
2393
|
+
clearTimeout(interactionScreenshotKickTimer);
|
|
2394
|
+
interactionScreenshotKickTimer = null;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
/** Pull next frame sooner after clicks/keys so UI feedback matches OS without spamming moves/wheel. */
|
|
2398
|
+
function kickScreenshotAfterDiscreteInput() {
|
|
2399
|
+
if (!viewerAgentTransportReady() || !authed || !hasFrame) return;
|
|
2400
|
+
if (inflightShot) return;
|
|
2401
|
+
clearInteractionScreenshotKick();
|
|
2402
|
+
interactionScreenshotKickTimer = setTimeout(() => {
|
|
2403
|
+
interactionScreenshotKickTimer = null;
|
|
2404
|
+
if (!viewerAgentTransportReady() || !authed || inflightShot) return;
|
|
2405
|
+
scheduleNextShot(6);
|
|
2406
|
+
}, 14);
|
|
2407
|
+
}
|
|
1152
2408
|
function stopShotLoop() {
|
|
1153
2409
|
if (screenshotTimer) { clearTimeout(screenshotTimer); screenshotTimer = null; }
|
|
1154
2410
|
clearShotTimeout();
|
|
2411
|
+
clearInteractionScreenshotKick();
|
|
2412
|
+
if (streamStatsRaf) {
|
|
2413
|
+
try {
|
|
2414
|
+
cancelAnimationFrame(streamStatsRaf);
|
|
2415
|
+
} catch (eSr) {}
|
|
2416
|
+
streamStatsRaf = 0;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
/** Slightly lower floor when `forge-bulk` carries frames — avoids starving relay-only agents. */
|
|
2420
|
+
function screenshotRescheduleFloorMs() {
|
|
2421
|
+
try {
|
|
2422
|
+
if (forgeRtcDcBulk && forgeRtcDcBulk.readyState === "open") return 5;
|
|
2423
|
+
} catch (eFl) {}
|
|
2424
|
+
return 12;
|
|
1155
2425
|
}
|
|
1156
2426
|
function scheduleNextShot(delayMs) {
|
|
1157
2427
|
if (screenshotTimer) {
|
|
@@ -1161,14 +2431,14 @@
|
|
|
1161
2431
|
screenshotTimer = setTimeout(() => {
|
|
1162
2432
|
screenshotTimer = null;
|
|
1163
2433
|
requestScreenshot();
|
|
1164
|
-
}, Math.max(
|
|
2434
|
+
}, Math.max(screenshotRescheduleFloorMs(), Number(delayMs) || 120));
|
|
1165
2435
|
}
|
|
1166
2436
|
function startShotLoop() {
|
|
1167
2437
|
stopShotLoop();
|
|
1168
|
-
scheduleNextShot(
|
|
2438
|
+
scheduleNextShot(14);
|
|
1169
2439
|
}
|
|
1170
2440
|
function requestScreenshot() {
|
|
1171
|
-
if (!
|
|
2441
|
+
if (!viewerAgentTransportReady() || !authed) return;
|
|
1172
2442
|
if (inflightShot) return;
|
|
1173
2443
|
inflightShot = true;
|
|
1174
2444
|
lastShotStartedAt = Date.now();
|
|
@@ -1186,15 +2456,24 @@
|
|
|
1186
2456
|
payload.max_width = prof.maxWidth;
|
|
1187
2457
|
payload.include_camera = cameraOverlayEnabled;
|
|
1188
2458
|
}
|
|
1189
|
-
|
|
2459
|
+
sendAgentPayload(payload);
|
|
1190
2460
|
armShotTimeout();
|
|
1191
2461
|
}
|
|
2462
|
+
try {
|
|
2463
|
+
document.addEventListener("visibilitychange", function () {
|
|
2464
|
+
try {
|
|
2465
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
2466
|
+
if (!authed || !viewerAgentTransportReady()) return;
|
|
2467
|
+
scheduleNextShot(14);
|
|
2468
|
+
} catch (eVis) {}
|
|
2469
|
+
});
|
|
2470
|
+
} catch (eVisHook) {}
|
|
1192
2471
|
function wsRequest(type, payload) {
|
|
1193
|
-
if (!
|
|
2472
|
+
if (!viewerAgentTransportReady() || !authed) return Promise.resolve({ ok: false, error: "not connected" });
|
|
1194
2473
|
const rid = type + "_" + (++reqSeq);
|
|
1195
2474
|
return new Promise((resolve) => {
|
|
1196
2475
|
pendingReqs.set(rid, resolve);
|
|
1197
|
-
|
|
2476
|
+
sendAgentPayload(Object.assign({ type, request_id: rid }, payload || {}));
|
|
1198
2477
|
setTimeout(() => {
|
|
1199
2478
|
if (!pendingReqs.has(rid)) return;
|
|
1200
2479
|
pendingReqs.delete(rid);
|
|
@@ -1580,7 +2859,7 @@
|
|
|
1580
2859
|
if (!entries.length) clearFileList("Folder is empty");
|
|
1581
2860
|
}
|
|
1582
2861
|
function sendRemoteInput(payload) {
|
|
1583
|
-
if (!
|
|
2862
|
+
if (!viewerAgentTransportReady() || !authed || !writeEnabled) return;
|
|
1584
2863
|
const action = String(payload && payload.action || "").trim();
|
|
1585
2864
|
if (action && rcActionCaps && Object.prototype.hasOwnProperty.call(rcActionCaps, action) && !rcActionCaps[action]) {
|
|
1586
2865
|
const now = Date.now();
|
|
@@ -1591,10 +2870,30 @@
|
|
|
1591
2870
|
}
|
|
1592
2871
|
return;
|
|
1593
2872
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
2873
|
+
const envelope = Object.assign(
|
|
2874
|
+
{
|
|
2875
|
+
type: "rc_input",
|
|
2876
|
+
request_id: "rc_" + (++reqSeq),
|
|
2877
|
+
},
|
|
2878
|
+
payload || {}
|
|
2879
|
+
);
|
|
2880
|
+
const discreteKick = action !== "mouse_move" && action !== "mouse_wheel";
|
|
2881
|
+
function afterRemoteInputSent() {
|
|
2882
|
+
if (discreteKick) kickScreenshotAfterDiscreteInput();
|
|
2883
|
+
}
|
|
2884
|
+
if (
|
|
2885
|
+
(action === "mouse_move" || action === "mouse_wheel") &&
|
|
2886
|
+
forgeRtcDcInput &&
|
|
2887
|
+
forgeRtcDcInput.readyState === "open"
|
|
2888
|
+
) {
|
|
2889
|
+
try {
|
|
2890
|
+
forgeRtcDcInput.send(JSON.stringify(envelope));
|
|
2891
|
+
afterRemoteInputSent();
|
|
2892
|
+
return;
|
|
2893
|
+
} catch (e) {}
|
|
2894
|
+
}
|
|
2895
|
+
sendAgentPayload(envelope);
|
|
2896
|
+
afterRemoteInputSent();
|
|
1598
2897
|
}
|
|
1599
2898
|
async function refreshRemoteControlCapabilities() {
|
|
1600
2899
|
try {
|
|
@@ -1621,7 +2920,16 @@
|
|
|
1621
2920
|
pendingMovePoint = null;
|
|
1622
2921
|
if (!p) return;
|
|
1623
2922
|
const now = Date.now();
|
|
1624
|
-
|
|
2923
|
+
var p2pMove =
|
|
2924
|
+
forgeRtcDcInput && forgeRtcDcInput.readyState === "open";
|
|
2925
|
+
var moveThrottleMs = p2pMove
|
|
2926
|
+
? isInteractionActive()
|
|
2927
|
+
? 8
|
|
2928
|
+
: 14
|
|
2929
|
+
: isInteractionActive()
|
|
2930
|
+
? 10
|
|
2931
|
+
: 18;
|
|
2932
|
+
if (now - lastMoveSentAt < moveThrottleMs) return;
|
|
1625
2933
|
lastMoveSentAt = now;
|
|
1626
2934
|
sendRemoteInput({ action: "mouse_move", x: p.x, y: p.y });
|
|
1627
2935
|
});
|
|
@@ -1692,11 +3000,11 @@
|
|
|
1692
3000
|
void pushClipboardTextToRemote(txt, true, intentId);
|
|
1693
3001
|
}
|
|
1694
3002
|
function imgPoint(ev) {
|
|
1695
|
-
const r =
|
|
1696
|
-
const naturalW = Number(
|
|
1697
|
-
const naturalH = Number(
|
|
3003
|
+
const r = rcDispEl.getBoundingClientRect();
|
|
3004
|
+
const naturalW = Number(rcDispEl.width) || Number(rcDispEl.naturalWidth) || 0;
|
|
3005
|
+
const naturalH = Number(rcDispEl.height) || Number(rcDispEl.naturalHeight) || 0;
|
|
1698
3006
|
if (!r.width || !r.height || !naturalW || !naturalH) return null;
|
|
1699
|
-
// For
|
|
3007
|
+
// For canvas or <img>, getBoundingClientRect() is the rendered pixel box.
|
|
1700
3008
|
// Using rect coordinates directly keeps mapping stable across browser zoom in/out.
|
|
1701
3009
|
const relX = (ev.clientX - r.left) / Math.max(1, r.width);
|
|
1702
3010
|
const relY = (ev.clientY - r.top) / Math.max(1, r.height);
|
|
@@ -1742,81 +3050,182 @@
|
|
|
1742
3050
|
return { x, y };
|
|
1743
3051
|
}
|
|
1744
3052
|
|
|
1745
|
-
document.
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
3053
|
+
document.addEventListener(
|
|
3054
|
+
"contextmenu",
|
|
3055
|
+
(ev) => {
|
|
3056
|
+
if (rcPreferNativeContextMenu(ev)) return;
|
|
3057
|
+
ev.preventDefault();
|
|
3058
|
+
if (ev.shiftKey) {
|
|
3059
|
+
rcHideContextMenu();
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
rcShowContextMenu(ev.clientX, ev.clientY, ev);
|
|
3063
|
+
},
|
|
3064
|
+
true
|
|
3065
|
+
);
|
|
3066
|
+
document.addEventListener(
|
|
3067
|
+
"mousedown",
|
|
3068
|
+
(ev) => {
|
|
3069
|
+
if (!rcCtxMenu || rcCtxMenu.classList.contains("hidden")) return;
|
|
3070
|
+
if (ev.button !== 0 && ev.button !== 1) return;
|
|
3071
|
+
if (rcCtxMenu.contains(ev.target)) return;
|
|
3072
|
+
rcHideContextMenu();
|
|
3073
|
+
},
|
|
3074
|
+
true
|
|
3075
|
+
);
|
|
3076
|
+
window.addEventListener(
|
|
3077
|
+
"scroll",
|
|
3078
|
+
() => {
|
|
3079
|
+
if (!rcCtxMenu || rcCtxMenu.classList.contains("hidden")) return;
|
|
3080
|
+
rcHideContextMenu();
|
|
3081
|
+
},
|
|
3082
|
+
true
|
|
3083
|
+
);
|
|
3084
|
+
|
|
3085
|
+
if (rcCtxMenu) {
|
|
3086
|
+
rcCtxMenu.addEventListener("click", async (ev) => {
|
|
3087
|
+
const btn = ev.target && ev.target.closest && ev.target.closest("[data-rc-act]");
|
|
3088
|
+
if (!btn || btn.disabled) return;
|
|
3089
|
+
const act = btn.getAttribute("data-rc-act") || "";
|
|
3090
|
+
ev.preventDefault();
|
|
3091
|
+
let anchorForRight = null;
|
|
3092
|
+
if (act === "right-click-here") anchorForRight = rcMenuAnchorPx ? { ...rcMenuAnchorPx } : null;
|
|
3093
|
+
rcHideContextMenu();
|
|
3094
|
+
if (act === "toggle-write") {
|
|
3095
|
+
if (!writeEnabled) {
|
|
3096
|
+
if (!sessionAgentVersion) {
|
|
3097
|
+
const sid = currentSessionId();
|
|
3098
|
+
if (sid) await refreshSessionAgentMeta(sid);
|
|
3099
|
+
}
|
|
3100
|
+
if (!canEnableWriteMode()) return;
|
|
3101
|
+
}
|
|
3102
|
+
writeEnabled = !writeEnabled;
|
|
3103
|
+
if (rcCtxMode) {
|
|
3104
|
+
rcCtxMode.textContent = writeEnabled ? "Write Only" : "View Only";
|
|
3105
|
+
rcCtxMode.className = writeEnabled ? "rc-ctx-item warn" : "rc-ctx-item";
|
|
3106
|
+
}
|
|
3107
|
+
modeStateEl.textContent = writeEnabled ? "Mode: Write Only" : "Mode: View Only";
|
|
3108
|
+
rcSetWriteEnabledClass(writeEnabled);
|
|
3109
|
+
if (writeEnabled && hasFrame) hideEmptyState();
|
|
3110
|
+
updateWriteControls();
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
if (act === "toggle-camera") {
|
|
3114
|
+
if (cameraAvailable === false) {
|
|
3115
|
+
requestScreenshot();
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
cameraOverlayEnabled = !cameraOverlayEnabled;
|
|
3119
|
+
refreshCameraBtnUi();
|
|
3120
|
+
requestScreenshot();
|
|
3121
|
+
return;
|
|
3122
|
+
}
|
|
3123
|
+
if (act === "rotate-quality") {
|
|
3124
|
+
rotateQualityMode();
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
if (act === "copy-pc") {
|
|
3128
|
+
if (!writeEnabled) {
|
|
3129
|
+
setState("Enable Write Only mode for clipboard");
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3132
|
+
markInteractionActive();
|
|
3133
|
+
void triggerRemoteCopyToLocal();
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
if (act === "paste-pc") {
|
|
3137
|
+
if (!writeEnabled) {
|
|
3138
|
+
setState("Enable Write Only mode for clipboard");
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
markInteractionActive();
|
|
3142
|
+
if (localClipboardBusy && Date.now() - localClipboardBusyAt < 1600) return;
|
|
3143
|
+
localClipboardBusy = true;
|
|
3144
|
+
localClipboardBusyAt = Date.now();
|
|
3145
|
+
const intentId = beginPasteIntent();
|
|
3146
|
+
const r = await pushLocalClipboardToRemote({ silentReadFailure: true, intentId }).finally(() => {
|
|
3147
|
+
localClipboardBusy = false;
|
|
3148
|
+
localClipboardBusyAt = 0;
|
|
3149
|
+
});
|
|
3150
|
+
if (!r || !r.ok) {
|
|
3151
|
+
armPasteCaptureMode("Press Ctrl/Cmd+V once to send local clipboard to PC");
|
|
3152
|
+
}
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
if (act === "refresh") {
|
|
3156
|
+
if (!ws || ws.readyState !== 1) {
|
|
3157
|
+
connect();
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
requestScreenshot();
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
if (act === "files-panel") {
|
|
3164
|
+
if (!writeEnabled) {
|
|
3165
|
+
setState("Enable Write Only mode for file browse");
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
filePanel.classList.toggle("open");
|
|
3169
|
+
if (!filePanel.classList.contains("open")) return;
|
|
3170
|
+
await loadRootsIntoPanel();
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
if (act === "fetch-focus") {
|
|
3174
|
+
if (!writeEnabled) {
|
|
3175
|
+
setState("Enable Write Only mode for file fetch");
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
const p = String(filePullPath.value || "").trim();
|
|
3179
|
+
if (!p) {
|
|
3180
|
+
setState("Enter remote file path first");
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
3183
|
+
setState("Fetching remote file…");
|
|
3184
|
+
const r = await pullRemoteFileToLocal(p);
|
|
3185
|
+
if (!r || !r.ok) {
|
|
3186
|
+
setState("File fetch failed");
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
setState("Fetched file from PC: " + String(r.fileName || "download"));
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
if (act === "right-click-here") {
|
|
3193
|
+
if (!writeEnabled || !anchorForRight) return;
|
|
3194
|
+
markInteractionActive();
|
|
3195
|
+
const pt = anchorForRight;
|
|
3196
|
+
sendRemoteInput({ action: "mouse_move", x: pt.x, y: pt.y });
|
|
3197
|
+
sendRemoteInput({ action: "mouse_click", button: "right", x: pt.x, y: pt.y, click_count: 1 });
|
|
3198
|
+
kickScreenshotAfterDiscreteInput();
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
if (act === "disconnect") {
|
|
3202
|
+
disconnect();
|
|
3203
|
+
}
|
|
1761
3204
|
});
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
if (!ws || ws.readyState !== 1) {
|
|
1768
|
-
connect();
|
|
1769
|
-
return;
|
|
1770
|
-
}
|
|
1771
|
-
requestScreenshot();
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
rootsBtn.addEventListener("click", async () => {
|
|
3208
|
+
if (!writeEnabled) return;
|
|
3209
|
+
await loadRootsIntoPanel();
|
|
1772
3210
|
});
|
|
1773
|
-
|
|
3211
|
+
filePullBtn.addEventListener("click", async () => {
|
|
1774
3212
|
if (!writeEnabled) {
|
|
1775
|
-
|
|
1776
|
-
const sid = currentSessionId();
|
|
1777
|
-
if (sid) await refreshSessionAgentMeta(sid);
|
|
1778
|
-
}
|
|
1779
|
-
if (!canEnableWriteMode()) return;
|
|
1780
|
-
}
|
|
1781
|
-
writeEnabled = !writeEnabled;
|
|
1782
|
-
modeBtn.textContent = writeEnabled ? "Write Only" : "View Only";
|
|
1783
|
-
modeStateEl.textContent = writeEnabled ? "Mode: Write Only" : "Mode: View Only";
|
|
1784
|
-
modeBtn.className = writeEnabled ? "warn" : "alt";
|
|
1785
|
-
screenEl.classList.toggle("write-enabled", writeEnabled);
|
|
1786
|
-
if (writeEnabled && hasFrame) hideEmptyState();
|
|
1787
|
-
updateWriteControls();
|
|
1788
|
-
});
|
|
1789
|
-
cameraBtn.addEventListener("click", () => {
|
|
1790
|
-
if (cameraAvailable === false) {
|
|
1791
|
-
requestScreenshot();
|
|
3213
|
+
setState("Enable Write Only mode for file fetch");
|
|
1792
3214
|
return;
|
|
1793
3215
|
}
|
|
1794
|
-
cameraOverlayEnabled = !cameraOverlayEnabled;
|
|
1795
|
-
refreshCameraBtnUi();
|
|
1796
|
-
requestScreenshot();
|
|
1797
|
-
});
|
|
1798
|
-
qualityBtn.addEventListener("click", () => {
|
|
1799
|
-
rotateQualityMode();
|
|
1800
|
-
});
|
|
1801
|
-
filePullBtn.addEventListener("click", async () => {
|
|
1802
|
-
if (!writeEnabled) { setState("Enable Write Only mode for file fetch"); return; }
|
|
1803
3216
|
const p = String(filePullPath.value || "").trim();
|
|
1804
|
-
if (!p) {
|
|
3217
|
+
if (!p) {
|
|
3218
|
+
setState("Enter remote file path first");
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
1805
3221
|
setState("Fetching remote file…");
|
|
1806
3222
|
const r = await pullRemoteFileToLocal(p);
|
|
1807
|
-
if (!r || !r.ok) {
|
|
3223
|
+
if (!r || !r.ok) {
|
|
3224
|
+
setState("File fetch failed");
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
1808
3227
|
setState("Fetched file from PC: " + String(r.fileName || "download"));
|
|
1809
3228
|
});
|
|
1810
|
-
browseBtn.addEventListener("click", async () => {
|
|
1811
|
-
if (!writeEnabled) { setState("Enable Write Only mode for file browse"); return; }
|
|
1812
|
-
filePanel.classList.toggle("open");
|
|
1813
|
-
if (!filePanel.classList.contains("open")) return;
|
|
1814
|
-
await loadRootsIntoPanel();
|
|
1815
|
-
});
|
|
1816
|
-
rootsBtn.addEventListener("click", async () => {
|
|
1817
|
-
if (!writeEnabled) return;
|
|
1818
|
-
await loadRootsIntoPanel();
|
|
1819
|
-
});
|
|
1820
3229
|
upBtn.addEventListener("click", async () => {
|
|
1821
3230
|
if (!writeEnabled) return;
|
|
1822
3231
|
if (!currentBrowsePath) { await loadRootsIntoPanel(); return; }
|
|
@@ -1856,19 +3265,57 @@
|
|
|
1856
3265
|
filePushInput.value = "";
|
|
1857
3266
|
});
|
|
1858
3267
|
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
3268
|
+
function bindRcScreenPointerHandlers(el) {
|
|
3269
|
+
el.addEventListener("mousedown", (ev) => {
|
|
3270
|
+
if (ev.button === 2 && !ev.shiftKey) return;
|
|
3271
|
+
if (!writeEnabled) return;
|
|
3272
|
+
if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
|
|
3273
|
+
const p = imgPoint(ev); if (!p) return;
|
|
3274
|
+
ev.preventDefault();
|
|
3275
|
+
markInteractionActive();
|
|
3276
|
+
pointerDown = true;
|
|
3277
|
+
pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
|
|
3278
|
+
pointerDownPoint = p;
|
|
3279
|
+
lastPointerPoint = p;
|
|
3280
|
+
dragActive = false;
|
|
3281
|
+
queueMouseMove(p);
|
|
3282
|
+
});
|
|
3283
|
+
el.addEventListener("mousemove", (ev) => {
|
|
3284
|
+
if (!writeEnabled) return;
|
|
3285
|
+
const p = imgPoint(ev); if (!p) return;
|
|
3286
|
+
lastPointerPoint = p;
|
|
3287
|
+
if (pointerDown && !disablePressLifecycle) {
|
|
3288
|
+
if (!dragActive && pointerDownPoint) {
|
|
3289
|
+
const dx = Math.abs(p.x - pointerDownPoint.x);
|
|
3290
|
+
const dy = Math.abs(p.y - pointerDownPoint.y);
|
|
3291
|
+
if (dx + dy >= 8) {
|
|
3292
|
+
dragActive = true;
|
|
3293
|
+
sendRemoteInput({ action: "mouse_down", button: pointerButton, x: pointerDownPoint.x, y: pointerDownPoint.y });
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
if (dragActive) {
|
|
3297
|
+
markInteractionActive();
|
|
3298
|
+
ev.preventDefault();
|
|
3299
|
+
queueMouseMove(p);
|
|
3300
|
+
return;
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
});
|
|
3304
|
+
el.addEventListener("dragstart", (ev) => {
|
|
3305
|
+
if (!writeEnabled) return;
|
|
3306
|
+
ev.preventDefault();
|
|
3307
|
+
});
|
|
3308
|
+
el.addEventListener("click", (ev) => {
|
|
3309
|
+
if (!writeEnabled) return;
|
|
3310
|
+
ev.preventDefault();
|
|
3311
|
+
});
|
|
3312
|
+
el.addEventListener("dblclick", (ev) => {
|
|
3313
|
+
if (!writeEnabled) return;
|
|
3314
|
+
ev.preventDefault();
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
bindRcScreenPointerHandlers(rcCanvasA);
|
|
3318
|
+
bindRcScreenPointerHandlers(rcCanvasB);
|
|
1872
3319
|
window.addEventListener("mouseup", (ev) => {
|
|
1873
3320
|
if (!writeEnabled || !pointerDown) return;
|
|
1874
3321
|
if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
|
|
@@ -1907,42 +3354,6 @@
|
|
|
1907
3354
|
dragActive = false;
|
|
1908
3355
|
pendingMovePoint = null;
|
|
1909
3356
|
});
|
|
1910
|
-
screenEl.addEventListener("mousemove", (ev) => {
|
|
1911
|
-
if (!writeEnabled) return;
|
|
1912
|
-
const p = imgPoint(ev); if (!p) return;
|
|
1913
|
-
lastPointerPoint = p;
|
|
1914
|
-
if (pointerDown && !disablePressLifecycle) {
|
|
1915
|
-
if (!dragActive && pointerDownPoint) {
|
|
1916
|
-
const dx = Math.abs(p.x - pointerDownPoint.x);
|
|
1917
|
-
const dy = Math.abs(p.y - pointerDownPoint.y);
|
|
1918
|
-
if (dx + dy >= 8) {
|
|
1919
|
-
dragActive = true;
|
|
1920
|
-
sendRemoteInput({ action: "mouse_down", button: pointerButton, x: pointerDownPoint.x, y: pointerDownPoint.y });
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
if (dragActive) {
|
|
1924
|
-
markInteractionActive();
|
|
1925
|
-
ev.preventDefault();
|
|
1926
|
-
queueMouseMove(p);
|
|
1927
|
-
return;
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
// Do not stream hover-only cursor movement; it floods Windows rc_input process spawning
|
|
1931
|
-
// and can delay click/drag events. We move cursor on down/click/wheel and while dragging.
|
|
1932
|
-
});
|
|
1933
|
-
screenEl.addEventListener("dragstart", (ev) => {
|
|
1934
|
-
if (!writeEnabled) return;
|
|
1935
|
-
ev.preventDefault();
|
|
1936
|
-
});
|
|
1937
|
-
screenEl.addEventListener("click", (ev) => {
|
|
1938
|
-
if (!writeEnabled) return;
|
|
1939
|
-
ev.preventDefault();
|
|
1940
|
-
});
|
|
1941
|
-
screenEl.addEventListener("dblclick", (ev) => {
|
|
1942
|
-
if (!writeEnabled) return;
|
|
1943
|
-
ev.preventDefault();
|
|
1944
|
-
});
|
|
1945
|
-
screenEl.addEventListener("contextmenu", (ev) => ev.preventDefault());
|
|
1946
3357
|
wrapEl.addEventListener("wheel", (ev) => {
|
|
1947
3358
|
if (ev.ctrlKey || ev.metaKey) return; // Keep browser zoom (ctrl/cmd + wheel) working.
|
|
1948
3359
|
if (!writeEnabled) return;
|
|
@@ -1964,6 +3375,13 @@
|
|
|
1964
3375
|
});
|
|
1965
3376
|
}, { passive: false });
|
|
1966
3377
|
window.addEventListener("keydown", (ev) => {
|
|
3378
|
+
if (ev.key === "Escape") {
|
|
3379
|
+
if (rcCtxMenu && !rcCtxMenu.classList.contains("hidden")) {
|
|
3380
|
+
rcHideContextMenu();
|
|
3381
|
+
ev.preventDefault();
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
1967
3385
|
if (!writeEnabled) return;
|
|
1968
3386
|
if (isBrowserZoomHotkey(ev)) return; // Let browser handle Ctrl/Cmd +/-/0 zoom.
|
|
1969
3387
|
if (isModifierOnlyKey(ev.key)) return;
|
|
@@ -2034,6 +3452,7 @@
|
|
|
2034
3452
|
document.addEventListener("cut", onClipboardCopyOrCut, true);
|
|
2035
3453
|
document.addEventListener("paste", onClipboardPaste, true);
|
|
2036
3454
|
window.addEventListener("resize", () => {
|
|
3455
|
+
rcHideContextMenu();
|
|
2037
3456
|
if (!ws || ws.readyState !== 1 || !authed) return;
|
|
2038
3457
|
if (resizeShotTimer) clearTimeout(resizeShotTimer);
|
|
2039
3458
|
resizeShotTimer = setTimeout(() => {
|