@remcp/runtime 0.2.120 → 0.2.122
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/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/src/extended/catalog.mjs +4 -3
- package/src/extended/diagnostics.mjs +233 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.122 — 2026-09-30
|
|
4
|
+
|
|
5
|
+
- No runtime tool names, schemas, or device-side implementations change in this release. The runtime package version remains in lockstep with the Windows bootstrap release, which pins the private Windows Node execution engine for reproducible installs.
|
|
6
|
+
|
|
7
|
+
## 0.2.121 — 2026-09-29
|
|
8
|
+
|
|
9
|
+
- record_screen gains a GNOME Shell Screencast backend for GNOME Wayland, so a native GNOME desktop no longer needs wf-recorder merely to advertise and perform bounded screen recording. The backend keeps start/stop on one D-Bus connection and produces WebM.
|
|
10
|
+
- Capability discovery probes ScreencastSupported fail-closed, caches the result for 15 seconds, and coalesces concurrent probes before tools/list_changed can expose the capability. Existing wf-recorder, X11 ffmpeg, AVFoundation and gdigrab paths remain available.
|
|
11
|
+
- Explicit recording destinations must use an extension matching the actual container (.webm for native GNOME Wayland, .mp4 for the other current backends).
|
|
12
|
+
|
|
3
13
|
## 0.2.116 — 2026-09-29
|
|
4
14
|
|
|
5
15
|
- Linux Wayland `list_windows` stops its AT-SPI inventory traversal at top-level windows instead of walking descendant controls. On the verified ASUS desktop this kept the same 13 stable window records while cutting the call from roughly 7.2 seconds to roughly 2.1 seconds; `computer_snapshot` first-look latency fell from 7.50 seconds to 2.45 seconds in the same state.
|
package/package.json
CHANGED
package/src/extended/catalog.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { isWaylandSession } from '../screenshot-portal.mjs';
|
|
|
9
9
|
import { waylandPortalCandidate } from '../wayland-remote-desktop.mjs';
|
|
10
10
|
import { commandExists, runFile } from './common.mjs';
|
|
11
11
|
import { desktopHandlers } from './desktop.mjs';
|
|
12
|
-
import { diagnosticHandlers, recordScreenAvailable } from './diagnostics.mjs';
|
|
12
|
+
import { diagnosticHandlers, gnomeScreencastSupported, recordScreenAvailable } from './diagnostics.mjs';
|
|
13
13
|
import { documentHandlers } from './documents.mjs';
|
|
14
14
|
|
|
15
15
|
const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
@@ -566,7 +566,7 @@ export const extendedToolDefinitions = [
|
|
|
566
566
|
define('environment', 'Runtime environment', 'Report platform, architecture, Node version, working/home/temp paths, shell, PATH, and optionally environment variables. Environment variables are opt-in and secret-looking or credential-bearing values are masked.', o({ include_env:b('Include sanitized environment variables; default false.') }), readOnly, diagnosticHandlers.environment),
|
|
567
567
|
define('audio', 'Audio control', 'Read or change default output volume or mute state using native operating-system audio controls when available.', o({ action:e(['status','set_volume','mute','unmute']), volume:n('0..100 for set_volume.') }, ['action']), mutatingNonDestructive, diagnosticHandlers.audio, ['audio']),
|
|
568
568
|
define('power_action', 'Power action', 'Lock, sleep, restart, or shut down the paired computer using native operating-system power facilities. The action passes through ReMCP command policy.', o({ action:e(['lock','sleep','restart','shutdown']), delay_seconds:n('Optional delay, maximum 3600 seconds.') }, ['action']), mutating, diagnosticHandlers.power_action, ['power']),
|
|
569
|
-
define('record_screen', 'Record screen', 'Record a short bounded desktop video to a permitted local file for debugging and return its path and size. Uses native/available recording helpers.', o({ duration_seconds:n('1..120 seconds.'), fps:n('1..60.'), destination:s('Permitted output path.') }), mutating, diagnosticHandlers.record_screen, ['record']),
|
|
569
|
+
define('record_screen', 'Record screen', 'Record a short bounded desktop video to a permitted local file for debugging and return its path and size. Uses native/available recording helpers.', o({ duration_seconds:n('1..120 seconds.'), fps:n('1..60.'), destination:s('Permitted output path. If an extension is supplied, use .webm for native GNOME Wayland recording and .mp4 for the other recorder backends.') }), mutating, diagnosticHandlers.record_screen, ['record']),
|
|
570
570
|
|
|
571
571
|
define('read_document', 'Read document', 'Read PDF, DOCX, XLSX, TXT, Markdown, CSV, JSON, or XML. PDF/DOCX extraction is built in and XLSX is parsed from its OOXML worksheet cells.', o({ path:s('Document path.'), sheet:s('Worksheet name for XLSX.'), max_cells:n() }, ['path']), readOnly, documentHandlers.read_document, ['documents']),
|
|
572
572
|
define('edit_spreadsheet', 'Edit spreadsheet', 'Create a new XLSX workbook or edit up to 500 expanded XLSX cells directly in OOXML without launching Excel. For creation set create=true and use path as a new destination; existing files are never overwritten by create mode. Each edit may target one cell or a rectangular range, with a scalar fill, a values matrix, or a formula.', o({ path:s('Existing .xlsx path, or the new workbook destination when create=true.'), create:b('Create a new XLSX at path. Fails if path already exists; do not combine with output.'), output:s('Optional output .xlsx when editing an existing workbook; defaults to replacing input.'), sheet:s('Worksheet name. For create=true, defaults to Sheet1 and becomes the new workbook sheet name.'), edits:{ type:'array', maxItems:500, items:o({ cell:s('Single A1 cell reference.'), range:s('Rectangular range such as A1:C3.'), value:{ description:'Scalar value for one cell or to fill a range.' }, values:{ type:'array', items:{ type:'array', items:{} }, description:'2D matrix matching the range dimensions.' }, formula:s('Optional formula; for a range it is written to each expanded cell.') }) } }, ['path','edits']), mutating, documentHandlers.edit_spreadsheet, ['documents']),
|
|
@@ -604,6 +604,7 @@ export async function capabilitySnapshot() {
|
|
|
604
604
|
const linux = process.platform === 'linux';
|
|
605
605
|
const pyAtSpi = linux ? await linuxHasPyAtSpi() : false;
|
|
606
606
|
const wayland = linux && isWaylandSession();
|
|
607
|
+
const gnomeRecord = wayland ? await gnomeScreencastSupported() : false;
|
|
607
608
|
const portalInput = wayland && waylandPortalCandidate();
|
|
608
609
|
const x11Input = linux && !wayland && (commandExists('wdotool') || commandExists('xdotool'));
|
|
609
610
|
const browserCdp = browserRemoteEnabled() && await browserControlAvailable(undefined, 500);
|
|
@@ -626,7 +627,7 @@ export async function capabilitySnapshot() {
|
|
|
626
627
|
apps: win || mac || commandExists('dpkg-query') || commandExists('rpm'),
|
|
627
628
|
audio: win || mac || commandExists('wpctl') || commandExists('pactl') || commandExists('amixer'),
|
|
628
629
|
power: win || mac || commandExists('systemctl') || commandExists('loginctl'),
|
|
629
|
-
record: recordScreenAvailable({ platform:process.platform, wayland }),
|
|
630
|
+
record: recordScreenAvailable({ platform:process.platform, wayland, gnomeSupported:gnomeRecord }),
|
|
630
631
|
ooxml: linux && (commandExists('zip') && commandExists('unzip')),
|
|
631
632
|
};
|
|
632
633
|
}
|
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { constants, existsSync } from 'node:fs';
|
|
8
|
-
import { lstat, mkdtemp, open, rename, rm, stat, unlink } from 'node:fs/promises';
|
|
8
|
+
import { lstat, mkdtemp, open, rename, rm, stat, unlink, writeFile } from 'node:fs/promises';
|
|
9
9
|
import { assertAllowedCommand } from '../policy.mjs';
|
|
10
10
|
import { isWaylandSession } from '../screenshot-portal.mjs';
|
|
11
11
|
import { openDirectoryPath } from '../tools/files.mjs';
|
|
@@ -282,21 +282,228 @@ async function avfoundationScreenInput(ffmpeg) {
|
|
|
282
282
|
return index;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
export function
|
|
285
|
+
export function gnomeScreencastCandidate({
|
|
286
|
+
platform = process.platform,
|
|
287
|
+
wayland = platform === 'linux' ? isWaylandSession() : false,
|
|
288
|
+
desktop = process.env.XDG_CURRENT_DESKTOP || '',
|
|
289
|
+
gnomeSessionMode = process.env.GNOME_SHELL_SESSION_MODE || '',
|
|
290
|
+
sessionBus = process.env.DBUS_SESSION_BUS_ADDRESS || '',
|
|
291
|
+
commandExistsFn = commandExists,
|
|
292
|
+
} = {}) {
|
|
293
|
+
const gnomeSession = /gnome/i.test(String(desktop)) || Boolean(String(gnomeSessionMode).trim());
|
|
294
|
+
return platform === 'linux'
|
|
295
|
+
&& wayland
|
|
296
|
+
&& gnomeSession
|
|
297
|
+
&& Boolean(String(sessionBus).trim())
|
|
298
|
+
&& commandExistsFn('gjs');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function recordScreenBackend({
|
|
286
302
|
platform = process.platform,
|
|
287
303
|
wayland = platform === 'linux' ? isWaylandSession() : false,
|
|
288
304
|
display = process.env.DISPLAY || '',
|
|
305
|
+
desktop = process.env.XDG_CURRENT_DESKTOP || '',
|
|
306
|
+
gnomeSessionMode = process.env.GNOME_SHELL_SESSION_MODE || '',
|
|
307
|
+
sessionBus = process.env.DBUS_SESSION_BUS_ADDRESS || '',
|
|
308
|
+
gnomeSupported = false,
|
|
289
309
|
commandExistsFn = commandExists,
|
|
290
310
|
existsSyncFn = existsSync,
|
|
291
311
|
} = {}) {
|
|
292
312
|
if (platform === 'linux') {
|
|
293
|
-
if (wayland)
|
|
294
|
-
|
|
313
|
+
if (wayland) {
|
|
314
|
+
if (gnomeSupported && gnomeScreencastCandidate({
|
|
315
|
+
platform,
|
|
316
|
+
wayland,
|
|
317
|
+
desktop,
|
|
318
|
+
gnomeSessionMode,
|
|
319
|
+
sessionBus,
|
|
320
|
+
commandExistsFn,
|
|
321
|
+
})) return 'gnome-shell';
|
|
322
|
+
if (commandExistsFn('wf-recorder') && commandExistsFn('timeout')) return 'wf-recorder';
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return Boolean(String(display).trim()) && commandExistsFn('ffmpeg') ? 'ffmpeg-x11' : null;
|
|
326
|
+
}
|
|
327
|
+
if (platform === 'darwin') {
|
|
328
|
+
return resolveRecordScreenFfmpeg({ platform, commandExistsFn, existsSyncFn }) ? 'ffmpeg-avfoundation' : null;
|
|
295
329
|
}
|
|
296
|
-
if (platform === '
|
|
297
|
-
return
|
|
330
|
+
if (platform === 'win32') {
|
|
331
|
+
return resolveRecordScreenFfmpeg({ platform, commandExistsFn, existsSyncFn }) ? 'ffmpeg-gdigrab' : null;
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function recordScreenAvailable({
|
|
337
|
+
platform = process.platform,
|
|
338
|
+
wayland = platform === 'linux' ? isWaylandSession() : false,
|
|
339
|
+
display = process.env.DISPLAY || '',
|
|
340
|
+
desktop = process.env.XDG_CURRENT_DESKTOP || '',
|
|
341
|
+
gnomeSessionMode = process.env.GNOME_SHELL_SESSION_MODE || '',
|
|
342
|
+
sessionBus = process.env.DBUS_SESSION_BUS_ADDRESS || '',
|
|
343
|
+
gnomeSupported = false,
|
|
344
|
+
commandExistsFn = commandExists,
|
|
345
|
+
existsSyncFn = existsSync,
|
|
346
|
+
} = {}) {
|
|
347
|
+
return Boolean(recordScreenBackend({
|
|
348
|
+
platform,
|
|
349
|
+
wayland,
|
|
350
|
+
display,
|
|
351
|
+
desktop,
|
|
352
|
+
gnomeSessionMode,
|
|
353
|
+
sessionBus,
|
|
354
|
+
gnomeSupported,
|
|
355
|
+
commandExistsFn,
|
|
356
|
+
existsSyncFn,
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const GNOME_SCREENCAST_SUPPORT_HELPER = `const {Gio, GLib} = imports.gi;
|
|
361
|
+
const bus = Gio.bus_get_sync(Gio.BusType.SESSION, null);
|
|
362
|
+
const value = bus.call_sync(
|
|
363
|
+
'org.gnome.Shell.Screencast',
|
|
364
|
+
'/org/gnome/Shell/Screencast',
|
|
365
|
+
'org.freedesktop.DBus.Properties',
|
|
366
|
+
'Get',
|
|
367
|
+
new GLib.Variant('(ss)', ['org.gnome.Shell.Screencast', 'ScreencastSupported']),
|
|
368
|
+
new GLib.VariantType('(v)'),
|
|
369
|
+
Gio.DBusCallFlags.NONE,
|
|
370
|
+
3000,
|
|
371
|
+
null
|
|
372
|
+
).deepUnpack();
|
|
373
|
+
print(value[0].deepUnpack() ? 'true' : 'false');
|
|
374
|
+
`;
|
|
375
|
+
|
|
376
|
+
let gnomeScreencastSupportCache = { key:'', value:false, expiresAt:0 };
|
|
377
|
+
let gnomeScreencastSupportProbe = { key:'', promise:null };
|
|
378
|
+
|
|
379
|
+
export async function gnomeScreencastSupported({
|
|
380
|
+
platform = process.platform,
|
|
381
|
+
wayland = platform === 'linux' ? isWaylandSession() : false,
|
|
382
|
+
desktop = process.env.XDG_CURRENT_DESKTOP || '',
|
|
383
|
+
gnomeSessionMode = process.env.GNOME_SHELL_SESSION_MODE || '',
|
|
384
|
+
sessionBus = process.env.DBUS_SESSION_BUS_ADDRESS || '',
|
|
385
|
+
commandExistsFn = commandExists,
|
|
386
|
+
runFileFn = runFile,
|
|
387
|
+
cacheMs = 15_000,
|
|
388
|
+
nowFn = Date.now,
|
|
389
|
+
} = {}) {
|
|
390
|
+
if (!gnomeScreencastCandidate({
|
|
391
|
+
platform,
|
|
392
|
+
wayland,
|
|
393
|
+
desktop,
|
|
394
|
+
gnomeSessionMode,
|
|
395
|
+
sessionBus,
|
|
396
|
+
commandExistsFn,
|
|
397
|
+
})) return false;
|
|
398
|
+
|
|
399
|
+
const key = [platform, wayland ? 'wayland' : '', desktop, gnomeSessionMode, sessionBus].join('|');
|
|
400
|
+
const now = nowFn();
|
|
401
|
+
if (cacheMs > 0 && gnomeScreencastSupportCache.key === key && gnomeScreencastSupportCache.expiresAt > now) {
|
|
402
|
+
return gnomeScreencastSupportCache.value;
|
|
403
|
+
}
|
|
404
|
+
if (gnomeScreencastSupportProbe.key === key && gnomeScreencastSupportProbe.promise) {
|
|
405
|
+
return gnomeScreencastSupportProbe.promise;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const promise = (async () => {
|
|
409
|
+
let value = false;
|
|
410
|
+
try {
|
|
411
|
+
const result = await runFileFn('gjs', ['-c', GNOME_SCREENCAST_SUPPORT_HELPER], {
|
|
412
|
+
label:'GNOME Shell screencast capability probe',
|
|
413
|
+
timeout:5000,
|
|
414
|
+
maxBuffer:64 * 1024,
|
|
415
|
+
allowFailure:true,
|
|
416
|
+
});
|
|
417
|
+
value = Number(result.code) === 0 && String(result.stdout || '').trim() === 'true';
|
|
418
|
+
} catch {
|
|
419
|
+
value = false;
|
|
420
|
+
}
|
|
421
|
+
gnomeScreencastSupportCache = {
|
|
422
|
+
key,
|
|
423
|
+
value,
|
|
424
|
+
expiresAt:nowFn() + Math.max(0, cacheMs),
|
|
425
|
+
};
|
|
426
|
+
return value;
|
|
427
|
+
})();
|
|
428
|
+
|
|
429
|
+
gnomeScreencastSupportProbe = { key, promise };
|
|
430
|
+
try {
|
|
431
|
+
return await promise;
|
|
432
|
+
} finally {
|
|
433
|
+
if (gnomeScreencastSupportProbe.key === key && gnomeScreencastSupportProbe.promise === promise) {
|
|
434
|
+
gnomeScreencastSupportProbe = { key:'', promise:null };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const GNOME_SCREENCAST_HELPER = `const {Gio, GLib} = imports.gi;
|
|
440
|
+
const [destination, secondsRaw, fpsRaw] = ARGV;
|
|
441
|
+
const seconds = Number(secondsRaw);
|
|
442
|
+
const fps = Number(fpsRaw);
|
|
443
|
+
const bus = Gio.bus_get_sync(Gio.BusType.SESSION, null);
|
|
444
|
+
const destinationName = 'org.gnome.Shell.Screencast';
|
|
445
|
+
const objectPath = '/org/gnome/Shell/Screencast';
|
|
446
|
+
const interfaceName = 'org.gnome.Shell.Screencast';
|
|
447
|
+
const property = bus.call_sync(
|
|
448
|
+
destinationName,
|
|
449
|
+
objectPath,
|
|
450
|
+
'org.freedesktop.DBus.Properties',
|
|
451
|
+
'Get',
|
|
452
|
+
new GLib.Variant('(ss)', [interfaceName, 'ScreencastSupported']),
|
|
453
|
+
new GLib.VariantType('(v)'),
|
|
454
|
+
Gio.DBusCallFlags.NONE,
|
|
455
|
+
5000,
|
|
456
|
+
null
|
|
457
|
+
).deepUnpack();
|
|
458
|
+
if (!property[0].deepUnpack()) throw new Error('GNOME Shell reports ScreencastSupported=false');
|
|
459
|
+
const options = {
|
|
460
|
+
'draw-cursor': new GLib.Variant('b', true),
|
|
461
|
+
'framerate': new GLib.Variant('i', fps),
|
|
462
|
+
};
|
|
463
|
+
const started = bus.call_sync(
|
|
464
|
+
destinationName,
|
|
465
|
+
objectPath,
|
|
466
|
+
interfaceName,
|
|
467
|
+
'Screencast',
|
|
468
|
+
new GLib.Variant('(sa{sv})', [destination, options]),
|
|
469
|
+
new GLib.VariantType('(bs)'),
|
|
470
|
+
Gio.DBusCallFlags.NONE,
|
|
471
|
+
10000,
|
|
472
|
+
null
|
|
473
|
+
).deepUnpack();
|
|
474
|
+
if (!started[0]) throw new Error('GNOME Shell refused to start screencast');
|
|
475
|
+
GLib.usleep(Math.round(seconds * 1000000));
|
|
476
|
+
const stopped = bus.call_sync(
|
|
477
|
+
destinationName,
|
|
478
|
+
objectPath,
|
|
479
|
+
interfaceName,
|
|
480
|
+
'StopScreencast',
|
|
481
|
+
null,
|
|
482
|
+
new GLib.VariantType('(b)'),
|
|
483
|
+
Gio.DBusCallFlags.NONE,
|
|
484
|
+
10000,
|
|
485
|
+
null
|
|
486
|
+
).deepUnpack();
|
|
487
|
+
if (!stopped[0]) throw new Error('GNOME Shell did not stop screencast cleanly');
|
|
488
|
+
print(JSON.stringify({ destination:started[1], stopped:stopped[0] }));
|
|
489
|
+
`;
|
|
490
|
+
|
|
491
|
+
async function recordGnomeShellScreen({ staged, stagingDirectory, seconds, fps }) {
|
|
492
|
+
const helper = path.join(stagingDirectory, 'gnome-screencast.js');
|
|
493
|
+
await writeFile(helper, GNOME_SCREENCAST_HELPER, { mode:0o600 });
|
|
494
|
+
await runFile('gjs', [helper, staged, String(seconds), String(fps)], {
|
|
495
|
+
label:'GNOME Shell screen recording',
|
|
496
|
+
timeout:(seconds + 20) * 1000,
|
|
497
|
+
maxBuffer:2 * 1024 * 1024,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function validateRecordingDestinationFormat(destination, format) {
|
|
502
|
+
if (!destination) return;
|
|
503
|
+
const extension = path.extname(destination).slice(1).toLowerCase();
|
|
504
|
+
if (extension && extension !== format) {
|
|
505
|
+
throw new Error(`Screen recording backend produces .${format}; destination must use .${format} or omit the extension`);
|
|
298
506
|
}
|
|
299
|
-
return false;
|
|
300
507
|
}
|
|
301
508
|
|
|
302
509
|
async function openRecordingParent(destination) {
|
|
@@ -348,24 +555,37 @@ async function copyRecordingFile(source, destination) {
|
|
|
348
555
|
export async function recordScreen(args) {
|
|
349
556
|
const seconds = clamp(args.duration_seconds, 5, 1, 120);
|
|
350
557
|
const fps = clamp(args.fps, 15, 1, 60);
|
|
558
|
+
const wayland = process.platform === 'linux' && isWaylandSession();
|
|
559
|
+
const gnomeSupported = wayland ? await gnomeScreencastSupported() : false;
|
|
560
|
+
const backend = recordScreenBackend({ platform:process.platform, wayland, gnomeSupported });
|
|
561
|
+
if (!backend) {
|
|
562
|
+
if (wayland) unavailable('Screen recording', 'GNOME Shell Screencast or wf-recorder with timeout is required on Wayland');
|
|
563
|
+
if (process.platform === 'linux' && !String(process.env.DISPLAY || '').trim()) unavailable('Screen recording', 'an active X11 DISPLAY is required on Linux X11');
|
|
564
|
+
if (process.platform === 'darwin') unavailable('Screen recording', 'ffmpeg is required on macOS');
|
|
565
|
+
if (process.platform === 'win32') unavailable('Screen recording', 'ffmpeg is required on Windows');
|
|
566
|
+
unavailable('Screen recording', 'ffmpeg is required on X11');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const format = backend === 'gnome-shell' ? 'webm' : 'mp4';
|
|
351
570
|
const requestedDestination = args.destination ? await resolveSafePath(args.destination, 'destination') : '';
|
|
352
571
|
if (requestedDestination) {
|
|
572
|
+
validateRecordingDestinationFormat(requestedDestination, format);
|
|
353
573
|
const existing = await lstat(requestedDestination).catch(() => null);
|
|
354
574
|
if (existing?.isSymbolicLink()) throw new Error('Screen recording destination cannot be a symbolic link');
|
|
355
575
|
if (existing && !existing.isFile()) throw new Error('Screen recording destination is not a regular file');
|
|
356
576
|
}
|
|
577
|
+
|
|
357
578
|
const stagingDirectory = await mkdtemp(path.join(os.tmpdir(), 'remcp-screen-'));
|
|
358
|
-
const staged = path.join(stagingDirectory, `capture-${randomUUID()}
|
|
359
|
-
const wayland = process.platform === 'linux' && isWaylandSession();
|
|
579
|
+
const staged = path.join(stagingDirectory, `capture-${randomUUID()}.${format}`);
|
|
360
580
|
let keepStaging = false;
|
|
361
581
|
try {
|
|
362
|
-
if (
|
|
582
|
+
if (backend === 'gnome-shell') {
|
|
583
|
+
await recordGnomeShellScreen({ staged, stagingDirectory, seconds, fps });
|
|
584
|
+
} else if (backend === 'wf-recorder') {
|
|
363
585
|
const result = await runFile('timeout', ['--signal=INT', `${seconds}s`, 'wf-recorder', '-f',staged,'-r',String(fps),'-c','libx264'], { label:'screen recording', timeout:(seconds+10)*1000, allowFailure:true });
|
|
364
586
|
if (![0, 124, 130].includes(Number(result.code))) throw new Error(result.stderr.trim() || `wf-recorder exited ${result.code}`);
|
|
365
587
|
} else {
|
|
366
588
|
const ffmpeg = resolveRecordScreenFfmpeg();
|
|
367
|
-
if (wayland) unavailable('Screen recording', 'wf-recorder and timeout are required on Wayland');
|
|
368
|
-
if (process.platform === 'linux' && !String(process.env.DISPLAY || '').trim()) unavailable('Screen recording', 'an active X11 DISPLAY is required on Linux X11');
|
|
369
589
|
if (!ffmpeg) {
|
|
370
590
|
if (process.platform === 'darwin') unavailable('Screen recording', 'ffmpeg is required on macOS');
|
|
371
591
|
if (process.platform === 'win32') unavailable('Screen recording', 'ffmpeg is required on Windows');
|
|
@@ -386,7 +606,7 @@ export async function recordScreen(args) {
|
|
|
386
606
|
const output = requestedDestination || staged;
|
|
387
607
|
const info = await stat(output);
|
|
388
608
|
keepStaging = !requestedDestination;
|
|
389
|
-
return jsonResult({ path:output, bytes:info.size, duration_seconds:seconds, format
|
|
609
|
+
return jsonResult({ path:output, bytes:info.size, duration_seconds:seconds, format });
|
|
390
610
|
} finally {
|
|
391
611
|
if (!keepStaging) await rm(stagingDirectory, { recursive:true, force:true }).catch(() => {});
|
|
392
612
|
}
|