@rubytech/taskmaster 1.0.49 → 1.0.50
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/screencast.js +88 -25
- package/dist/build-info.json +3 -3
- package/package.json +1 -1
|
@@ -4,10 +4,15 @@ import { getHeadersWithAuth } from "./cdp.helpers.js";
|
|
|
4
4
|
import { rawDataToString } from "../infra/ws.js";
|
|
5
5
|
const log = createSubsystemLogger("browser").child("screencast-cdp");
|
|
6
6
|
let sessionCounter = 0;
|
|
7
|
+
/** Interval (ms) for the Page.captureScreenshot polling fallback. */
|
|
8
|
+
const POLL_INTERVAL_MS = 500;
|
|
9
|
+
/** How long to wait for Page.startScreencast frames before falling back. */
|
|
10
|
+
const SCREENCAST_STALL_MS = 3000;
|
|
7
11
|
/**
|
|
8
12
|
* Start a persistent CDP screencast session.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
13
|
+
* Uses Page.startScreencast for compositor-driven frames, but falls back
|
|
14
|
+
* to Page.captureScreenshot polling when the compositor doesn't produce
|
|
15
|
+
* continuous frames (common on headless / software-rendered / Pi setups).
|
|
11
16
|
*/
|
|
12
17
|
export async function startScreencast(opts) {
|
|
13
18
|
const sessionId = ++sessionCounter;
|
|
@@ -31,29 +36,80 @@ export async function startScreencast(opts) {
|
|
|
31
36
|
pending.set(msgId, { resolve, reject });
|
|
32
37
|
});
|
|
33
38
|
};
|
|
39
|
+
let frameCount = 0;
|
|
40
|
+
let pollInterval = null;
|
|
41
|
+
let stallTimeout = null;
|
|
42
|
+
let polling = false;
|
|
43
|
+
const cleanup = () => {
|
|
44
|
+
if (pollInterval) {
|
|
45
|
+
clearInterval(pollInterval);
|
|
46
|
+
pollInterval = null;
|
|
47
|
+
}
|
|
48
|
+
if (stallTimeout) {
|
|
49
|
+
clearTimeout(stallTimeout);
|
|
50
|
+
stallTimeout = null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
34
53
|
const session = {
|
|
35
54
|
id,
|
|
36
55
|
ws,
|
|
37
56
|
active: false,
|
|
38
57
|
stop: () => {
|
|
39
58
|
session.active = false;
|
|
59
|
+
cleanup();
|
|
60
|
+
// Don't send Page.stopScreencast — it's page-level and can race with
|
|
61
|
+
// a replacement session connecting to the same tab. Just close the socket.
|
|
40
62
|
try {
|
|
41
|
-
|
|
63
|
+
ws.close();
|
|
42
64
|
}
|
|
43
65
|
catch {
|
|
44
|
-
|
|
66
|
+
/* ignore */
|
|
45
67
|
}
|
|
46
|
-
setTimeout(() => {
|
|
47
|
-
try {
|
|
48
|
-
ws.close();
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
/* ignore */
|
|
52
|
-
}
|
|
53
|
-
}, 100);
|
|
54
68
|
},
|
|
55
69
|
};
|
|
56
|
-
|
|
70
|
+
/** Synthetic metadata for polling-based frames. */
|
|
71
|
+
const syntheticMeta = {
|
|
72
|
+
offsetTop: 0,
|
|
73
|
+
pageScaleFactor: 1,
|
|
74
|
+
deviceWidth: maxWidth,
|
|
75
|
+
deviceHeight: maxHeight,
|
|
76
|
+
scrollOffsetX: 0,
|
|
77
|
+
scrollOffsetY: 0,
|
|
78
|
+
};
|
|
79
|
+
/** Take a single screenshot and deliver it as a frame. */
|
|
80
|
+
const pollOnce = async () => {
|
|
81
|
+
if (!session.active)
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
const result = (await send("Page.captureScreenshot", {
|
|
85
|
+
format,
|
|
86
|
+
quality,
|
|
87
|
+
}));
|
|
88
|
+
const data = result?.data;
|
|
89
|
+
if (data && session.active) {
|
|
90
|
+
frameCount++;
|
|
91
|
+
if (frameCount <= 3 || frameCount % 100 === 0) {
|
|
92
|
+
log.info(`poll frame #${frameCount}: dataLen=${data.length}`);
|
|
93
|
+
}
|
|
94
|
+
opts.onFrame({ data, metadata: syntheticMeta, sessionId: 0 });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// ignore — socket may be closing
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
/** Switch from compositor-driven screencasting to polling. */
|
|
102
|
+
const startPollingFallback = () => {
|
|
103
|
+
if (polling || !session.active)
|
|
104
|
+
return;
|
|
105
|
+
polling = true;
|
|
106
|
+
log.warn(`screencast stalled (${frameCount} frames in ${SCREENCAST_STALL_MS}ms) — falling back to Page.captureScreenshot polling`);
|
|
107
|
+
// Stop the compositor screencast (best-effort)
|
|
108
|
+
send("Page.stopScreencast").catch(() => { });
|
|
109
|
+
pollInterval = setInterval(pollOnce, POLL_INTERVAL_MS);
|
|
110
|
+
// Fire one immediately so the UI gets a frame right away
|
|
111
|
+
pollOnce();
|
|
112
|
+
};
|
|
57
113
|
ws.on("message", (raw) => {
|
|
58
114
|
try {
|
|
59
115
|
const msg = JSON.parse(rawDataToString(raw));
|
|
@@ -71,8 +127,8 @@ export async function startScreencast(opts) {
|
|
|
71
127
|
}
|
|
72
128
|
return;
|
|
73
129
|
}
|
|
74
|
-
// Handle screencast frame events
|
|
75
|
-
if (msg.method === "Page.screencastFrame") {
|
|
130
|
+
// Handle screencast frame events (compositor-driven)
|
|
131
|
+
if (msg.method === "Page.screencastFrame" && !polling) {
|
|
76
132
|
frameCount++;
|
|
77
133
|
const params = msg.params;
|
|
78
134
|
if (frameCount <= 3 || frameCount % 100 === 0) {
|
|
@@ -90,12 +146,18 @@ export async function startScreencast(opts) {
|
|
|
90
146
|
});
|
|
91
147
|
}
|
|
92
148
|
}
|
|
93
|
-
else if (msg.method) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
149
|
+
else if (msg.method === "Page.screencastVisibilityChanged") {
|
|
150
|
+
const visible = msg.params?.visible;
|
|
151
|
+
log.info(`Page.screencastVisibilityChanged visible=${visible}`);
|
|
152
|
+
// If the page becomes "not visible", the compositor won't produce
|
|
153
|
+
// frames. Switch to polling immediately.
|
|
154
|
+
if (visible === false && !polling && session.active) {
|
|
155
|
+
startPollingFallback();
|
|
97
156
|
}
|
|
98
157
|
}
|
|
158
|
+
else if (msg.method && frameCount === 0 && !polling) {
|
|
159
|
+
log.debug(`CDP event: ${msg.method}`);
|
|
160
|
+
}
|
|
99
161
|
}
|
|
100
162
|
catch {
|
|
101
163
|
// ignore parse errors
|
|
@@ -104,6 +166,7 @@ export async function startScreencast(opts) {
|
|
|
104
166
|
ws.on("close", (code, reason) => {
|
|
105
167
|
log.info(`CDP socket closed: code=${code} reason=${reason?.toString() ?? ""}`);
|
|
106
168
|
session.active = false;
|
|
169
|
+
cleanup();
|
|
107
170
|
for (const [, p] of pending) {
|
|
108
171
|
p.reject(new Error("CDP socket closed"));
|
|
109
172
|
}
|
|
@@ -120,7 +183,6 @@ export async function startScreencast(opts) {
|
|
|
120
183
|
ws.once("error", (err) => reject(err));
|
|
121
184
|
});
|
|
122
185
|
log.info("CDP socket connected, enabling Page domain");
|
|
123
|
-
// Enable Page domain and start screencast
|
|
124
186
|
await send("Page.enable");
|
|
125
187
|
log.info(`starting Page.startScreencast format=${format} quality=${quality}`);
|
|
126
188
|
await send("Page.startScreencast", {
|
|
@@ -131,12 +193,13 @@ export async function startScreencast(opts) {
|
|
|
131
193
|
});
|
|
132
194
|
session.active = true;
|
|
133
195
|
log.info(`screencast active: ${id}`);
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
196
|
+
// If the compositor doesn't produce continuous frames within SCREENCAST_STALL_MS,
|
|
197
|
+
// fall back to polling. This handles headless, software-rendered, and Pi setups.
|
|
198
|
+
stallTimeout = setTimeout(() => {
|
|
199
|
+
if (session.active && !polling && frameCount <= 1) {
|
|
200
|
+
startPollingFallback();
|
|
138
201
|
}
|
|
139
|
-
},
|
|
202
|
+
}, SCREENCAST_STALL_MS);
|
|
140
203
|
return session;
|
|
141
204
|
}
|
|
142
205
|
/**
|
package/dist/build-info.json
CHANGED