autokap 1.3.21 → 1.3.23
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/dist/browser.js +16 -2
- package/dist/xvfb-process.d.ts +28 -4
- package/dist/xvfb-process.js +65 -4
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -885,11 +885,25 @@ export class Browser {
|
|
|
885
885
|
}
|
|
886
886
|
// Kiosk + zero-position anchor for Xvfb: Chromium normally renders its
|
|
887
887
|
// own toolbar/tabbar in headed mode, which would appear at the top of
|
|
888
|
-
// every clip. `--kiosk` removes
|
|
889
|
-
// `--window-size` make the page fill the
|
|
888
|
+
// every clip. `--kiosk` removes the address bar + tab strip;
|
|
889
|
+
// `--window-position=0,0` and `--window-size` make the page fill the
|
|
890
|
+
// Xvfb screen exactly. The `--disable-features` block kills the
|
|
891
|
+
// separate "infobar" surfaces (translate suggestion, save-password
|
|
892
|
+
// prompt, autofill banner, "Chrome is being controlled by automated
|
|
893
|
+
// software" warning) — these render OUTSIDE kiosk's chrome and would
|
|
894
|
+
// otherwise show up at the top of every clip captured via x11grab.
|
|
895
|
+
// CDP screenshot capture (Mac/Win/local Linux) hits the page surface
|
|
896
|
+
// directly so it never sees these; only ffmpeg x11grab does.
|
|
890
897
|
const xvfbWindowArgs = isLinuxWithGpu ? [
|
|
891
898
|
'--kiosk',
|
|
892
899
|
'--window-position=0,0',
|
|
900
|
+
'--disable-features=Translate,TranslateUI,AutofillServerCommunication,InfoBars',
|
|
901
|
+
'--disable-infobars',
|
|
902
|
+
'--disable-blink-features=AutomationControlled',
|
|
903
|
+
'--disable-translate',
|
|
904
|
+
'--no-default-browser-check',
|
|
905
|
+
'--no-first-run',
|
|
906
|
+
'--noerrdialogs',
|
|
893
907
|
] : [];
|
|
894
908
|
const clipArgs = [
|
|
895
909
|
...baseArgs,
|
package/dist/xvfb-process.d.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Xvfb (X virtual framebuffer) process wrapper
|
|
2
|
+
* Xvfb (X virtual framebuffer) process wrapper, plus a minimal window
|
|
3
|
+
* manager (matchbox) so headed Chromium's --kiosk flag actually takes
|
|
4
|
+
* effect.
|
|
3
5
|
*
|
|
4
6
|
* Spins up a virtual X display that headed Chromium can render to. Used by
|
|
5
7
|
* cloud clip capture so the recording surface is reachable by ffmpeg via
|
|
6
8
|
* `x11grab` — bypassing the slow `Page.captureScreenshot` CDP path that
|
|
7
9
|
* software-rasterized Linux compositors cap at ~6 fps on heavy React UIs.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* Why matchbox: Chromium's --kiosk / --start-fullscreen flags request
|
|
12
|
+
* fullscreen mode from the WM via _NET_WM_STATE_FULLSCREEN. Without a WM
|
|
13
|
+
* to honor that request, Chromium silently falls back to a normal
|
|
14
|
+
* decorated window with the full chrome (tab bar, address bar, infobars)
|
|
15
|
+
* visible — and ffmpeg x11grab captures all of it, ruining clips.
|
|
16
|
+
* matchbox-window-manager (~100KB, designed for embedded kiosks)
|
|
17
|
+
* auto-fullscreens every window with no decorations. With matchbox,
|
|
18
|
+
* --kiosk takes effect and the page fills the whole framebuffer cleanly.
|
|
19
|
+
*
|
|
20
|
+
* Lifecycle: Xvfb + matchbox both run for the entire browser process
|
|
21
|
+
* lifetime. ffmpeg recording starts/stops per BEGIN_CLIP/END_CLIP and
|
|
22
|
+
* grabs from the same display.
|
|
12
23
|
*/
|
|
13
24
|
export interface XvfbProcessOptions {
|
|
14
25
|
/** Display number (without leading colon). E.g. 99 → DISPLAY=:99. */
|
|
@@ -21,10 +32,23 @@ export interface XvfbProcessOptions {
|
|
|
21
32
|
export declare class XvfbProcess {
|
|
22
33
|
private readonly opts;
|
|
23
34
|
private process;
|
|
35
|
+
private wmProcess;
|
|
24
36
|
private exited;
|
|
25
37
|
constructor(opts: XvfbProcessOptions);
|
|
26
38
|
/** DISPLAY string suitable for `process.env.DISPLAY` (e.g. `:99`). */
|
|
27
39
|
get display(): string;
|
|
28
40
|
start(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Start matchbox-window-manager. Required for Chromium's --kiosk to
|
|
43
|
+
* actually take effect: kiosk requests fullscreen via the WM, and
|
|
44
|
+
* without a WM Chromium falls back to a normal decorated window with
|
|
45
|
+
* the full chrome leaking into x11grab captures. matchbox is tiny
|
|
46
|
+
* (~100KB), auto-fullscreens every window, and removes decorations.
|
|
47
|
+
* `-use_titlebar no` and `-use_cursor no` are belt-and-suspenders;
|
|
48
|
+
* matchbox already defaults to no decorations, but the explicit flags
|
|
49
|
+
* also stop it from drawing its own cursor (which would superimpose
|
|
50
|
+
* on Chromium's cursor and confuse the cursor overlay script).
|
|
51
|
+
*/
|
|
52
|
+
private startWindowManager;
|
|
29
53
|
stop(): Promise<void>;
|
|
30
54
|
}
|
package/dist/xvfb-process.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Xvfb (X virtual framebuffer) process wrapper
|
|
2
|
+
* Xvfb (X virtual framebuffer) process wrapper, plus a minimal window
|
|
3
|
+
* manager (matchbox) so headed Chromium's --kiosk flag actually takes
|
|
4
|
+
* effect.
|
|
3
5
|
*
|
|
4
6
|
* Spins up a virtual X display that headed Chromium can render to. Used by
|
|
5
7
|
* cloud clip capture so the recording surface is reachable by ffmpeg via
|
|
6
8
|
* `x11grab` — bypassing the slow `Page.captureScreenshot` CDP path that
|
|
7
9
|
* software-rasterized Linux compositors cap at ~6 fps on heavy React UIs.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* Why matchbox: Chromium's --kiosk / --start-fullscreen flags request
|
|
12
|
+
* fullscreen mode from the WM via _NET_WM_STATE_FULLSCREEN. Without a WM
|
|
13
|
+
* to honor that request, Chromium silently falls back to a normal
|
|
14
|
+
* decorated window with the full chrome (tab bar, address bar, infobars)
|
|
15
|
+
* visible — and ffmpeg x11grab captures all of it, ruining clips.
|
|
16
|
+
* matchbox-window-manager (~100KB, designed for embedded kiosks)
|
|
17
|
+
* auto-fullscreens every window with no decorations. With matchbox,
|
|
18
|
+
* --kiosk takes effect and the page fills the whole framebuffer cleanly.
|
|
19
|
+
*
|
|
20
|
+
* Lifecycle: Xvfb + matchbox both run for the entire browser process
|
|
21
|
+
* lifetime. ffmpeg recording starts/stops per BEGIN_CLIP/END_CLIP and
|
|
22
|
+
* grabs from the same display.
|
|
12
23
|
*/
|
|
13
24
|
import { spawn } from 'node:child_process';
|
|
14
25
|
import fs from 'node:fs/promises';
|
|
@@ -16,9 +27,11 @@ import { logger } from './logger.js';
|
|
|
16
27
|
const XVFB_READY_TIMEOUT_MS = 5_000;
|
|
17
28
|
const XVFB_READY_POLL_MS = 50;
|
|
18
29
|
const XVFB_STOP_GRACE_MS = 2_000;
|
|
30
|
+
const WM_STARTUP_GRACE_MS = 200;
|
|
19
31
|
export class XvfbProcess {
|
|
20
32
|
opts;
|
|
21
33
|
process = null;
|
|
34
|
+
wmProcess = null;
|
|
22
35
|
exited = false;
|
|
23
36
|
constructor(opts) {
|
|
24
37
|
this.opts = opts;
|
|
@@ -72,6 +85,7 @@ export class XvfbProcess {
|
|
|
72
85
|
await fs.access(socketPath);
|
|
73
86
|
logger.info(`[xvfb] ready on display ${this.display} (${this.opts.width}×${this.opts.height}) ` +
|
|
74
87
|
`after ${Date.now() - startedAt}ms`);
|
|
88
|
+
await this.startWindowManager();
|
|
75
89
|
return;
|
|
76
90
|
}
|
|
77
91
|
catch {
|
|
@@ -81,11 +95,58 @@ export class XvfbProcess {
|
|
|
81
95
|
}
|
|
82
96
|
throw new Error(`Xvfb did not become ready within ${XVFB_READY_TIMEOUT_MS}ms`);
|
|
83
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Start matchbox-window-manager. Required for Chromium's --kiosk to
|
|
100
|
+
* actually take effect: kiosk requests fullscreen via the WM, and
|
|
101
|
+
* without a WM Chromium falls back to a normal decorated window with
|
|
102
|
+
* the full chrome leaking into x11grab captures. matchbox is tiny
|
|
103
|
+
* (~100KB), auto-fullscreens every window, and removes decorations.
|
|
104
|
+
* `-use_titlebar no` and `-use_cursor no` are belt-and-suspenders;
|
|
105
|
+
* matchbox already defaults to no decorations, but the explicit flags
|
|
106
|
+
* also stop it from drawing its own cursor (which would superimpose
|
|
107
|
+
* on Chromium's cursor and confuse the cursor overlay script).
|
|
108
|
+
*/
|
|
109
|
+
async startWindowManager() {
|
|
110
|
+
this.wmProcess = spawn('matchbox-window-manager', ['-use_titlebar', 'no', '-use_cursor', 'no'], {
|
|
111
|
+
env: { ...process.env, DISPLAY: this.display },
|
|
112
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
113
|
+
detached: false,
|
|
114
|
+
});
|
|
115
|
+
this.wmProcess.stderr?.on('data', (chunk) => {
|
|
116
|
+
const text = String(chunk).trim();
|
|
117
|
+
if (text)
|
|
118
|
+
logger.warn(`[matchbox-wm] ${text}`);
|
|
119
|
+
});
|
|
120
|
+
this.wmProcess.on('exit', (code, signal) => {
|
|
121
|
+
// Don't mark this.exited — Xvfb is still alive. Just log if abnormal.
|
|
122
|
+
if (code !== 0 && code !== null) {
|
|
123
|
+
logger.warn(`[matchbox-wm] exited unexpectedly: code=${code} signal=${signal}`);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
this.wmProcess.on('error', (err) => {
|
|
127
|
+
logger.error(`[matchbox-wm] spawn error: ${err.message}`);
|
|
128
|
+
});
|
|
129
|
+
// Brief grace so matchbox can register as the WM with the X server
|
|
130
|
+
// before Chromium opens its first window.
|
|
131
|
+
await new Promise(r => setTimeout(r, WM_STARTUP_GRACE_MS));
|
|
132
|
+
logger.info(`[matchbox-wm] started on display ${this.display}`);
|
|
133
|
+
}
|
|
84
134
|
async stop() {
|
|
85
135
|
if (!this.process)
|
|
86
136
|
return;
|
|
87
137
|
const proc = this.process;
|
|
88
138
|
this.process = null;
|
|
139
|
+
const wmProc = this.wmProcess;
|
|
140
|
+
this.wmProcess = null;
|
|
141
|
+
// Tear down matchbox first — it would die when Xvfb goes anyway, but
|
|
142
|
+
// killing it explicitly avoids a SIGPIPE / "X connection lost" warning
|
|
143
|
+
// in the logs.
|
|
144
|
+
if (wmProc) {
|
|
145
|
+
try {
|
|
146
|
+
wmProc.kill('SIGTERM');
|
|
147
|
+
}
|
|
148
|
+
catch { /* already dead */ }
|
|
149
|
+
}
|
|
89
150
|
proc.kill('SIGTERM');
|
|
90
151
|
await new Promise(resolve => {
|
|
91
152
|
const timer = setTimeout(() => {
|