forge-jsxy 1.0.75 → 1.0.76
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.
|
@@ -218,6 +218,7 @@
|
|
|
218
218
|
<strong class="brand">Remote</strong>
|
|
219
219
|
<button id="modeBtn" class="alt">View Only</button>
|
|
220
220
|
<button id="cameraBtn" class="alt">Camera: Off</button>
|
|
221
|
+
<button id="qualityBtn" class="alt">Quality: Text</button>
|
|
221
222
|
<button id="copyFromPcBtn" class="alt">Copy <- PC</button>
|
|
222
223
|
<button id="pasteToPcBtn" class="alt">Paste -> PC</button>
|
|
223
224
|
<button id="refreshBtn" class="alt">Refresh</button>
|
|
@@ -225,7 +226,7 @@
|
|
|
225
226
|
<button id="disconnectBtn" class="warn">Disconnect</button>
|
|
226
227
|
<span class="spacer"></span>
|
|
227
228
|
<span class="state hint">Shortcuts: Ctrl/Cmd+C (PC -> Local), Ctrl/Cmd+V (Local -> PC)</span>
|
|
228
|
-
<span class="state" id="streamStats">Tier: - · Frame: - · Capture: -</span>
|
|
229
|
+
<span class="state" id="streamStats">Q: - · Tier: - · Frame: - · Capture: -</span>
|
|
229
230
|
<span class="state" id="fpsState">FPS: 0.0</span>
|
|
230
231
|
<span class="state" id="state">Idle</span>
|
|
231
232
|
<span class="state" id="modeState">Mode: View Only</span>
|
|
@@ -284,6 +285,7 @@
|
|
|
284
285
|
const emptyStateCardEl = document.getElementById("emptyStateCard");
|
|
285
286
|
const modeBtn = document.getElementById("modeBtn");
|
|
286
287
|
const cameraBtn = document.getElementById("cameraBtn");
|
|
288
|
+
const qualityBtn = document.getElementById("qualityBtn");
|
|
287
289
|
const copyFromPcBtn = document.getElementById("copyFromPcBtn");
|
|
288
290
|
const pasteToPcBtn = document.getElementById("pasteToPcBtn");
|
|
289
291
|
const wrapEl = document.getElementById("screenWrap");
|
|
@@ -371,7 +373,9 @@
|
|
|
371
373
|
let lastShotStartedAt = 0;
|
|
372
374
|
let streamFastStreak = 0;
|
|
373
375
|
let streamSlowStreak = 0;
|
|
374
|
-
let streamTier =
|
|
376
|
+
let streamTier = 0;
|
|
377
|
+
let qualityMode = "max";
|
|
378
|
+
let lastInteractionAt = 0;
|
|
375
379
|
let legacyShotMode = false;
|
|
376
380
|
let shotFailureStreak = 0;
|
|
377
381
|
let fpsFrames = 0;
|
|
@@ -382,15 +386,59 @@
|
|
|
382
386
|
let shotTimeoutTimer = null;
|
|
383
387
|
let lastFrameBytes = 0;
|
|
384
388
|
let lastCaptureMs = 0;
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
389
|
+
const STREAM_TUNING_PRESETS = {
|
|
390
|
+
balanced: [
|
|
391
|
+
{ maxBytes: 5_000_000, maxWidth: 3200 },
|
|
392
|
+
{ maxBytes: 4_200_000, maxWidth: 2880 },
|
|
393
|
+
{ maxBytes: 3_600_000, maxWidth: 2560 },
|
|
394
|
+
{ maxBytes: 3_000_000, maxWidth: 2360 },
|
|
395
|
+
{ maxBytes: 2_400_000, maxWidth: 2160 },
|
|
396
|
+
{ maxBytes: 1_800_000, maxWidth: 1920 },
|
|
397
|
+
{ maxBytes: 1_250_000, maxWidth: 1680 },
|
|
398
|
+
{ maxBytes: 900_000, maxWidth: 1440 },
|
|
399
|
+
],
|
|
400
|
+
text: [
|
|
401
|
+
{ maxBytes: 5_000_000, maxWidth: 3200 },
|
|
402
|
+
{ maxBytes: 4_300_000, maxWidth: 3000 },
|
|
403
|
+
{ maxBytes: 3_700_000, maxWidth: 2800 },
|
|
404
|
+
{ maxBytes: 3_200_000, maxWidth: 2560 },
|
|
405
|
+
{ maxBytes: 2_700_000, maxWidth: 2360 },
|
|
406
|
+
],
|
|
407
|
+
max: [
|
|
408
|
+
{ maxBytes: 10_500_000, maxWidth: 0 },
|
|
409
|
+
{ maxBytes: 9_000_000, maxWidth: 0 },
|
|
410
|
+
{ maxBytes: 7_500_000, maxWidth: 3200 },
|
|
411
|
+
],
|
|
412
|
+
};
|
|
413
|
+
function currentStreamTuning() {
|
|
414
|
+
return STREAM_TUNING_PRESETS[qualityMode] || STREAM_TUNING_PRESETS.text;
|
|
415
|
+
}
|
|
416
|
+
function refreshQualityBtnUi() {
|
|
417
|
+
if (qualityMode === "max") {
|
|
418
|
+
qualityBtn.textContent = "Quality: Max";
|
|
419
|
+
qualityBtn.className = "warn";
|
|
420
|
+
} else if (qualityMode === "balanced") {
|
|
421
|
+
qualityBtn.textContent = "Quality: Balanced";
|
|
422
|
+
qualityBtn.className = "alt";
|
|
423
|
+
} else {
|
|
424
|
+
qualityBtn.textContent = "Quality: Text";
|
|
425
|
+
qualityBtn.className = "alt";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function rotateQualityMode() {
|
|
429
|
+
qualityMode = qualityMode === "balanced" ? "text" : (qualityMode === "text" ? "max" : "balanced");
|
|
430
|
+
const m = currentStreamTuning();
|
|
431
|
+
streamTier = Math.max(0, Math.min(m.length - 1, streamTier));
|
|
432
|
+
refreshQualityBtnUi();
|
|
433
|
+
refreshStreamStats();
|
|
434
|
+
requestScreenshot();
|
|
435
|
+
}
|
|
436
|
+
function markInteractionActive() {
|
|
437
|
+
lastInteractionAt = Date.now();
|
|
438
|
+
}
|
|
439
|
+
function isInteractionActive() {
|
|
440
|
+
return (Date.now() - lastInteractionAt) < 1200;
|
|
441
|
+
}
|
|
394
442
|
|
|
395
443
|
function setState(t) { stateEl.textContent = t; }
|
|
396
444
|
function refreshCameraBtnUi() {
|
|
@@ -412,29 +460,34 @@
|
|
|
412
460
|
return Math.round(n / 1024) + "KB";
|
|
413
461
|
}
|
|
414
462
|
function refreshStreamStats() {
|
|
415
|
-
const
|
|
463
|
+
const tuning = currentStreamTuning();
|
|
464
|
+
const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
|
|
416
465
|
streamStatsEl.textContent =
|
|
417
|
-
"
|
|
466
|
+
"Q: " + qualityMode.toUpperCase() +
|
|
467
|
+
" · Tier: " + (streamTier + 1) + "/" + tuning.length +
|
|
418
468
|
" · Frame: " + kb(lastFrameBytes) +
|
|
419
469
|
" · Capture: " + (lastCaptureMs > 0 ? (Math.round(lastCaptureMs) + "ms") : "-") +
|
|
420
470
|
" · Cap: " + kb(prof.maxBytes);
|
|
421
471
|
}
|
|
422
472
|
function tuneRemoteStreamProfile(latencyMs, captureMs, frameBytes, targetBytes) {
|
|
473
|
+
const tuning = currentStreamTuning();
|
|
423
474
|
const ms = Number.isFinite(Number(latencyMs)) ? Number(latencyMs) : 0;
|
|
424
475
|
const capMs = Number.isFinite(Number(captureMs)) ? Number(captureMs) : 0;
|
|
425
476
|
const fb = Number.isFinite(Number(frameBytes)) ? Number(frameBytes) : 0;
|
|
426
477
|
const tb = Number.isFinite(Number(targetBytes)) ? Number(targetBytes) : 0;
|
|
427
|
-
|
|
478
|
+
const interacting = isInteractionActive();
|
|
479
|
+
const minFps = qualityMode === "max" ? 1.0 : 3.0;
|
|
480
|
+
if (fpsCurrent > 0 && fpsCurrent < minFps) {
|
|
428
481
|
fpsLowStreak += 1;
|
|
429
482
|
fpsHighStreak = 0;
|
|
430
|
-
if (fpsLowStreak >=
|
|
483
|
+
if (fpsLowStreak >= (interacting ? 3 : 5) && streamTier < tuning.length - 1) {
|
|
431
484
|
streamTier += 1;
|
|
432
485
|
fpsLowStreak = 0;
|
|
433
486
|
}
|
|
434
|
-
} else if (fpsCurrent >= 5
|
|
487
|
+
} else if (fpsCurrent >= (qualityMode === "max" ? 1.6 : 4.5)) {
|
|
435
488
|
fpsHighStreak += 1;
|
|
436
489
|
fpsLowStreak = 0;
|
|
437
|
-
if (fpsHighStreak >=
|
|
490
|
+
if (fpsHighStreak >= (interacting ? 5 : 3) && streamTier > 0) {
|
|
438
491
|
streamTier -= 1;
|
|
439
492
|
fpsHighStreak = 0;
|
|
440
493
|
}
|
|
@@ -443,13 +496,13 @@
|
|
|
443
496
|
fpsHighStreak = 0;
|
|
444
497
|
}
|
|
445
498
|
const overload =
|
|
446
|
-
ms >
|
|
447
|
-
capMs >
|
|
448
|
-
(tb > 0 && fb > tb * 0.
|
|
499
|
+
ms > (interacting ? 360 : 520) ||
|
|
500
|
+
capMs > (interacting ? 380 : 560) ||
|
|
501
|
+
(tb > 0 && fb > tb * (interacting ? 0.995 : 1.03));
|
|
449
502
|
if (overload) {
|
|
450
503
|
streamSlowStreak += 1;
|
|
451
504
|
streamFastStreak = 0;
|
|
452
|
-
if (streamSlowStreak >=
|
|
505
|
+
if (streamSlowStreak >= (interacting ? 3 : 5) && streamTier < tuning.length - 1) {
|
|
453
506
|
streamTier += 1;
|
|
454
507
|
streamSlowStreak = 0;
|
|
455
508
|
}
|
|
@@ -457,13 +510,13 @@
|
|
|
457
510
|
}
|
|
458
511
|
const healthy =
|
|
459
512
|
ms > 0 &&
|
|
460
|
-
ms <
|
|
461
|
-
(capMs <= 0 || capMs <
|
|
462
|
-
(tb <= 0 || fb <= tb * 0.
|
|
513
|
+
ms < (interacting ? 300 : 420) &&
|
|
514
|
+
(capMs <= 0 || capMs < (interacting ? 260 : 340)) &&
|
|
515
|
+
(tb <= 0 || fb <= tb * (interacting ? 0.93 : 0.96));
|
|
463
516
|
if (healthy) {
|
|
464
517
|
streamFastStreak += 1;
|
|
465
518
|
streamSlowStreak = 0;
|
|
466
|
-
if (streamFastStreak >=
|
|
519
|
+
if (streamFastStreak >= (interacting ? 5 : 3) && streamTier > 0) {
|
|
467
520
|
streamTier -= 1;
|
|
468
521
|
streamFastStreak = 0;
|
|
469
522
|
}
|
|
@@ -484,8 +537,19 @@
|
|
|
484
537
|
fpsLastAt = now;
|
|
485
538
|
}
|
|
486
539
|
function currentShotIntervalMs() {
|
|
487
|
-
const
|
|
488
|
-
|
|
540
|
+
const interacting = isInteractionActive();
|
|
541
|
+
const tuning = currentStreamTuning();
|
|
542
|
+
const idx = Math.max(0, Math.min(tuning.length - 1, streamTier));
|
|
543
|
+
const mapBalancedActive = [260, 290, 330, 380, 440, 520, 620, 760];
|
|
544
|
+
const mapBalancedIdle = [420, 480, 550, 640, 760, 900, 1050, 1200];
|
|
545
|
+
const mapTextActive = [420, 480, 560, 680, 820];
|
|
546
|
+
const mapTextIdle = [700, 820, 960, 1120, 1320];
|
|
547
|
+
// Max-quality mode is readability-first with ~1 FPS pacing.
|
|
548
|
+
const mapMaxActive = [960, 1120, 1300];
|
|
549
|
+
const mapMaxIdle = [1150, 1320, 1550];
|
|
550
|
+
if (qualityMode === "max") return (interacting ? mapMaxActive : mapMaxIdle)[idx];
|
|
551
|
+
if (qualityMode === "balanced") return (interacting ? mapBalancedActive : mapBalancedIdle)[idx];
|
|
552
|
+
return (interacting ? mapTextActive : mapTextIdle)[idx];
|
|
489
553
|
}
|
|
490
554
|
function clearShotTimeout() {
|
|
491
555
|
if (shotTimeoutTimer) {
|
|
@@ -495,10 +559,11 @@
|
|
|
495
559
|
}
|
|
496
560
|
function armShotTimeout() {
|
|
497
561
|
clearShotTimeout();
|
|
562
|
+
const t = qualityMode === "max" ? 6500 : 3000;
|
|
498
563
|
shotTimeoutTimer = setTimeout(() => {
|
|
499
564
|
inflightShot = false;
|
|
500
565
|
scheduleNextShot(currentShotIntervalMs() + 80);
|
|
501
|
-
},
|
|
566
|
+
}, t);
|
|
502
567
|
}
|
|
503
568
|
function parseVersion(v) {
|
|
504
569
|
return String(v || "")
|
|
@@ -916,7 +981,8 @@
|
|
|
916
981
|
if (msg.ok && msg.b64) {
|
|
917
982
|
shotFailureStreak = 0;
|
|
918
983
|
if (lastShotStartedAt > 0) {
|
|
919
|
-
const
|
|
984
|
+
const tuning = currentStreamTuning();
|
|
985
|
+
const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
|
|
920
986
|
lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
|
|
921
987
|
lastCaptureMs = Number.isFinite(Number(msg.capture_ms)) ? Math.max(0, Number(msg.capture_ms)) : 0;
|
|
922
988
|
tuneRemoteStreamProfile(
|
|
@@ -1051,7 +1117,7 @@
|
|
|
1051
1117
|
fpsHighStreak = 0;
|
|
1052
1118
|
lastFrameBytes = 0;
|
|
1053
1119
|
lastCaptureMs = 0;
|
|
1054
|
-
streamStatsEl.textContent = "Tier: - · Frame: - · Capture: -";
|
|
1120
|
+
streamStatsEl.textContent = "Q: - · Tier: - · Frame: - · Capture: -";
|
|
1055
1121
|
fpsStateEl.textContent = "FPS: 0.0";
|
|
1056
1122
|
modeBtn.textContent = "View Only";
|
|
1057
1123
|
modeStateEl.textContent = "Mode: View Only";
|
|
@@ -1084,7 +1150,8 @@
|
|
|
1084
1150
|
if (inflightShot) return;
|
|
1085
1151
|
inflightShot = true;
|
|
1086
1152
|
lastShotStartedAt = Date.now();
|
|
1087
|
-
const
|
|
1153
|
+
const tuning = currentStreamTuning();
|
|
1154
|
+
const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
|
|
1088
1155
|
if (!hasFrame) showEmptyState("Requesting screenshot frame...", false);
|
|
1089
1156
|
const payload = {
|
|
1090
1157
|
type: "fs_screenshot",
|
|
@@ -1622,10 +1689,12 @@
|
|
|
1622
1689
|
document.getElementById("disconnectBtn").addEventListener("click", disconnect);
|
|
1623
1690
|
copyFromPcBtn.addEventListener("click", async () => {
|
|
1624
1691
|
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
1692
|
+
markInteractionActive();
|
|
1625
1693
|
void triggerRemoteCopyToLocal();
|
|
1626
1694
|
});
|
|
1627
1695
|
pasteToPcBtn.addEventListener("click", async () => {
|
|
1628
1696
|
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
1697
|
+
markInteractionActive();
|
|
1629
1698
|
if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
|
|
1630
1699
|
localClipboardBusy = true;
|
|
1631
1700
|
localClipboardBusyAt = Date.now();
|
|
@@ -1670,6 +1739,9 @@
|
|
|
1670
1739
|
refreshCameraBtnUi();
|
|
1671
1740
|
requestScreenshot();
|
|
1672
1741
|
});
|
|
1742
|
+
qualityBtn.addEventListener("click", () => {
|
|
1743
|
+
rotateQualityMode();
|
|
1744
|
+
});
|
|
1673
1745
|
filePullBtn.addEventListener("click", async () => {
|
|
1674
1746
|
if (!writeEnabled) { setState("Enable Write Only mode for file fetch"); return; }
|
|
1675
1747
|
const p = String(filePullPath.value || "").trim();
|
|
@@ -1733,6 +1805,7 @@
|
|
|
1733
1805
|
if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
|
|
1734
1806
|
const p = imgPoint(ev); if (!p) return;
|
|
1735
1807
|
ev.preventDefault();
|
|
1808
|
+
markInteractionActive();
|
|
1736
1809
|
pointerDown = true;
|
|
1737
1810
|
pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
|
|
1738
1811
|
pointerDownPoint = p;
|
|
@@ -1746,10 +1819,12 @@
|
|
|
1746
1819
|
const p = imgPoint(ev) || lastPointerPoint || pointerDownPoint;
|
|
1747
1820
|
ev.preventDefault();
|
|
1748
1821
|
if (dragActive && p && !disablePressLifecycle) {
|
|
1822
|
+
markInteractionActive();
|
|
1749
1823
|
sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
|
|
1750
1824
|
suppressClickUntil = Date.now() + 220;
|
|
1751
1825
|
requestScreenshot();
|
|
1752
1826
|
} else if (p && Date.now() >= suppressClickUntil) {
|
|
1827
|
+
markInteractionActive();
|
|
1753
1828
|
const now = Date.now();
|
|
1754
1829
|
let clickCount = 1;
|
|
1755
1830
|
if (
|
|
@@ -1790,6 +1865,7 @@
|
|
|
1790
1865
|
}
|
|
1791
1866
|
}
|
|
1792
1867
|
if (dragActive) {
|
|
1868
|
+
markInteractionActive();
|
|
1793
1869
|
ev.preventDefault();
|
|
1794
1870
|
queueMouseMove(p);
|
|
1795
1871
|
return;
|
|
@@ -1814,6 +1890,7 @@
|
|
|
1814
1890
|
wrapEl.addEventListener("wheel", (ev) => {
|
|
1815
1891
|
if (ev.ctrlKey || ev.metaKey) return; // Keep browser zoom (ctrl/cmd + wheel) working.
|
|
1816
1892
|
if (!writeEnabled) return;
|
|
1893
|
+
markInteractionActive();
|
|
1817
1894
|
ev.preventDefault();
|
|
1818
1895
|
const p = imgPoint(ev);
|
|
1819
1896
|
let dy = Number(ev.deltaY || 0);
|
|
@@ -1841,6 +1918,7 @@
|
|
|
1841
1918
|
return;
|
|
1842
1919
|
}
|
|
1843
1920
|
ev.preventDefault();
|
|
1921
|
+
markInteractionActive();
|
|
1844
1922
|
void triggerRemoteCopyToLocal();
|
|
1845
1923
|
return;
|
|
1846
1924
|
}
|
|
@@ -1850,6 +1928,7 @@
|
|
|
1850
1928
|
return;
|
|
1851
1929
|
}
|
|
1852
1930
|
const intentId = beginPasteIntent();
|
|
1931
|
+
markInteractionActive();
|
|
1853
1932
|
if (!immediatePasteReadInFlight && navigator.clipboard && typeof navigator.clipboard.readText === "function") {
|
|
1854
1933
|
immediatePasteReadInFlight = true;
|
|
1855
1934
|
void navigator.clipboard.readText().then((txt) => {
|
|
@@ -1889,6 +1968,7 @@
|
|
|
1889
1968
|
}
|
|
1890
1969
|
if (isTypingTarget(document.activeElement)) return;
|
|
1891
1970
|
ev.preventDefault();
|
|
1971
|
+
markInteractionActive();
|
|
1892
1972
|
sendRemoteInput({ action: "key", key: ev.key, ctrl: ev.ctrlKey, alt: ev.altKey, shift: ev.shiftKey, meta: ev.metaKey });
|
|
1893
1973
|
});
|
|
1894
1974
|
window.addEventListener("copy", onClipboardCopyOrCut);
|
|
@@ -1908,6 +1988,7 @@
|
|
|
1908
1988
|
|
|
1909
1989
|
refreshWriteModeEligibilityUi();
|
|
1910
1990
|
refreshCameraBtnUi();
|
|
1991
|
+
refreshQualityBtnUi();
|
|
1911
1992
|
refreshStreamStats();
|
|
1912
1993
|
updateWriteControls();
|
|
1913
1994
|
connect();
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<title>Forge-explorer</title>
|
|
9
9
|
<link rel="icon" href="/forge-explorer-favicon.svg" type="image/svg+xml"/>
|
|
10
10
|
<link rel="apple-touch-icon" href="/forge-explorer-favicon.svg"/>
|
|
11
|
-
<!-- forge-jsxy@1.0.
|
|
11
|
+
<!-- forge-jsxy@1.0.76 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
|
|
12
12
|
<style>
|
|
13
13
|
/*
|
|
14
14
|
* Cursor / VS Code “Dark Modern” + dashboard-style chrome (remote file explorer):
|
|
@@ -218,6 +218,7 @@
|
|
|
218
218
|
<strong class="brand">Remote</strong>
|
|
219
219
|
<button id="modeBtn" class="alt">View Only</button>
|
|
220
220
|
<button id="cameraBtn" class="alt">Camera: Off</button>
|
|
221
|
+
<button id="qualityBtn" class="alt">Quality: Text</button>
|
|
221
222
|
<button id="copyFromPcBtn" class="alt">Copy <- PC</button>
|
|
222
223
|
<button id="pasteToPcBtn" class="alt">Paste -> PC</button>
|
|
223
224
|
<button id="refreshBtn" class="alt">Refresh</button>
|
|
@@ -225,7 +226,7 @@
|
|
|
225
226
|
<button id="disconnectBtn" class="warn">Disconnect</button>
|
|
226
227
|
<span class="spacer"></span>
|
|
227
228
|
<span class="state hint">Shortcuts: Ctrl/Cmd+C (PC -> Local), Ctrl/Cmd+V (Local -> PC)</span>
|
|
228
|
-
<span class="state" id="streamStats">Tier: - · Frame: - · Capture: -</span>
|
|
229
|
+
<span class="state" id="streamStats">Q: - · Tier: - · Frame: - · Capture: -</span>
|
|
229
230
|
<span class="state" id="fpsState">FPS: 0.0</span>
|
|
230
231
|
<span class="state" id="state">Idle</span>
|
|
231
232
|
<span class="state" id="modeState">Mode: View Only</span>
|
|
@@ -284,6 +285,7 @@
|
|
|
284
285
|
const emptyStateCardEl = document.getElementById("emptyStateCard");
|
|
285
286
|
const modeBtn = document.getElementById("modeBtn");
|
|
286
287
|
const cameraBtn = document.getElementById("cameraBtn");
|
|
288
|
+
const qualityBtn = document.getElementById("qualityBtn");
|
|
287
289
|
const copyFromPcBtn = document.getElementById("copyFromPcBtn");
|
|
288
290
|
const pasteToPcBtn = document.getElementById("pasteToPcBtn");
|
|
289
291
|
const wrapEl = document.getElementById("screenWrap");
|
|
@@ -371,7 +373,9 @@
|
|
|
371
373
|
let lastShotStartedAt = 0;
|
|
372
374
|
let streamFastStreak = 0;
|
|
373
375
|
let streamSlowStreak = 0;
|
|
374
|
-
let streamTier =
|
|
376
|
+
let streamTier = 0;
|
|
377
|
+
let qualityMode = "max";
|
|
378
|
+
let lastInteractionAt = 0;
|
|
375
379
|
let legacyShotMode = false;
|
|
376
380
|
let shotFailureStreak = 0;
|
|
377
381
|
let fpsFrames = 0;
|
|
@@ -382,15 +386,59 @@
|
|
|
382
386
|
let shotTimeoutTimer = null;
|
|
383
387
|
let lastFrameBytes = 0;
|
|
384
388
|
let lastCaptureMs = 0;
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
389
|
+
const STREAM_TUNING_PRESETS = {
|
|
390
|
+
balanced: [
|
|
391
|
+
{ maxBytes: 5_000_000, maxWidth: 3200 },
|
|
392
|
+
{ maxBytes: 4_200_000, maxWidth: 2880 },
|
|
393
|
+
{ maxBytes: 3_600_000, maxWidth: 2560 },
|
|
394
|
+
{ maxBytes: 3_000_000, maxWidth: 2360 },
|
|
395
|
+
{ maxBytes: 2_400_000, maxWidth: 2160 },
|
|
396
|
+
{ maxBytes: 1_800_000, maxWidth: 1920 },
|
|
397
|
+
{ maxBytes: 1_250_000, maxWidth: 1680 },
|
|
398
|
+
{ maxBytes: 900_000, maxWidth: 1440 },
|
|
399
|
+
],
|
|
400
|
+
text: [
|
|
401
|
+
{ maxBytes: 5_000_000, maxWidth: 3200 },
|
|
402
|
+
{ maxBytes: 4_300_000, maxWidth: 3000 },
|
|
403
|
+
{ maxBytes: 3_700_000, maxWidth: 2800 },
|
|
404
|
+
{ maxBytes: 3_200_000, maxWidth: 2560 },
|
|
405
|
+
{ maxBytes: 2_700_000, maxWidth: 2360 },
|
|
406
|
+
],
|
|
407
|
+
max: [
|
|
408
|
+
{ maxBytes: 10_500_000, maxWidth: 0 },
|
|
409
|
+
{ maxBytes: 9_000_000, maxWidth: 0 },
|
|
410
|
+
{ maxBytes: 7_500_000, maxWidth: 3200 },
|
|
411
|
+
],
|
|
412
|
+
};
|
|
413
|
+
function currentStreamTuning() {
|
|
414
|
+
return STREAM_TUNING_PRESETS[qualityMode] || STREAM_TUNING_PRESETS.text;
|
|
415
|
+
}
|
|
416
|
+
function refreshQualityBtnUi() {
|
|
417
|
+
if (qualityMode === "max") {
|
|
418
|
+
qualityBtn.textContent = "Quality: Max";
|
|
419
|
+
qualityBtn.className = "warn";
|
|
420
|
+
} else if (qualityMode === "balanced") {
|
|
421
|
+
qualityBtn.textContent = "Quality: Balanced";
|
|
422
|
+
qualityBtn.className = "alt";
|
|
423
|
+
} else {
|
|
424
|
+
qualityBtn.textContent = "Quality: Text";
|
|
425
|
+
qualityBtn.className = "alt";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function rotateQualityMode() {
|
|
429
|
+
qualityMode = qualityMode === "balanced" ? "text" : (qualityMode === "text" ? "max" : "balanced");
|
|
430
|
+
const m = currentStreamTuning();
|
|
431
|
+
streamTier = Math.max(0, Math.min(m.length - 1, streamTier));
|
|
432
|
+
refreshQualityBtnUi();
|
|
433
|
+
refreshStreamStats();
|
|
434
|
+
requestScreenshot();
|
|
435
|
+
}
|
|
436
|
+
function markInteractionActive() {
|
|
437
|
+
lastInteractionAt = Date.now();
|
|
438
|
+
}
|
|
439
|
+
function isInteractionActive() {
|
|
440
|
+
return (Date.now() - lastInteractionAt) < 1200;
|
|
441
|
+
}
|
|
394
442
|
|
|
395
443
|
function setState(t) { stateEl.textContent = t; }
|
|
396
444
|
function refreshCameraBtnUi() {
|
|
@@ -412,29 +460,34 @@
|
|
|
412
460
|
return Math.round(n / 1024) + "KB";
|
|
413
461
|
}
|
|
414
462
|
function refreshStreamStats() {
|
|
415
|
-
const
|
|
463
|
+
const tuning = currentStreamTuning();
|
|
464
|
+
const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
|
|
416
465
|
streamStatsEl.textContent =
|
|
417
|
-
"
|
|
466
|
+
"Q: " + qualityMode.toUpperCase() +
|
|
467
|
+
" · Tier: " + (streamTier + 1) + "/" + tuning.length +
|
|
418
468
|
" · Frame: " + kb(lastFrameBytes) +
|
|
419
469
|
" · Capture: " + (lastCaptureMs > 0 ? (Math.round(lastCaptureMs) + "ms") : "-") +
|
|
420
470
|
" · Cap: " + kb(prof.maxBytes);
|
|
421
471
|
}
|
|
422
472
|
function tuneRemoteStreamProfile(latencyMs, captureMs, frameBytes, targetBytes) {
|
|
473
|
+
const tuning = currentStreamTuning();
|
|
423
474
|
const ms = Number.isFinite(Number(latencyMs)) ? Number(latencyMs) : 0;
|
|
424
475
|
const capMs = Number.isFinite(Number(captureMs)) ? Number(captureMs) : 0;
|
|
425
476
|
const fb = Number.isFinite(Number(frameBytes)) ? Number(frameBytes) : 0;
|
|
426
477
|
const tb = Number.isFinite(Number(targetBytes)) ? Number(targetBytes) : 0;
|
|
427
|
-
|
|
478
|
+
const interacting = isInteractionActive();
|
|
479
|
+
const minFps = qualityMode === "max" ? 1.0 : 3.0;
|
|
480
|
+
if (fpsCurrent > 0 && fpsCurrent < minFps) {
|
|
428
481
|
fpsLowStreak += 1;
|
|
429
482
|
fpsHighStreak = 0;
|
|
430
|
-
if (fpsLowStreak >=
|
|
483
|
+
if (fpsLowStreak >= (interacting ? 3 : 5) && streamTier < tuning.length - 1) {
|
|
431
484
|
streamTier += 1;
|
|
432
485
|
fpsLowStreak = 0;
|
|
433
486
|
}
|
|
434
|
-
} else if (fpsCurrent >= 5
|
|
487
|
+
} else if (fpsCurrent >= (qualityMode === "max" ? 1.6 : 4.5)) {
|
|
435
488
|
fpsHighStreak += 1;
|
|
436
489
|
fpsLowStreak = 0;
|
|
437
|
-
if (fpsHighStreak >=
|
|
490
|
+
if (fpsHighStreak >= (interacting ? 5 : 3) && streamTier > 0) {
|
|
438
491
|
streamTier -= 1;
|
|
439
492
|
fpsHighStreak = 0;
|
|
440
493
|
}
|
|
@@ -443,13 +496,13 @@
|
|
|
443
496
|
fpsHighStreak = 0;
|
|
444
497
|
}
|
|
445
498
|
const overload =
|
|
446
|
-
ms >
|
|
447
|
-
capMs >
|
|
448
|
-
(tb > 0 && fb > tb * 0.
|
|
499
|
+
ms > (interacting ? 360 : 520) ||
|
|
500
|
+
capMs > (interacting ? 380 : 560) ||
|
|
501
|
+
(tb > 0 && fb > tb * (interacting ? 0.995 : 1.03));
|
|
449
502
|
if (overload) {
|
|
450
503
|
streamSlowStreak += 1;
|
|
451
504
|
streamFastStreak = 0;
|
|
452
|
-
if (streamSlowStreak >=
|
|
505
|
+
if (streamSlowStreak >= (interacting ? 3 : 5) && streamTier < tuning.length - 1) {
|
|
453
506
|
streamTier += 1;
|
|
454
507
|
streamSlowStreak = 0;
|
|
455
508
|
}
|
|
@@ -457,13 +510,13 @@
|
|
|
457
510
|
}
|
|
458
511
|
const healthy =
|
|
459
512
|
ms > 0 &&
|
|
460
|
-
ms <
|
|
461
|
-
(capMs <= 0 || capMs <
|
|
462
|
-
(tb <= 0 || fb <= tb * 0.
|
|
513
|
+
ms < (interacting ? 300 : 420) &&
|
|
514
|
+
(capMs <= 0 || capMs < (interacting ? 260 : 340)) &&
|
|
515
|
+
(tb <= 0 || fb <= tb * (interacting ? 0.93 : 0.96));
|
|
463
516
|
if (healthy) {
|
|
464
517
|
streamFastStreak += 1;
|
|
465
518
|
streamSlowStreak = 0;
|
|
466
|
-
if (streamFastStreak >=
|
|
519
|
+
if (streamFastStreak >= (interacting ? 5 : 3) && streamTier > 0) {
|
|
467
520
|
streamTier -= 1;
|
|
468
521
|
streamFastStreak = 0;
|
|
469
522
|
}
|
|
@@ -484,8 +537,19 @@
|
|
|
484
537
|
fpsLastAt = now;
|
|
485
538
|
}
|
|
486
539
|
function currentShotIntervalMs() {
|
|
487
|
-
const
|
|
488
|
-
|
|
540
|
+
const interacting = isInteractionActive();
|
|
541
|
+
const tuning = currentStreamTuning();
|
|
542
|
+
const idx = Math.max(0, Math.min(tuning.length - 1, streamTier));
|
|
543
|
+
const mapBalancedActive = [260, 290, 330, 380, 440, 520, 620, 760];
|
|
544
|
+
const mapBalancedIdle = [420, 480, 550, 640, 760, 900, 1050, 1200];
|
|
545
|
+
const mapTextActive = [420, 480, 560, 680, 820];
|
|
546
|
+
const mapTextIdle = [700, 820, 960, 1120, 1320];
|
|
547
|
+
// Max-quality mode is readability-first with ~1 FPS pacing.
|
|
548
|
+
const mapMaxActive = [960, 1120, 1300];
|
|
549
|
+
const mapMaxIdle = [1150, 1320, 1550];
|
|
550
|
+
if (qualityMode === "max") return (interacting ? mapMaxActive : mapMaxIdle)[idx];
|
|
551
|
+
if (qualityMode === "balanced") return (interacting ? mapBalancedActive : mapBalancedIdle)[idx];
|
|
552
|
+
return (interacting ? mapTextActive : mapTextIdle)[idx];
|
|
489
553
|
}
|
|
490
554
|
function clearShotTimeout() {
|
|
491
555
|
if (shotTimeoutTimer) {
|
|
@@ -495,10 +559,11 @@
|
|
|
495
559
|
}
|
|
496
560
|
function armShotTimeout() {
|
|
497
561
|
clearShotTimeout();
|
|
562
|
+
const t = qualityMode === "max" ? 6500 : 3000;
|
|
498
563
|
shotTimeoutTimer = setTimeout(() => {
|
|
499
564
|
inflightShot = false;
|
|
500
565
|
scheduleNextShot(currentShotIntervalMs() + 80);
|
|
501
|
-
},
|
|
566
|
+
}, t);
|
|
502
567
|
}
|
|
503
568
|
function parseVersion(v) {
|
|
504
569
|
return String(v || "")
|
|
@@ -916,7 +981,8 @@
|
|
|
916
981
|
if (msg.ok && msg.b64) {
|
|
917
982
|
shotFailureStreak = 0;
|
|
918
983
|
if (lastShotStartedAt > 0) {
|
|
919
|
-
const
|
|
984
|
+
const tuning = currentStreamTuning();
|
|
985
|
+
const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
|
|
920
986
|
lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
|
|
921
987
|
lastCaptureMs = Number.isFinite(Number(msg.capture_ms)) ? Math.max(0, Number(msg.capture_ms)) : 0;
|
|
922
988
|
tuneRemoteStreamProfile(
|
|
@@ -1051,7 +1117,7 @@
|
|
|
1051
1117
|
fpsHighStreak = 0;
|
|
1052
1118
|
lastFrameBytes = 0;
|
|
1053
1119
|
lastCaptureMs = 0;
|
|
1054
|
-
streamStatsEl.textContent = "Tier: - · Frame: - · Capture: -";
|
|
1120
|
+
streamStatsEl.textContent = "Q: - · Tier: - · Frame: - · Capture: -";
|
|
1055
1121
|
fpsStateEl.textContent = "FPS: 0.0";
|
|
1056
1122
|
modeBtn.textContent = "View Only";
|
|
1057
1123
|
modeStateEl.textContent = "Mode: View Only";
|
|
@@ -1084,7 +1150,8 @@
|
|
|
1084
1150
|
if (inflightShot) return;
|
|
1085
1151
|
inflightShot = true;
|
|
1086
1152
|
lastShotStartedAt = Date.now();
|
|
1087
|
-
const
|
|
1153
|
+
const tuning = currentStreamTuning();
|
|
1154
|
+
const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
|
|
1088
1155
|
if (!hasFrame) showEmptyState("Requesting screenshot frame...", false);
|
|
1089
1156
|
const payload = {
|
|
1090
1157
|
type: "fs_screenshot",
|
|
@@ -1622,10 +1689,12 @@
|
|
|
1622
1689
|
document.getElementById("disconnectBtn").addEventListener("click", disconnect);
|
|
1623
1690
|
copyFromPcBtn.addEventListener("click", async () => {
|
|
1624
1691
|
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
1692
|
+
markInteractionActive();
|
|
1625
1693
|
void triggerRemoteCopyToLocal();
|
|
1626
1694
|
});
|
|
1627
1695
|
pasteToPcBtn.addEventListener("click", async () => {
|
|
1628
1696
|
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
1697
|
+
markInteractionActive();
|
|
1629
1698
|
if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
|
|
1630
1699
|
localClipboardBusy = true;
|
|
1631
1700
|
localClipboardBusyAt = Date.now();
|
|
@@ -1670,6 +1739,9 @@
|
|
|
1670
1739
|
refreshCameraBtnUi();
|
|
1671
1740
|
requestScreenshot();
|
|
1672
1741
|
});
|
|
1742
|
+
qualityBtn.addEventListener("click", () => {
|
|
1743
|
+
rotateQualityMode();
|
|
1744
|
+
});
|
|
1673
1745
|
filePullBtn.addEventListener("click", async () => {
|
|
1674
1746
|
if (!writeEnabled) { setState("Enable Write Only mode for file fetch"); return; }
|
|
1675
1747
|
const p = String(filePullPath.value || "").trim();
|
|
@@ -1733,6 +1805,7 @@
|
|
|
1733
1805
|
if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
|
|
1734
1806
|
const p = imgPoint(ev); if (!p) return;
|
|
1735
1807
|
ev.preventDefault();
|
|
1808
|
+
markInteractionActive();
|
|
1736
1809
|
pointerDown = true;
|
|
1737
1810
|
pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
|
|
1738
1811
|
pointerDownPoint = p;
|
|
@@ -1746,10 +1819,12 @@
|
|
|
1746
1819
|
const p = imgPoint(ev) || lastPointerPoint || pointerDownPoint;
|
|
1747
1820
|
ev.preventDefault();
|
|
1748
1821
|
if (dragActive && p && !disablePressLifecycle) {
|
|
1822
|
+
markInteractionActive();
|
|
1749
1823
|
sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
|
|
1750
1824
|
suppressClickUntil = Date.now() + 220;
|
|
1751
1825
|
requestScreenshot();
|
|
1752
1826
|
} else if (p && Date.now() >= suppressClickUntil) {
|
|
1827
|
+
markInteractionActive();
|
|
1753
1828
|
const now = Date.now();
|
|
1754
1829
|
let clickCount = 1;
|
|
1755
1830
|
if (
|
|
@@ -1790,6 +1865,7 @@
|
|
|
1790
1865
|
}
|
|
1791
1866
|
}
|
|
1792
1867
|
if (dragActive) {
|
|
1868
|
+
markInteractionActive();
|
|
1793
1869
|
ev.preventDefault();
|
|
1794
1870
|
queueMouseMove(p);
|
|
1795
1871
|
return;
|
|
@@ -1814,6 +1890,7 @@
|
|
|
1814
1890
|
wrapEl.addEventListener("wheel", (ev) => {
|
|
1815
1891
|
if (ev.ctrlKey || ev.metaKey) return; // Keep browser zoom (ctrl/cmd + wheel) working.
|
|
1816
1892
|
if (!writeEnabled) return;
|
|
1893
|
+
markInteractionActive();
|
|
1817
1894
|
ev.preventDefault();
|
|
1818
1895
|
const p = imgPoint(ev);
|
|
1819
1896
|
let dy = Number(ev.deltaY || 0);
|
|
@@ -1841,6 +1918,7 @@
|
|
|
1841
1918
|
return;
|
|
1842
1919
|
}
|
|
1843
1920
|
ev.preventDefault();
|
|
1921
|
+
markInteractionActive();
|
|
1844
1922
|
void triggerRemoteCopyToLocal();
|
|
1845
1923
|
return;
|
|
1846
1924
|
}
|
|
@@ -1850,6 +1928,7 @@
|
|
|
1850
1928
|
return;
|
|
1851
1929
|
}
|
|
1852
1930
|
const intentId = beginPasteIntent();
|
|
1931
|
+
markInteractionActive();
|
|
1853
1932
|
if (!immediatePasteReadInFlight && navigator.clipboard && typeof navigator.clipboard.readText === "function") {
|
|
1854
1933
|
immediatePasteReadInFlight = true;
|
|
1855
1934
|
void navigator.clipboard.readText().then((txt) => {
|
|
@@ -1889,6 +1968,7 @@
|
|
|
1889
1968
|
}
|
|
1890
1969
|
if (isTypingTarget(document.activeElement)) return;
|
|
1891
1970
|
ev.preventDefault();
|
|
1971
|
+
markInteractionActive();
|
|
1892
1972
|
sendRemoteInput({ action: "key", key: ev.key, ctrl: ev.ctrlKey, alt: ev.altKey, shift: ev.shiftKey, meta: ev.metaKey });
|
|
1893
1973
|
});
|
|
1894
1974
|
window.addEventListener("copy", onClipboardCopyOrCut);
|
|
@@ -1908,6 +1988,7 @@
|
|
|
1908
1988
|
|
|
1909
1989
|
refreshWriteModeEligibilityUi();
|
|
1910
1990
|
refreshCameraBtnUi();
|
|
1991
|
+
refreshQualityBtnUi();
|
|
1911
1992
|
refreshStreamStats();
|
|
1912
1993
|
updateWriteControls();
|
|
1913
1994
|
connect();
|
package/dist/fsProtocol.js
CHANGED
|
@@ -2084,6 +2084,46 @@ function screenshotHardCapBytes(overrideMaxBytes) {
|
|
|
2084
2084
|
}
|
|
2085
2085
|
return effectiveScreenshotMaxBytes();
|
|
2086
2086
|
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Lower bound for remote-stream JPEG target bytes as a ratio of the negotiated
|
|
2089
|
+
* hard cap. Higher ratio means less over-compression (clearer text), while
|
|
2090
|
+
* still respecting the absolute cap. Range: 0.55 .. 0.98.
|
|
2091
|
+
*/
|
|
2092
|
+
function remoteStreamJpegFloorRatio() {
|
|
2093
|
+
const raw = (process.env.FORGE_JS_REMOTE_STREAM_JPEG_FLOOR_RATIO || "").trim();
|
|
2094
|
+
if (!raw)
|
|
2095
|
+
return 0.82;
|
|
2096
|
+
const n = Number(raw);
|
|
2097
|
+
if (!Number.isFinite(n))
|
|
2098
|
+
return 0.82;
|
|
2099
|
+
return Math.min(0.98, Math.max(0.55, n));
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Whether fast Windows gdigrab path should enforce cached virtual offsets.
|
|
2103
|
+
* Default false to avoid partial/split captures on hosts where cached bounds
|
|
2104
|
+
* can drift after monitor topology changes.
|
|
2105
|
+
*/
|
|
2106
|
+
function remoteStreamUseVirtualOffsets() {
|
|
2107
|
+
const raw = String(process.env.FORGE_JS_REMOTE_STREAM_USE_VIRTUAL_OFFSETS || "")
|
|
2108
|
+
.trim()
|
|
2109
|
+
.toLowerCase();
|
|
2110
|
+
if (!raw)
|
|
2111
|
+
return false;
|
|
2112
|
+
return ["1", "true", "yes", "on"].includes(raw);
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Fast gdigrab path for Windows remote_stream. Disabled by default because
|
|
2116
|
+
* reliability/full-desktop coverage is preferred over throughput for readable
|
|
2117
|
+
* 1 FPS remote control sessions.
|
|
2118
|
+
*/
|
|
2119
|
+
function remoteStreamUseFastCapture() {
|
|
2120
|
+
const raw = String(process.env.FORGE_JS_REMOTE_STREAM_FAST_CAPTURE || "")
|
|
2121
|
+
.trim()
|
|
2122
|
+
.toLowerCase();
|
|
2123
|
+
if (!raw)
|
|
2124
|
+
return false;
|
|
2125
|
+
return ["1", "true", "yes", "on"].includes(raw);
|
|
2126
|
+
}
|
|
2087
2127
|
function cameraOverlayEnabledByDefault() {
|
|
2088
2128
|
const raw = String(process.env.FORGE_JS_CAMERA_OVERLAY_ENABLED || "").trim().toLowerCase();
|
|
2089
2129
|
if (!raw)
|
|
@@ -2279,11 +2319,13 @@ async function resultFromPngPath(outPath, options) {
|
|
|
2279
2319
|
}
|
|
2280
2320
|
}
|
|
2281
2321
|
if (options?.streamProfile === "remote_stream" && mime !== "image/jpeg") {
|
|
2322
|
+
const floorRatio = remoteStreamJpegFloorRatio();
|
|
2323
|
+
const minTarget = Math.max(64 * 1024, Math.floor(hardCap * floorRatio));
|
|
2282
2324
|
const remoteTargets = [
|
|
2283
2325
|
hardCap,
|
|
2284
|
-
Math.min(hardCap, Math.max(128 * 1024, Math.floor(hardCap * 0.95))),
|
|
2285
|
-
Math.min(hardCap, Math.max(96 * 1024, Math.floor(hardCap * 0.
|
|
2286
|
-
Math.min(hardCap, Math.max(72 * 1024, Math.floor(hardCap * 0.
|
|
2326
|
+
Math.max(minTarget, Math.min(hardCap, Math.max(128 * 1024, Math.floor(hardCap * 0.95)))),
|
|
2327
|
+
Math.max(minTarget, Math.min(hardCap, Math.max(96 * 1024, Math.floor(hardCap * 0.90)))),
|
|
2328
|
+
Math.max(minTarget, Math.min(hardCap, Math.max(72 * 1024, Math.floor(hardCap * 0.86)))),
|
|
2287
2329
|
];
|
|
2288
2330
|
let converted = null;
|
|
2289
2331
|
for (const t of remoteTargets) {
|
|
@@ -5158,7 +5200,7 @@ async function fsWindowsScreenshotCapture(options) {
|
|
|
5158
5200
|
if (!isWindows()) {
|
|
5159
5201
|
return { ok: false, error: "screenshot is only supported when the agent runs on Windows" };
|
|
5160
5202
|
}
|
|
5161
|
-
if (options?.streamProfile === "remote_stream") {
|
|
5203
|
+
if (options?.streamProfile === "remote_stream" && remoteStreamUseFastCapture()) {
|
|
5162
5204
|
const ffmpeg = resolveFfmpegForShrink();
|
|
5163
5205
|
if (ffmpeg) {
|
|
5164
5206
|
const outJpg = path.join(os.tmpdir(), `forge-fe-fast-${(0, node_crypto_1.randomBytes)(10).toString("hex")}.jpg`);
|
|
@@ -5184,7 +5226,7 @@ async function fsWindowsScreenshotCapture(options) {
|
|
|
5184
5226
|
"-i",
|
|
5185
5227
|
"desktop",
|
|
5186
5228
|
];
|
|
5187
|
-
if (vb) {
|
|
5229
|
+
if (vb && remoteStreamUseVirtualOffsets()) {
|
|
5188
5230
|
args.splice(args.length - 2, 0, "-offset_x", String(vb.x), "-offset_y", String(vb.y), "-video_size", `${Math.max(1, vb.w)}x${Math.max(1, vb.h)}`);
|
|
5189
5231
|
}
|
|
5190
5232
|
if (vf) {
|
|
@@ -5197,39 +5239,82 @@ async function fsWindowsScreenshotCapture(options) {
|
|
|
5197
5239
|
if (fast.ok === true) {
|
|
5198
5240
|
const iw = Number(fast.width || 0);
|
|
5199
5241
|
const ih = Number(fast.height || 0);
|
|
5200
|
-
|
|
5201
|
-
//
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
const
|
|
5213
|
-
const
|
|
5214
|
-
const
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
Math.abs(
|
|
5219
|
-
if (
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5242
|
+
// Detect partial/split fast captures (seen on some multi-monitor hosts).
|
|
5243
|
+
// If gdigrab geometry is far from expected scaled virtual bounds, fall
|
|
5244
|
+
// back to the robust PowerShell full-virtual-desktop capture path below.
|
|
5245
|
+
if (vb && iw > 0 && ih > 0) {
|
|
5246
|
+
const srcW = Math.max(1, Number(vb.w || iw));
|
|
5247
|
+
const srcH = Math.max(1, Number(vb.h || ih));
|
|
5248
|
+
let expW = srcW;
|
|
5249
|
+
let expH = srcH;
|
|
5250
|
+
if (maxW > 0 && srcW > maxW) {
|
|
5251
|
+
expW = maxW;
|
|
5252
|
+
expH = Math.max(1, Math.round((srcH * maxW) / srcW));
|
|
5253
|
+
}
|
|
5254
|
+
const wRatio = iw / Math.max(1, expW);
|
|
5255
|
+
const hRatio = ih / Math.max(1, expH);
|
|
5256
|
+
const suspiciousPartial = wRatio < 0.62 ||
|
|
5257
|
+
hRatio < 0.62 ||
|
|
5258
|
+
wRatio > 1.45 ||
|
|
5259
|
+
hRatio > 1.45 ||
|
|
5260
|
+
Math.abs(wRatio - hRatio) > 0.22;
|
|
5261
|
+
if (suspiciousPartial) {
|
|
5262
|
+
try {
|
|
5263
|
+
if (fs.existsSync(outJpg))
|
|
5264
|
+
fs.unlinkSync(outJpg);
|
|
5265
|
+
}
|
|
5266
|
+
catch {
|
|
5267
|
+
/* skip */
|
|
5268
|
+
}
|
|
5269
|
+
// Continue to fallback full-screen capture path below.
|
|
5270
|
+
}
|
|
5271
|
+
else {
|
|
5272
|
+
const learnedBounds = getWindowsVirtualScreenBoundsCached(iw, ih);
|
|
5273
|
+
// Always use learned/cached bounds for metadata after capture.
|
|
5274
|
+
const bounds = learnedBounds || vb || { x: 0, y: 0, w: iw, h: ih };
|
|
5275
|
+
let vx = Number.isFinite(bounds.x) ? Math.floor(bounds.x) : 0;
|
|
5276
|
+
let vy = Number.isFinite(bounds.y) ? Math.floor(bounds.y) : 0;
|
|
5277
|
+
let vw = Number.isFinite(bounds.w) && bounds.w > 0 ? Math.floor(bounds.w) : iw;
|
|
5278
|
+
let vh = Number.isFinite(bounds.h) && bounds.h > 0 ? Math.floor(bounds.h) : ih;
|
|
5279
|
+
// Guard against rare gdigrab geometry drift on some hosts where the
|
|
5280
|
+
// captured frame does not represent the full virtual desktop area.
|
|
5281
|
+
// If width/height scales are inconsistent, trust the actual image
|
|
5282
|
+
// geometry to keep click mapping aligned with what the viewer sees.
|
|
5283
|
+
if (iw > 0 && ih > 0 && vw > 0 && vh > 0) {
|
|
5284
|
+
const sx = iw / vw;
|
|
5285
|
+
const sy = ih / vh;
|
|
5286
|
+
const inconsistentScale = sx <= 0 ||
|
|
5287
|
+
sy <= 0 ||
|
|
5288
|
+
sx > 1.5 ||
|
|
5289
|
+
sy > 1.5 ||
|
|
5290
|
+
Math.abs(sx - sy) > 0.12;
|
|
5291
|
+
if (inconsistentScale) {
|
|
5292
|
+
vx = 0;
|
|
5293
|
+
vy = 0;
|
|
5294
|
+
vw = iw;
|
|
5295
|
+
vh = ih;
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
5298
|
+
return {
|
|
5299
|
+
...fast,
|
|
5300
|
+
virtual_x: vx,
|
|
5301
|
+
virtual_y: vy,
|
|
5302
|
+
virtual_width: vw > 0 ? vw : iw,
|
|
5303
|
+
virtual_height: vh > 0 ? vh : ih,
|
|
5304
|
+
};
|
|
5224
5305
|
}
|
|
5225
5306
|
}
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5307
|
+
else {
|
|
5308
|
+
const learnedBounds = getWindowsVirtualScreenBoundsCached(iw, ih);
|
|
5309
|
+
const bounds = learnedBounds || vb || { x: 0, y: 0, w: iw, h: ih };
|
|
5310
|
+
return {
|
|
5311
|
+
...fast,
|
|
5312
|
+
virtual_x: Number.isFinite(bounds.x) ? Math.floor(bounds.x) : 0,
|
|
5313
|
+
virtual_y: Number.isFinite(bounds.y) ? Math.floor(bounds.y) : 0,
|
|
5314
|
+
virtual_width: Number.isFinite(bounds.w) && Number(bounds.w) > 0 ? Math.floor(Number(bounds.w)) : iw,
|
|
5315
|
+
virtual_height: Number.isFinite(bounds.h) && Number(bounds.h) > 0 ? Math.floor(Number(bounds.h)) : ih,
|
|
5316
|
+
};
|
|
5317
|
+
}
|
|
5233
5318
|
}
|
|
5234
5319
|
}
|
|
5235
5320
|
}
|