autokap 1.3.22 → 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.
@@ -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
- * Lifecycle: Xvfb runs for the entire browser process lifetime. ffmpeg
10
- * recording starts/stops per BEGIN_CLIP/END_CLIP and grabs from the same
11
- * display.
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
  }
@@ -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
- * Lifecycle: Xvfb runs for the entire browser process lifetime. ffmpeg
10
- * recording starts/stops per BEGIN_CLIP/END_CLIP and grabs from the same
11
- * display.
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(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autokap",
3
- "version": "1.3.22",
3
+ "version": "1.3.23",
4
4
  "description": "AI-powered CLI tool for capturing clean screenshots of websites",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",