forge-jsxy 1.0.74 → 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.
@@ -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.82))),
2286
- Math.min(hardCap, Math.max(72 * 1024, Math.floor(hardCap * 0.68))),
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) {
@@ -2442,8 +2484,15 @@ function getWindowsVirtualScreenBoundsCached(fallbackW, fallbackH) {
2442
2484
  "using System.Runtime.InteropServices;",
2443
2485
  "public class ForgeVirtualBounds {",
2444
2486
  " [DllImport(\"user32.dll\")] public static extern int GetSystemMetrics(int nIndex);",
2487
+ " [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiContext);",
2488
+ " [DllImport(\"shcore.dll\", SetLastError=true)] public static extern int SetProcessDpiAwareness(int value);",
2489
+ " [DllImport(\"user32.dll\")] public static extern bool SetProcessDPIAware();",
2445
2490
  "}",
2446
2491
  "'@",
2492
+ "$dpiOk = $false",
2493
+ "try { if ([ForgeVirtualBounds]::SetProcessDpiAwarenessContext([System.IntPtr](-4))) { $dpiOk = $true } } catch { }",
2494
+ "if (-not $dpiOk) { try { if ([ForgeVirtualBounds]::SetProcessDpiAwareness(2) -eq 0) { $dpiOk = $true } } catch { } }",
2495
+ "if (-not $dpiOk) { try { [ForgeVirtualBounds]::SetProcessDPIAware() | Out-Null } catch { } }",
2447
2496
  "$x=[ForgeVirtualBounds]::GetSystemMetrics(76)",
2448
2497
  "$y=[ForgeVirtualBounds]::GetSystemMetrics(77)",
2449
2498
  "$w=[ForgeVirtualBounds]::GetSystemMetrics(78)",
@@ -5151,12 +5200,19 @@ async function fsWindowsScreenshotCapture(options) {
5151
5200
  if (!isWindows()) {
5152
5201
  return { ok: false, error: "screenshot is only supported when the agent runs on Windows" };
5153
5202
  }
5154
- if (options?.streamProfile === "remote_stream") {
5203
+ if (options?.streamProfile === "remote_stream" && remoteStreamUseFastCapture()) {
5155
5204
  const ffmpeg = resolveFfmpegForShrink();
5156
5205
  if (ffmpeg) {
5157
5206
  const outJpg = path.join(os.tmpdir(), `forge-fe-fast-${(0, node_crypto_1.randomBytes)(10).toString("hex")}.jpg`);
5158
5207
  const maxW = captureScaleMaxWidth(options?.maxWidth ?? null);
5159
5208
  const vf = maxW > 0 ? `scale='if(gt(iw,${maxW}),${maxW},iw)':-2` : "";
5209
+ // Force gdigrab to the virtual desktop only after we have a trusted cached
5210
+ // virtual-bounds sample. First run stays broad (`desktop`) to avoid using
5211
+ // guessed fallback geometry on unusual hosts.
5212
+ const hasCachedBounds = Boolean(windowsVirtualBoundsCache?.bounds);
5213
+ const vb = hasCachedBounds
5214
+ ? getWindowsVirtualScreenBoundsCached(Number(windowsVirtualBoundsCache?.bounds?.w || 1920), Number(windowsVirtualBoundsCache?.bounds?.h || 1080))
5215
+ : null;
5160
5216
  const args = [
5161
5217
  "-nostdin",
5162
5218
  "-hide_banner",
@@ -5170,6 +5226,9 @@ async function fsWindowsScreenshotCapture(options) {
5170
5226
  "-i",
5171
5227
  "desktop",
5172
5228
  ];
5229
+ if (vb && remoteStreamUseVirtualOffsets()) {
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)}`);
5231
+ }
5173
5232
  if (vf) {
5174
5233
  args.push("-vf", vf);
5175
5234
  }
@@ -5178,16 +5237,84 @@ async function fsWindowsScreenshotCapture(options) {
5178
5237
  if (ok && fs.existsSync(outJpg)) {
5179
5238
  const fast = await resultFromPngPath(outJpg, options);
5180
5239
  if (fast.ok === true) {
5181
- const vw = Number(fast.width || 0);
5182
- const vh = Number(fast.height || 0);
5183
- const vb = getWindowsVirtualScreenBoundsCached(vw, vh);
5184
- return {
5185
- ...fast,
5186
- virtual_x: vb.x,
5187
- virtual_y: vb.y,
5188
- virtual_width: vb.w > 0 ? vb.w : vw,
5189
- virtual_height: vb.h > 0 ? vb.h : vh,
5190
- };
5240
+ const iw = Number(fast.width || 0);
5241
+ const ih = Number(fast.height || 0);
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
+ };
5305
+ }
5306
+ }
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
+ }
5191
5318
  }
5192
5319
  }
5193
5320
  }
@@ -5523,12 +5650,20 @@ async function fsRemoteControlInput(payload) {
5523
5650
  return { ok: false, error: "remote control action is required" };
5524
5651
  const psPrelude = [
5525
5652
  "$ErrorActionPreference = 'Stop'",
5526
- "$forgeRcSrc = 'using System;using System.Runtime.InteropServices;public static class ForgeRcUser32 { [DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X, int Y); [DllImport(\"user32.dll\")] public static extern void mouse_event(uint f, uint x, uint y, uint d, UIntPtr e); [DllImport(\"user32.dll\")] public static extern bool SetProcessDPIAware(); [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiContext); [DllImport(\"shcore.dll\", SetLastError=true)] public static extern int SetProcessDpiAwareness(int v); }'",
5653
+ "$forgeRcSrc = 'using System;using System.Runtime.InteropServices;public static class ForgeRcUser32 { [DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X, int Y); [DllImport(\"user32.dll\")] public static extern void mouse_event(uint f, uint x, uint y, uint d, UIntPtr e); [DllImport(\"user32.dll\")] public static extern bool SetProcessDPIAware(); [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiContext); [DllImport(\"shcore.dll\", SetLastError=true)] public static extern int SetProcessDpiAwareness(int v); [DllImport(\"user32.dll\")] public static extern int GetSystemMetrics(int nIndex); }'",
5527
5654
  "Add-Type -TypeDefinition $forgeRcSrc",
5528
5655
  "$__dpiOk = $false",
5529
5656
  "try { if ([ForgeRcUser32]::SetProcessDpiAwarenessContext([System.IntPtr](-4))) { $__dpiOk = $true } } catch { }",
5530
5657
  "if (-not $__dpiOk) { try { if ([ForgeRcUser32]::SetProcessDpiAwareness(2) -eq 0) { $__dpiOk = $true } } catch { } }",
5531
5658
  "if (-not $__dpiOk) { try { [ForgeRcUser32]::SetProcessDPIAware() | Out-Null } catch { } }",
5659
+ "$SM_XVIRTUALSCREEN = 76; $SM_YVIRTUALSCREEN = 77; $SM_CXVIRTUALSCREEN = 78; $SM_CYVIRTUALSCREEN = 79",
5660
+ "$__vx = [ForgeRcUser32]::GetSystemMetrics($SM_XVIRTUALSCREEN)",
5661
+ "$__vy = [ForgeRcUser32]::GetSystemMetrics($SM_YVIRTUALSCREEN)",
5662
+ "$__vw = [ForgeRcUser32]::GetSystemMetrics($SM_CXVIRTUALSCREEN)",
5663
+ "$__vh = [ForgeRcUser32]::GetSystemMetrics($SM_CYVIRTUALSCREEN)",
5664
+ "if ($__vw -le 0) { $__vx = 0; $__vw = [Math]::Max(1, [ForgeRcUser32]::GetSystemMetrics(0)) }",
5665
+ "if ($__vh -le 0) { $__vy = 0; $__vh = [Math]::Max(1, [ForgeRcUser32]::GetSystemMetrics(1)) }",
5666
+ "$MOVEABS = 0x0001 -bor 0x8000 -bor 0x4000",
5532
5667
  "$LEFTDOWN = 0x0002; $LEFTUP = 0x0004; $RIGHTDOWN = 0x0008; $RIGHTUP = 0x0010;",
5533
5668
  "$MIDDLEDOWN = 0x0020; $MIDDLEUP = 0x0040; $WHEEL = 0x0800;",
5534
5669
  ];
@@ -5540,7 +5675,13 @@ async function fsRemoteControlInput(payload) {
5540
5675
  : null;
5541
5676
  const lines = [...psPrelude];
5542
5677
  if (x != null && y != null) {
5543
- lines.push(`[ForgeRcUser32]::SetCursorPos(${x}, ${y}) | Out-Null`);
5678
+ lines.push(`$__mx = ${x}`);
5679
+ lines.push(`$__my = ${y}`);
5680
+ lines.push("$__ax = [Math]::Round((($__mx - $__vx) * 65535.0) / [Math]::Max(1, $__vw - 1))");
5681
+ lines.push("$__ay = [Math]::Round((($__my - $__vy) * 65535.0) / [Math]::Max(1, $__vh - 1))");
5682
+ lines.push("if ($__ax -lt 0) { $__ax = 0 } elseif ($__ax -gt 65535) { $__ax = 65535 }");
5683
+ lines.push("if ($__ay -lt 0) { $__ay = 0 } elseif ($__ay -gt 65535) { $__ay = 65535 }");
5684
+ lines.push("[ForgeRcUser32]::mouse_event($MOVEABS, [uint32]$__ax, [uint32]$__ay, 0, [UIntPtr]::Zero)");
5544
5685
  }
5545
5686
  if (action === "mouse_move") {
5546
5687
  if (x == null || y == null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-jsxy",
3
- "version": "1.0.74",
3
+ "version": "1.0.76",
4
4
  "description": "Node.js integration layer for Autodesk Forge",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",