@vosjs/render-core 0.2.9 → 0.3.1
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/record/index.d.ts +813 -0
- package/dist/record/index.js +1324 -0
- package/dist/record/index.js.map +1 -0
- package/package.json +7 -2
|
@@ -0,0 +1,1324 @@
|
|
|
1
|
+
// src/record/driver.ts
|
|
2
|
+
var realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3
|
+
|
|
4
|
+
// src/record/cap.ts
|
|
5
|
+
function capReached(elapsedMs, capSeconds) {
|
|
6
|
+
return elapsedMs >= capSeconds * 1e3;
|
|
7
|
+
}
|
|
8
|
+
function clampWait(waitMs, elapsedMs, capSeconds) {
|
|
9
|
+
return Math.max(0, Math.min(waitMs, capSeconds * 1e3 - elapsedMs));
|
|
10
|
+
}
|
|
11
|
+
function formatDurationCap(seconds) {
|
|
12
|
+
if (seconds < 60) return `${seconds} s`;
|
|
13
|
+
const m = Math.round(seconds / 60);
|
|
14
|
+
if (m < 60) return `${m} min`;
|
|
15
|
+
const h = Math.floor(m / 60);
|
|
16
|
+
const rest = m % 60;
|
|
17
|
+
return rest ? `${h} h ${rest} min` : `${h} h`;
|
|
18
|
+
}
|
|
19
|
+
function cappedLine(capSeconds) {
|
|
20
|
+
return `stopped at ${formatDurationCap(capSeconds)} (--max-duration ${capSeconds}); the remaining steps did not run`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/record/pace.ts
|
|
24
|
+
var POINTER_MS_PER_PX = 0.7;
|
|
25
|
+
var POINTER_MIN_MS = 250;
|
|
26
|
+
var POINTER_MAX_MS = 800;
|
|
27
|
+
var MOTION_TICK_MS = 16;
|
|
28
|
+
var SETTLE_MS = {
|
|
29
|
+
click: 150,
|
|
30
|
+
type: 150,
|
|
31
|
+
scroll: 200,
|
|
32
|
+
drag: 80
|
|
33
|
+
};
|
|
34
|
+
var PRESS_LEAD_MS = 80;
|
|
35
|
+
var PRESS_HOLD_MS = 70;
|
|
36
|
+
var SCROLL_SETTLE_MS = 120;
|
|
37
|
+
var TRAILING_HOLD_MS = 400;
|
|
38
|
+
function pointerTravelMs(dist) {
|
|
39
|
+
return Math.min(
|
|
40
|
+
POINTER_MAX_MS,
|
|
41
|
+
Math.max(POINTER_MIN_MS, Math.round(dist * POINTER_MS_PER_PX))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
var RESPONSE_MS = 400;
|
|
45
|
+
var QUIET_MS = 250;
|
|
46
|
+
var SETTLE_CAP_MS = 1200;
|
|
47
|
+
function settleVerdict(t, c = { response: RESPONSE_MS, quiet: QUIET_MS, cap: SETTLE_CAP_MS }) {
|
|
48
|
+
if (t.sincePress >= c.cap) return "settled";
|
|
49
|
+
if (t.sinceChange === null)
|
|
50
|
+
return t.sincePress >= c.response ? "settled" : "wait";
|
|
51
|
+
return t.sinceChange >= c.quiet ? "settled" : "wait";
|
|
52
|
+
}
|
|
53
|
+
function holdLeftMs(holdMs, sinceChange) {
|
|
54
|
+
return Math.max(0, holdMs - (sinceChange ?? 0));
|
|
55
|
+
}
|
|
56
|
+
function settleMs(step) {
|
|
57
|
+
if (typeof step.ms === "number" && step.ms >= 0) return step.ms;
|
|
58
|
+
const d = step.do;
|
|
59
|
+
return SETTLE_MS[d] ?? 0;
|
|
60
|
+
}
|
|
61
|
+
var easeInOutCubic = (u) => u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
|
|
62
|
+
async function clockMotion(dur, at, clock) {
|
|
63
|
+
const start = clock.now();
|
|
64
|
+
let samples = 0;
|
|
65
|
+
for (; ; ) {
|
|
66
|
+
const elapsed = clock.now() - start;
|
|
67
|
+
if (elapsed >= dur) break;
|
|
68
|
+
await at(easeInOutCubic(Math.min(1, elapsed / dur)));
|
|
69
|
+
samples++;
|
|
70
|
+
const next = start + Math.ceil((clock.now() - start) / MOTION_TICK_MS) * MOTION_TICK_MS;
|
|
71
|
+
const wait = Math.min(next, start + dur) - clock.now();
|
|
72
|
+
if (wait > 0) await clock.sleep(wait);
|
|
73
|
+
}
|
|
74
|
+
await at(1);
|
|
75
|
+
return samples + 1;
|
|
76
|
+
}
|
|
77
|
+
async function clockTyping(chars, delay, type, clock) {
|
|
78
|
+
const start = clock.now();
|
|
79
|
+
for (let i = 0; i < chars.length; i++) {
|
|
80
|
+
await type(chars[i], i);
|
|
81
|
+
const due = start + (i + 1) * delay;
|
|
82
|
+
const wait = due - clock.now();
|
|
83
|
+
if (wait > 0 && i < chars.length - 1) await clock.sleep(wait);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function askedMs(step) {
|
|
87
|
+
switch (step.do) {
|
|
88
|
+
case "wait":
|
|
89
|
+
return step.ms ?? 0;
|
|
90
|
+
// A click's ms is the read after the page settled: the author's.
|
|
91
|
+
case "click":
|
|
92
|
+
return step.ms ?? SETTLE_MS.click;
|
|
93
|
+
case "hover":
|
|
94
|
+
return step.ms ?? 700;
|
|
95
|
+
case "drag":
|
|
96
|
+
return step.ms ?? 700;
|
|
97
|
+
case "type":
|
|
98
|
+
return (step.text?.length ?? 0) * (step.delayMs ?? 40);
|
|
99
|
+
default:
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function paceReport(steps) {
|
|
104
|
+
const askedMs2 = steps.reduce((a, s) => a + s.askedMs, 0);
|
|
105
|
+
const gestureMs = steps.reduce((a, s) => a + s.gestureMs, 0);
|
|
106
|
+
const wallMs = steps.reduce((a, s) => a + s.wallMs, 0);
|
|
107
|
+
const overheadMs = Math.max(0, wallMs - askedMs2 - gestureMs);
|
|
108
|
+
const slow = steps.filter((s) => s.wallMs > (s.askedMs + s.gestureMs) * 1.34 + 60).map((s) => ({
|
|
109
|
+
step: s.step,
|
|
110
|
+
do: s.do,
|
|
111
|
+
askedMs: s.askedMs,
|
|
112
|
+
wallMs: s.wallMs
|
|
113
|
+
}));
|
|
114
|
+
return {
|
|
115
|
+
askedMs: askedMs2,
|
|
116
|
+
gestureMs,
|
|
117
|
+
wallMs,
|
|
118
|
+
overheadMs,
|
|
119
|
+
overheadPct: wallMs > 0 ? Math.round(overheadMs / wallMs * 100) : 0,
|
|
120
|
+
slow
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function paceLine(r) {
|
|
124
|
+
const s = (ms) => `${(ms / 1e3).toFixed(1)} s`;
|
|
125
|
+
const slow = r.slow.length ? `; slow: ${r.slow.map((x) => `#${x.step} ${x.do} ${s(x.wallMs)} for ${s(x.askedMs)} asked`).join(", ")}` : "";
|
|
126
|
+
return `pace: the script asked ${s(r.askedMs)}, the gestures added ${s(r.gestureMs)}, the take ran ${s(r.wallMs)} (${r.overheadPct} % overhead${slow})`;
|
|
127
|
+
}
|
|
128
|
+
var READ_BEAT_MS = { page: 1e3, control: 600 };
|
|
129
|
+
var DEAD_STRETCH_WARN_MS = 1500;
|
|
130
|
+
var DEAD_SHARE_WARN_PCT = 20;
|
|
131
|
+
function deadTime(steps, frames, events, durationMs) {
|
|
132
|
+
const out = [];
|
|
133
|
+
let ms = 0;
|
|
134
|
+
let longestMs = 0;
|
|
135
|
+
for (const s of steps) {
|
|
136
|
+
const t0 = s.tStart * 1e3;
|
|
137
|
+
const t1 = s.tEnd * 1e3;
|
|
138
|
+
if (!(t1 > t0)) continue;
|
|
139
|
+
const press = events.find(
|
|
140
|
+
(e) => e.type === "down" && e.t >= t0 && e.t <= t1
|
|
141
|
+
);
|
|
142
|
+
const act = press ? press.t : t0;
|
|
143
|
+
let last = act;
|
|
144
|
+
for (const f of frames) {
|
|
145
|
+
if (f.tMs > act && f.tMs <= t1) last = f.tMs;
|
|
146
|
+
}
|
|
147
|
+
const settledMs = Math.round(last - act);
|
|
148
|
+
const heldMs = Math.round(t1 - last);
|
|
149
|
+
const moved = events.some(
|
|
150
|
+
(e) => (e.type === "move" || e.type === "scroll") && e.t > last && e.t <= t1
|
|
151
|
+
);
|
|
152
|
+
const beat = s.navigated || s.step === 0 ? READ_BEAT_MS.page : READ_BEAT_MS.control;
|
|
153
|
+
const deadMs = moved ? 0 : Math.max(0, heldMs - beat);
|
|
154
|
+
if (deadMs > 0) {
|
|
155
|
+
out.push({
|
|
156
|
+
step: s.step,
|
|
157
|
+
...s.id ? { id: s.id } : {},
|
|
158
|
+
do: s.do,
|
|
159
|
+
settledMs,
|
|
160
|
+
heldMs,
|
|
161
|
+
deadMs
|
|
162
|
+
});
|
|
163
|
+
ms += deadMs;
|
|
164
|
+
longestMs = Math.max(longestMs, deadMs);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
ms,
|
|
169
|
+
pct: durationMs > 0 ? Math.round(ms / durationMs * 100) : 0,
|
|
170
|
+
steps: out,
|
|
171
|
+
longestMs
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function deadWarns(r) {
|
|
175
|
+
return r.pct >= DEAD_SHARE_WARN_PCT || r.longestMs >= DEAD_STRETCH_WARN_MS;
|
|
176
|
+
}
|
|
177
|
+
function deadLine(r) {
|
|
178
|
+
const s = (ms) => `${(ms / 1e3).toFixed(1)} s`;
|
|
179
|
+
if (!r.steps.length) return "dead time: none";
|
|
180
|
+
const parts = r.steps.map(
|
|
181
|
+
(d) => `#${d.step}${d.id ? ` (${d.id})` : ""} ${d.do} held ${s(d.heldMs)} after it settled, ${s(d.deadMs)} past the beat`
|
|
182
|
+
);
|
|
183
|
+
return `dead time: ${s(r.ms)} (${r.pct} %), a still frame under a parked cursor past the reading beat: ${parts.join("; ")}. Cut those steps' ms; a hold is what it takes to read what changed.`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/record/wall.ts
|
|
187
|
+
var IDP_HOSTS = [
|
|
188
|
+
"accounts.google.com",
|
|
189
|
+
"login.microsoftonline.com",
|
|
190
|
+
"login.live.com",
|
|
191
|
+
"appleid.apple.com",
|
|
192
|
+
"auth0.com",
|
|
193
|
+
"okta.com",
|
|
194
|
+
"onelogin.com",
|
|
195
|
+
"clerk.accounts.dev",
|
|
196
|
+
"accounts.dev",
|
|
197
|
+
"b2clogin.com",
|
|
198
|
+
"amazoncognito.com",
|
|
199
|
+
"workos.com",
|
|
200
|
+
"stytch.com"
|
|
201
|
+
];
|
|
202
|
+
var SIGNIN_SEGMENTS = /* @__PURE__ */ new Set([
|
|
203
|
+
"login",
|
|
204
|
+
"log-in",
|
|
205
|
+
"signin",
|
|
206
|
+
"sign-in",
|
|
207
|
+
"sign_in",
|
|
208
|
+
"sso",
|
|
209
|
+
"auth",
|
|
210
|
+
"authenticate",
|
|
211
|
+
"authorize",
|
|
212
|
+
"oauth",
|
|
213
|
+
"oauth2",
|
|
214
|
+
"saml"
|
|
215
|
+
]);
|
|
216
|
+
function parse(url) {
|
|
217
|
+
try {
|
|
218
|
+
return new URL(url);
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
var bareHost = (h) => h.replace(/^www\./, "").toLowerCase();
|
|
224
|
+
var trimSlash = (p) => p.length > 1 ? p.replace(/\/+$/, "") : p;
|
|
225
|
+
var place = (u) => `${u.origin}${trimSlash(u.pathname)}`;
|
|
226
|
+
function isIdpHost(host) {
|
|
227
|
+
const h = host.toLowerCase();
|
|
228
|
+
if (h.startsWith("clerk.")) return true;
|
|
229
|
+
return IDP_HOSTS.some((idp) => h === idp || h.endsWith(`.${idp}`));
|
|
230
|
+
}
|
|
231
|
+
function isSignInPath(pathname) {
|
|
232
|
+
const segs = pathname.toLowerCase().split("/").filter(Boolean);
|
|
233
|
+
return segs.some((s) => SIGNIN_SEGMENTS.has(s));
|
|
234
|
+
}
|
|
235
|
+
function redirectedAway(asked, landed) {
|
|
236
|
+
if (bareHost(asked.hostname) !== bareHost(landed.hostname)) return true;
|
|
237
|
+
const a = trimSlash(asked.pathname).toLowerCase();
|
|
238
|
+
const l = trimSlash(landed.pathname).toLowerCase();
|
|
239
|
+
if (a === l || a === "/") return false;
|
|
240
|
+
if (l.startsWith(`${a}/`)) return false;
|
|
241
|
+
if (l.endsWith(a)) return false;
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
var LADDER = 'This is a missing or expired session, not a script bug. Ways past, cheapest first: mint a session from the test auth the project already has; script the sign-in off camera and save the storage state; or have a person sign in once (npx playwright open --channel chrome --save-storage=<file> <url>). Then pass --storage-state <file>. The ladder: https://vos.so/llms-full.txt, "Sessions". A take OF this page is --allow-wall.';
|
|
245
|
+
function wallVerdict(a) {
|
|
246
|
+
const asked = parse(a.askedUrl);
|
|
247
|
+
const landed = parse(a.landedUrl);
|
|
248
|
+
if (!asked || !landed) return null;
|
|
249
|
+
if (!/^https?:$/.test(asked.protocol)) return null;
|
|
250
|
+
const away = redirectedAway(asked, landed);
|
|
251
|
+
const where = { asked: place(asked), landed: place(landed) };
|
|
252
|
+
const host = bareHost(asked.hostname);
|
|
253
|
+
if (a.status === 401 || a.status === 403) {
|
|
254
|
+
return {
|
|
255
|
+
kind: "status",
|
|
256
|
+
level: "hard",
|
|
257
|
+
...where,
|
|
258
|
+
message: `asked for ${asked.pathname}, answered ${a.status}: no session for ${host}. ${LADDER}`
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const askedForSignIn = isSignInPath(asked.pathname) || isIdpHost(asked.hostname);
|
|
262
|
+
if (askedForSignIn && !away) return null;
|
|
263
|
+
if (away && isIdpHost(landed.hostname) && !isIdpHost(asked.hostname)) {
|
|
264
|
+
return {
|
|
265
|
+
kind: "idp",
|
|
266
|
+
level: "hard",
|
|
267
|
+
...where,
|
|
268
|
+
message: `asked for ${asked.pathname}, landed on ${landed.hostname}: no session for ${host}. ${LADDER}`
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const signInForm = a.passwordFields === 1 && !a.newPasswordField || a.oneTimeCodeField;
|
|
272
|
+
if (away && isSignInPath(landed.pathname) || signInForm && !askedForSignIn) {
|
|
273
|
+
const seen = away ? `landed on ${landed.pathname}` : "the page is a sign-in form";
|
|
274
|
+
return {
|
|
275
|
+
kind: "signin",
|
|
276
|
+
level: "hard",
|
|
277
|
+
...where,
|
|
278
|
+
message: `asked for ${asked.pathname}, ${seen}: no session for ${host}. ${LADDER}`
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (away) {
|
|
282
|
+
return {
|
|
283
|
+
kind: "redirect",
|
|
284
|
+
level: "soft",
|
|
285
|
+
...where,
|
|
286
|
+
message: `asked for ${asked.pathname}, landed on ${landed.pathname}: the site sent the recorder elsewhere, which is what a page behind a login does to a stranger. If that is the page you meant, record it by its own URL. ${LADDER}`
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
var WallError = class extends Error {
|
|
292
|
+
constructor(verdict) {
|
|
293
|
+
super(verdict.message);
|
|
294
|
+
this.verdict = verdict;
|
|
295
|
+
this.name = "WallError";
|
|
296
|
+
}
|
|
297
|
+
verdict;
|
|
298
|
+
};
|
|
299
|
+
var WALL_PROBE = `(() => {
|
|
300
|
+
const shown = (el) => {
|
|
301
|
+
const r = el.getBoundingClientRect()
|
|
302
|
+
if (r.width < 2 || r.height < 2) return false
|
|
303
|
+
const s = getComputedStyle(el)
|
|
304
|
+
return s.visibility !== 'hidden' && s.display !== 'none'
|
|
305
|
+
}
|
|
306
|
+
const pw = [...document.querySelectorAll('input[type="password"]')].filter(shown)
|
|
307
|
+
const otp = [...document.querySelectorAll('input[autocomplete="one-time-code"]')].filter(shown)
|
|
308
|
+
return {
|
|
309
|
+
passwordFields: pw.length,
|
|
310
|
+
newPasswordField: pw.some((el) => (el.getAttribute('autocomplete') || '').includes('new-password')),
|
|
311
|
+
oneTimeCodeField: otp.length > 0,
|
|
312
|
+
}
|
|
313
|
+
})()`;
|
|
314
|
+
|
|
315
|
+
// src/record/exposure.ts
|
|
316
|
+
var EXPOSURE_MATCHERS_SRC = `(() => {
|
|
317
|
+
const EMAIL = /[A-Za-z0-9._%+-]+@((?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,})/g
|
|
318
|
+
const SAFE_HOST = /(^|\\.)example\\.(com|org|net)$|\\.(test|example|invalid|localhost)$/i
|
|
319
|
+
const KEY = new RegExp([
|
|
320
|
+
'\\\\b[srp]k_(?:live|test)_[A-Za-z0-9]{8,}',
|
|
321
|
+
'\\\\bgh[pousr]_[A-Za-z0-9]{20,}',
|
|
322
|
+
'\\\\bgithub_pat_[A-Za-z0-9_]{20,}',
|
|
323
|
+
'\\\\bxox[abprs]-[A-Za-z0-9-]{10,}',
|
|
324
|
+
'\\\\bAKIA[0-9A-Z]{16}\\\\b',
|
|
325
|
+
'\\\\bAIza[0-9A-Za-z_-]{30,}',
|
|
326
|
+
'\\\\bvos_(?:sk|rg|ho)_[A-Za-z0-9]{8,}',
|
|
327
|
+
'\\\\beyJ[A-Za-z0-9_-]{8,}\\\\.eyJ[A-Za-z0-9_-]{8,}\\\\.[A-Za-z0-9_-]{8,}',
|
|
328
|
+
].join('|'))
|
|
329
|
+
const CARD = /(?:^|[^0-9])((?:[0-9][ -]?){12,18}[0-9])(?![0-9])/g
|
|
330
|
+
const TAIL = /[\\u2022\\u00b7*xX]{2,}[ -]?[0-9]{4}(?![0-9])/
|
|
331
|
+
const luhn = (digits) => {
|
|
332
|
+
let sum = 0, alt = false
|
|
333
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
334
|
+
let d = digits.charCodeAt(i) - 48
|
|
335
|
+
if (alt) { d *= 2; if (d > 9) d -= 9 }
|
|
336
|
+
sum += d; alt = !alt
|
|
337
|
+
}
|
|
338
|
+
return sum % 10 === 0
|
|
339
|
+
}
|
|
340
|
+
const kindsIn = (text) => {
|
|
341
|
+
const kinds = []
|
|
342
|
+
if (!text || text.length < 6) return kinds
|
|
343
|
+
EMAIL.lastIndex = 0
|
|
344
|
+
for (let m; (m = EMAIL.exec(text)); ) {
|
|
345
|
+
if (!SAFE_HOST.test(m[1])) { kinds.push('email'); break }
|
|
346
|
+
}
|
|
347
|
+
if (KEY.test(text)) kinds.push('key')
|
|
348
|
+
CARD.lastIndex = 0
|
|
349
|
+
for (let m; (m = CARD.exec(text)); ) {
|
|
350
|
+
const digits = m[1].replace(/[ -]/g, '')
|
|
351
|
+
if (digits.length >= 13 && digits.length <= 19 && !/^0+$/.test(digits) && luhn(digits)) { kinds.push('card'); break }
|
|
352
|
+
}
|
|
353
|
+
if (TAIL.test(text)) kinds.push('card-tail')
|
|
354
|
+
return kinds
|
|
355
|
+
}
|
|
356
|
+
return { kindsIn }
|
|
357
|
+
})()`;
|
|
358
|
+
var EXPOSURE_PROBE = `(() => {
|
|
359
|
+
const { kindsIn } = ${EXPOSURE_MATCHERS_SRC}
|
|
360
|
+
const vw = innerWidth, vh = innerHeight
|
|
361
|
+
// A selector that reaches THIS element and no other, so it can be pasted
|
|
362
|
+
// into "mask" as it is: climb until the path is unique, and say which
|
|
363
|
+
// sibling wherever a tag-and-class alone would match several.
|
|
364
|
+
const segment = (el) => {
|
|
365
|
+
const tid = el.getAttribute('data-testid')
|
|
366
|
+
if (tid) return el.tagName.toLowerCase() + '[data-testid="' + tid + '"]'
|
|
367
|
+
if (el.id && /^[A-Za-z_][\\w-]*$/.test(el.id)) return el.tagName.toLowerCase() + '#' + el.id
|
|
368
|
+
const cls = [...el.classList].filter((c) => /^[A-Za-z_-][\\w-]*$/.test(c)).slice(0, 2)
|
|
369
|
+
let seg = el.tagName.toLowerCase() + (cls.length ? '.' + cls.join('.') : '')
|
|
370
|
+
const p = el.parentElement
|
|
371
|
+
if (p && [...p.children].filter((c) => c.matches(seg)).length > 1)
|
|
372
|
+
seg += ':nth-child(' + ([...p.children].indexOf(el) + 1) + ')'
|
|
373
|
+
return seg
|
|
374
|
+
}
|
|
375
|
+
const name = (el) => {
|
|
376
|
+
const parts = []
|
|
377
|
+
for (let cur = el; cur && cur !== document.body && cur !== document.documentElement; cur = cur.parentElement) {
|
|
378
|
+
parts.unshift(segment(cur))
|
|
379
|
+
let n = 2
|
|
380
|
+
try { n = document.querySelectorAll(parts.join(' > ')).length } catch {}
|
|
381
|
+
// a bare tag is unique HERE and nowhere else: give it its parent too
|
|
382
|
+
const bare = parts.length === 1 && !/[#.\\[]/.test(parts[0])
|
|
383
|
+
if ((n === 1 && !bare) || parts.length >= 5) break
|
|
384
|
+
}
|
|
385
|
+
return parts.join(' > ')
|
|
386
|
+
}
|
|
387
|
+
const seen = (el) => {
|
|
388
|
+
if (el.closest('[data-vos-masked]')) return null
|
|
389
|
+
const r = el.getBoundingClientRect()
|
|
390
|
+
if (r.width < 2 || r.height < 2) return null
|
|
391
|
+
if (r.bottom <= 0 || r.right <= 0 || r.top >= vh || r.left >= vw) return null
|
|
392
|
+
const s = getComputedStyle(el)
|
|
393
|
+
if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity === 0) return null
|
|
394
|
+
return r
|
|
395
|
+
}
|
|
396
|
+
const out = [], keys = new Set()
|
|
397
|
+
const add = (el, text) => {
|
|
398
|
+
const kinds = kindsIn(text)
|
|
399
|
+
if (!kinds.length) return
|
|
400
|
+
const r = seen(el)
|
|
401
|
+
if (!r) return
|
|
402
|
+
const selector = name(el)
|
|
403
|
+
for (const kind of kinds) {
|
|
404
|
+
const k = kind + '|' + selector
|
|
405
|
+
if (keys.has(k) || out.length >= 40) continue
|
|
406
|
+
keys.add(k)
|
|
407
|
+
out.push({ kind, selector, rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } })
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT)
|
|
411
|
+
for (let n; (n = walker.nextNode()); ) {
|
|
412
|
+
const el = n.parentElement
|
|
413
|
+
if (!el || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/.test(el.tagName)) continue
|
|
414
|
+
add(el, n.nodeValue)
|
|
415
|
+
}
|
|
416
|
+
for (const el of document.querySelectorAll('input, textarea')) {
|
|
417
|
+
if (/^(password|hidden|file|checkbox|radio)$/.test(el.type)) continue
|
|
418
|
+
add(el, el.value)
|
|
419
|
+
}
|
|
420
|
+
const hits = {}
|
|
421
|
+
for (const [sel, n] of Object.entries(window.__vosMaskHits || {})) hits[sel] = n
|
|
422
|
+
return { exposures: out, maskHits: hits }
|
|
423
|
+
})()`;
|
|
424
|
+
function maskInitScript(masks) {
|
|
425
|
+
const rules = masks.map((m) => ({
|
|
426
|
+
selector: m.selector,
|
|
427
|
+
as: m.as === "text" ? "text" : "blur",
|
|
428
|
+
text: m.text ?? ""
|
|
429
|
+
}));
|
|
430
|
+
return `(() => {
|
|
431
|
+
const RULES = ${JSON.stringify(rules)}
|
|
432
|
+
const hits = (window.__vosMaskHits = window.__vosMaskHits || {})
|
|
433
|
+
for (const r of RULES) hits[r.selector] = hits[r.selector] || 0
|
|
434
|
+
const css = RULES.map((r) => r.selector + '[data-vos-masked="blur"]{filter:blur(7px) !important;user-select:none !important}').join('\\n')
|
|
435
|
+
const paint = () => {
|
|
436
|
+
const root = document.head || document.documentElement
|
|
437
|
+
if (!root || document.getElementById('__vos-mask-style')) return
|
|
438
|
+
const style = document.createElement('style')
|
|
439
|
+
style.id = '__vos-mask-style'
|
|
440
|
+
style.textContent = css
|
|
441
|
+
root.appendChild(style)
|
|
442
|
+
}
|
|
443
|
+
const apply = () => {
|
|
444
|
+
paint()
|
|
445
|
+
for (const r of RULES) {
|
|
446
|
+
let found
|
|
447
|
+
try { found = document.querySelectorAll(r.selector) } catch { continue }
|
|
448
|
+
if (found.length > hits[r.selector]) hits[r.selector] = found.length
|
|
449
|
+
for (const el of found) {
|
|
450
|
+
const form = /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)
|
|
451
|
+
const mode = r.as === 'text' && !form ? 'text' : 'blur'
|
|
452
|
+
if (el.getAttribute('data-vos-masked') !== mode) el.setAttribute('data-vos-masked', mode)
|
|
453
|
+
if (mode !== 'text') continue
|
|
454
|
+
const texts = []
|
|
455
|
+
const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
|
|
456
|
+
for (let n; (n = w.nextNode()); ) texts.push(n)
|
|
457
|
+
if (!texts.length) { if (el.textContent !== r.text) el.textContent = r.text; continue }
|
|
458
|
+
texts.forEach((n, i) => { const want = i === 0 ? r.text : ''; if (n.nodeValue !== want) n.nodeValue = want })
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
new MutationObserver(apply).observe(document, { childList: true, subtree: true, characterData: true })
|
|
463
|
+
document.addEventListener('DOMContentLoaded', apply)
|
|
464
|
+
apply()
|
|
465
|
+
})()`;
|
|
466
|
+
}
|
|
467
|
+
var ExposureLog = class {
|
|
468
|
+
byKey = /* @__PURE__ */ new Map();
|
|
469
|
+
maskHits = /* @__PURE__ */ new Map();
|
|
470
|
+
constructor(masks = []) {
|
|
471
|
+
for (const m of masks) this.maskHits.set(m.selector, 0);
|
|
472
|
+
}
|
|
473
|
+
add(step, scan) {
|
|
474
|
+
if (!scan) return;
|
|
475
|
+
for (const e of scan.exposures ?? []) {
|
|
476
|
+
const key = `${e.kind}|${e.selector}`;
|
|
477
|
+
const had = this.byKey.get(key);
|
|
478
|
+
if (had) had.seen += 1;
|
|
479
|
+
else this.byKey.set(key, { step, ...e, seen: 1 });
|
|
480
|
+
}
|
|
481
|
+
for (const [sel, n] of Object.entries(scan.maskHits ?? {})) {
|
|
482
|
+
if (n > (this.maskHits.get(sel) ?? 0)) this.maskHits.set(sel, n);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
exposures() {
|
|
486
|
+
return [...this.byKey.values()];
|
|
487
|
+
}
|
|
488
|
+
masks(rules) {
|
|
489
|
+
return rules.map((m) => ({
|
|
490
|
+
selector: m.selector,
|
|
491
|
+
as: m.as === "text" ? "text" : "blur",
|
|
492
|
+
hits: this.maskHits.get(m.selector) ?? 0
|
|
493
|
+
}));
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
var KIND_WORDS = {
|
|
497
|
+
email: "an email address",
|
|
498
|
+
key: "something shaped like an API key or token",
|
|
499
|
+
card: "a card number",
|
|
500
|
+
"card-tail": "a card's last digits"
|
|
501
|
+
};
|
|
502
|
+
function exposureLine(e) {
|
|
503
|
+
const when = e.step < 0 ? "as the page opened" : `at step #${e.step}`;
|
|
504
|
+
return `${KIND_WORDS[e.kind]} in ${e.selector} (${when}, ${e.rect.x},${e.rect.y} ${e.rect.w}\xD7${e.rect.h})`;
|
|
505
|
+
}
|
|
506
|
+
var EXPOSURE_ADVICE = 'The recording shows these. Record from a demo account, or hide them before the camera rolls with "mask" in actions.json: { "selector": "\u2026", "as": "text", "text": "demo@acme.test" } swaps the words, "as": "blur" blurs. Then re-record.';
|
|
507
|
+
|
|
508
|
+
// src/record/setup.ts
|
|
509
|
+
var VERBS = /* @__PURE__ */ new Set(["wait", "click", "type", "press", "goto"]);
|
|
510
|
+
function validateSetup(value) {
|
|
511
|
+
const errors = [];
|
|
512
|
+
if (!Array.isArray(value)) return ["setup must be an array of steps"];
|
|
513
|
+
value.forEach((raw, i) => {
|
|
514
|
+
const at = `setup[${i}]`;
|
|
515
|
+
if (typeof raw !== "object" || raw === null) {
|
|
516
|
+
errors.push(`${at}: must be an object`);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const s = raw;
|
|
520
|
+
if (typeof s.do !== "string" || !VERBS.has(s.do)) {
|
|
521
|
+
errors.push(`${at}: "do" must be one of ${[...VERBS].join(", ")}`);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if ((s.do === "click" || s.do === "type") && typeof s.selector !== "string")
|
|
525
|
+
errors.push(`${at}: ${s.do} needs a selector`);
|
|
526
|
+
if (s.do === "wait" && typeof s.ms !== "number")
|
|
527
|
+
errors.push(`${at}: wait needs ms`);
|
|
528
|
+
if (s.do === "press" && typeof s.key !== "string")
|
|
529
|
+
errors.push(`${at}: press needs a key`);
|
|
530
|
+
if (s.do === "goto" && typeof s.url !== "string")
|
|
531
|
+
errors.push(`${at}: goto needs a url`);
|
|
532
|
+
if (s.do === "type") {
|
|
533
|
+
const t = s.text;
|
|
534
|
+
const isEnv = typeof t === "object" && t !== null && typeof t.env === "string" && Object.keys(t).length === 1;
|
|
535
|
+
if (typeof t !== "string" && !isEnv)
|
|
536
|
+
errors.push(
|
|
537
|
+
`${at}: type needs text, a string or { "env": "NAME" } read at run time`
|
|
538
|
+
);
|
|
539
|
+
if (typeof t === "string" && /passw|secret|token/i.test(String(s.selector)) && t.length > 0)
|
|
540
|
+
errors.push(
|
|
541
|
+
`${at}: a literal value typed into ${String(s.selector)} would live in actions.json, which is committed and pushed. Use { "env": "NAME" }`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
return errors;
|
|
546
|
+
}
|
|
547
|
+
function setupEnvNames(steps) {
|
|
548
|
+
const out = /* @__PURE__ */ new Set();
|
|
549
|
+
for (const s of steps ?? []) {
|
|
550
|
+
if (s.do === "type" && typeof s.text === "object") out.add(s.text.env);
|
|
551
|
+
}
|
|
552
|
+
return [...out];
|
|
553
|
+
}
|
|
554
|
+
function resolveSetupText(t, env) {
|
|
555
|
+
if (typeof t === "string") return { value: t, secret: false };
|
|
556
|
+
const v = env[t.env];
|
|
557
|
+
if (v === void 0 || v === "")
|
|
558
|
+
throw new SetupEnvError(
|
|
559
|
+
`setup: the environment variable ${t.env} is not set, and a type step reads it. Export it in the shell that runs vos record; it is never written to a file`
|
|
560
|
+
);
|
|
561
|
+
return { value: v, secret: true };
|
|
562
|
+
}
|
|
563
|
+
async function runSetup(page, steps, opts) {
|
|
564
|
+
const env = opts.env ?? {};
|
|
565
|
+
const lines = [];
|
|
566
|
+
for (const [i, step] of steps.entries()) {
|
|
567
|
+
const settle = "ms" in step && typeof step.ms === "number" ? step.ms : 150;
|
|
568
|
+
try {
|
|
569
|
+
switch (step.do) {
|
|
570
|
+
case "wait":
|
|
571
|
+
await opts.sleep(step.ms);
|
|
572
|
+
lines.push(`setup #${i} wait ${step.ms}ms`);
|
|
573
|
+
break;
|
|
574
|
+
case "goto":
|
|
575
|
+
await page.goto(step.url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => null);
|
|
576
|
+
lines.push(`setup #${i} goto ${step.url}`);
|
|
577
|
+
break;
|
|
578
|
+
case "press":
|
|
579
|
+
await page.keyboard.press(step.key);
|
|
580
|
+
await opts.sleep(settle);
|
|
581
|
+
lines.push(`setup #${i} press ${step.key}`);
|
|
582
|
+
break;
|
|
583
|
+
case "click": {
|
|
584
|
+
const loc = page.locator(step.selector).first();
|
|
585
|
+
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
586
|
+
await loc.click();
|
|
587
|
+
await opts.sleep(settle);
|
|
588
|
+
lines.push(`setup #${i} click ${step.selector}`);
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
case "type": {
|
|
592
|
+
const loc = page.locator(step.selector).first();
|
|
593
|
+
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
594
|
+
const { value, secret } = resolveSetupText(step.text, env);
|
|
595
|
+
await loc.fill(value);
|
|
596
|
+
await opts.sleep(settle);
|
|
597
|
+
lines.push(
|
|
598
|
+
`setup #${i} type ${secret ? `\${${step.text.env}}` : `${value.length} chars`} into ${step.selector}`
|
|
599
|
+
);
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
} catch (e) {
|
|
604
|
+
if (e instanceof SetupEnvError) throw e;
|
|
605
|
+
return {
|
|
606
|
+
ran: i,
|
|
607
|
+
lines,
|
|
608
|
+
failed: {
|
|
609
|
+
step: i,
|
|
610
|
+
do: step.do,
|
|
611
|
+
..."selector" in step ? { selector: step.selector } : {}
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return { ran: steps.length, lines };
|
|
617
|
+
}
|
|
618
|
+
var SetupEnvError = class extends Error {
|
|
619
|
+
constructor(message) {
|
|
620
|
+
super(message);
|
|
621
|
+
this.name = "SetupEnvError";
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
var SetupError = class extends Error {
|
|
625
|
+
constructor(failed) {
|
|
626
|
+
super(
|
|
627
|
+
`setup #${failed.step} ${failed.do}${failed.selector ? ` ${failed.selector}` : ""}: the selector never appeared, so the take would begin at a half-finished setup. Nothing was recorded. Fix the setup step (rehearse with --dry-run), then record.`
|
|
628
|
+
);
|
|
629
|
+
this.failed = failed;
|
|
630
|
+
this.name = "SetupError";
|
|
631
|
+
}
|
|
632
|
+
failed;
|
|
633
|
+
};
|
|
634
|
+
function parseHeaders(given) {
|
|
635
|
+
const out = {};
|
|
636
|
+
for (const h of given ?? []) {
|
|
637
|
+
const i = h.indexOf("=");
|
|
638
|
+
if (i <= 0) throw new Error(`--header wants name=value, got "${h}"`);
|
|
639
|
+
out[h.slice(0, i).trim()] = h.slice(i + 1);
|
|
640
|
+
}
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/record/recorder.ts
|
|
645
|
+
var DRY_PAUSE_MS = 120;
|
|
646
|
+
function jpegDims(buf) {
|
|
647
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
648
|
+
let i = 2;
|
|
649
|
+
while (i < buf.length - 9) {
|
|
650
|
+
if (buf[i] !== 255) return null;
|
|
651
|
+
const marker = buf[i + 1];
|
|
652
|
+
const len = view.getUint16(i + 2);
|
|
653
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
654
|
+
return { h: view.getUint16(i + 5), w: view.getUint16(i + 7) };
|
|
655
|
+
}
|
|
656
|
+
i += 2 + len;
|
|
657
|
+
}
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
function base64Bytes(data) {
|
|
661
|
+
const bin = atob(data);
|
|
662
|
+
const out = new Uint8Array(bin.length);
|
|
663
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
async function recordTake(browser, url, actions, sink, log, opts = {}) {
|
|
667
|
+
const dry = opts.dryRun === true;
|
|
668
|
+
const sleep = dry ? (ms) => realSleep(Math.min(ms, DRY_PAUSE_MS)) : realSleep;
|
|
669
|
+
const maxSeconds = dry ? Infinity : opts.maxDurationSeconds ?? Infinity;
|
|
670
|
+
const vw = actions.viewport?.width ?? 1280;
|
|
671
|
+
const vh = actions.viewport?.height ?? 720;
|
|
672
|
+
const context = opts.context ?? await browser.newContext({
|
|
673
|
+
viewport: { width: vw, height: vh },
|
|
674
|
+
deviceScaleFactor: 1,
|
|
675
|
+
...opts.storageState ? { storageState: opts.storageState } : {},
|
|
676
|
+
...opts.headers && Object.keys(opts.headers).length ? { extraHTTPHeaders: opts.headers } : {}
|
|
677
|
+
});
|
|
678
|
+
if (opts.context && opts.headers && Object.keys(opts.headers).length)
|
|
679
|
+
await context.setExtraHTTPHeaders(opts.headers);
|
|
680
|
+
const masks = actions.mask ?? [];
|
|
681
|
+
if (masks.length) await context.addInitScript(maskInitScript(masks));
|
|
682
|
+
const exposureLog = new ExposureLog(masks);
|
|
683
|
+
const page = await context.newPage();
|
|
684
|
+
const scanExposures = async (step) => {
|
|
685
|
+
const scan = await page.evaluate(EXPOSURE_PROBE).catch(() => null);
|
|
686
|
+
exposureLog.add(step, scan);
|
|
687
|
+
};
|
|
688
|
+
page.on("console", (m) => {
|
|
689
|
+
if (m.type() === "error") log(` [page error] ${m.text()}`);
|
|
690
|
+
});
|
|
691
|
+
log(`goto ${url}`);
|
|
692
|
+
let navTimeout = false;
|
|
693
|
+
let response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
|
|
694
|
+
navTimeout = true;
|
|
695
|
+
log(" (networkidle timeout \u2014 continuing)");
|
|
696
|
+
return null;
|
|
697
|
+
});
|
|
698
|
+
await sleep(800);
|
|
699
|
+
const setup = actions.setup ?? [];
|
|
700
|
+
if (setup.length) {
|
|
701
|
+
log(`setup: ${setup.length} step(s), off camera`);
|
|
702
|
+
const result = await runSetup(page, setup, {
|
|
703
|
+
sleep: realSleep,
|
|
704
|
+
env: opts.env ?? {}
|
|
705
|
+
});
|
|
706
|
+
for (const line of result.lines) log(` ${line}`);
|
|
707
|
+
if (result.failed) {
|
|
708
|
+
await context.close().catch(() => {
|
|
709
|
+
});
|
|
710
|
+
throw new SetupError(result.failed);
|
|
711
|
+
}
|
|
712
|
+
log(`goto ${url} (after setup)`);
|
|
713
|
+
response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => null);
|
|
714
|
+
await sleep(800);
|
|
715
|
+
}
|
|
716
|
+
let wall = null;
|
|
717
|
+
if (opts.onArrival) {
|
|
718
|
+
const seen = await page.evaluate(WALL_PROBE).catch(() => null);
|
|
719
|
+
try {
|
|
720
|
+
wall = await opts.onArrival({
|
|
721
|
+
askedUrl: url,
|
|
722
|
+
landedUrl: page.url(),
|
|
723
|
+
...response ? { status: response.status() } : {},
|
|
724
|
+
passwordFields: seen?.passwordFields ?? 0,
|
|
725
|
+
newPasswordField: seen?.newPasswordField ?? false,
|
|
726
|
+
oneTimeCodeField: seen?.oneTimeCodeField ?? false
|
|
727
|
+
}) ?? null;
|
|
728
|
+
} catch (e) {
|
|
729
|
+
await context.close().catch(() => {
|
|
730
|
+
});
|
|
731
|
+
throw e;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
await scanExposures(-1);
|
|
735
|
+
const cdp = await context.newCDPSession(page);
|
|
736
|
+
const frames = [];
|
|
737
|
+
let frameIdx = 0;
|
|
738
|
+
let t0 = 0;
|
|
739
|
+
const first = { dims: null };
|
|
740
|
+
let sinkChain = Promise.resolve();
|
|
741
|
+
if (!dry)
|
|
742
|
+
cdp.on("Page.screencastFrame", (ev) => {
|
|
743
|
+
const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
|
|
744
|
+
const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
|
|
745
|
+
const bytes = base64Bytes(ev.data);
|
|
746
|
+
if (!first.dims) first.dims = jpegDims(bytes);
|
|
747
|
+
frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
|
|
748
|
+
sinkChain = sinkChain.then(() => sink.frame(file, bytes)).catch(() => {
|
|
749
|
+
});
|
|
750
|
+
cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
t0 = Date.now();
|
|
754
|
+
if (!dry)
|
|
755
|
+
await cdp.send("Page.startScreencast", {
|
|
756
|
+
format: "jpeg",
|
|
757
|
+
quality: 90,
|
|
758
|
+
maxWidth: vw,
|
|
759
|
+
maxHeight: vh,
|
|
760
|
+
everyNthFrame: 1
|
|
761
|
+
});
|
|
762
|
+
const events = [];
|
|
763
|
+
const cur = { x: 48, y: 48 };
|
|
764
|
+
const now = () => Date.now() - t0;
|
|
765
|
+
const emit = (e) => events.push({ t: now(), ...e });
|
|
766
|
+
await page.mouse.move(cur.x, cur.y);
|
|
767
|
+
emit({ x: cur.x, y: cur.y, type: "move" });
|
|
768
|
+
const clock = { now: () => Date.now(), sleep };
|
|
769
|
+
const moveTo = async (tx, ty) => {
|
|
770
|
+
const dist = Math.hypot(tx - cur.x, ty - cur.y);
|
|
771
|
+
const from = { ...cur };
|
|
772
|
+
await clockMotion(
|
|
773
|
+
dry ? 0 : pointerTravelMs(dist),
|
|
774
|
+
async (u) => {
|
|
775
|
+
cur.x = from.x + (tx - from.x) * u;
|
|
776
|
+
cur.y = from.y + (ty - from.y) * u;
|
|
777
|
+
await page.mouse.move(cur.x, cur.y);
|
|
778
|
+
emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
|
|
779
|
+
},
|
|
780
|
+
clock
|
|
781
|
+
);
|
|
782
|
+
};
|
|
783
|
+
const boxOf = async (selector) => {
|
|
784
|
+
const loc = page.locator(selector).first();
|
|
785
|
+
try {
|
|
786
|
+
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
787
|
+
const before = await page.evaluate(() => [window.scrollX, window.scrollY]).catch(() => [0, 0]);
|
|
788
|
+
await loc.scrollIntoViewIfNeeded();
|
|
789
|
+
const after = await page.evaluate(() => [window.scrollX, window.scrollY]).catch(() => before);
|
|
790
|
+
if (before[0] !== after[0] || before[1] !== after[1])
|
|
791
|
+
await sleep(SCROLL_SETTLE_MS);
|
|
792
|
+
const bb = await loc.boundingBox();
|
|
793
|
+
if (!bb) return null;
|
|
794
|
+
return { x: bb.x, y: bb.y, w: bb.width, h: bb.height };
|
|
795
|
+
} catch {
|
|
796
|
+
log(` (selector not found, skipping: ${selector})`);
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
const clickAt = async (rect) => {
|
|
801
|
+
await moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2);
|
|
802
|
+
await sleep(PRESS_LEAD_MS);
|
|
803
|
+
emit({
|
|
804
|
+
x: Math.round(cur.x),
|
|
805
|
+
y: Math.round(cur.y),
|
|
806
|
+
type: "down",
|
|
807
|
+
button: 0,
|
|
808
|
+
rect
|
|
809
|
+
});
|
|
810
|
+
await page.mouse.down();
|
|
811
|
+
await sleep(PRESS_HOLD_MS);
|
|
812
|
+
await page.mouse.up();
|
|
813
|
+
emit({
|
|
814
|
+
x: Math.round(cur.x),
|
|
815
|
+
y: Math.round(cur.y),
|
|
816
|
+
type: "up",
|
|
817
|
+
button: 0,
|
|
818
|
+
rect
|
|
819
|
+
});
|
|
820
|
+
};
|
|
821
|
+
const waitSettled = async () => {
|
|
822
|
+
if (dry) return null;
|
|
823
|
+
const pressAt = now();
|
|
824
|
+
const before = frames.length;
|
|
825
|
+
for (; ; ) {
|
|
826
|
+
const t = now();
|
|
827
|
+
const last = frames.length > before ? frames[frames.length - 1] : null;
|
|
828
|
+
const sinceChange = last ? t - last.tMs : null;
|
|
829
|
+
if (settleVerdict({ sincePress: t - pressAt, sinceChange }) === "settled")
|
|
830
|
+
return sinceChange;
|
|
831
|
+
await sleep(40);
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
const skipped = [];
|
|
835
|
+
const stepSpans = [];
|
|
836
|
+
const paces = [];
|
|
837
|
+
let capped = false;
|
|
838
|
+
for (const [stepIdx, step] of actions.steps.entries()) {
|
|
839
|
+
if (capReached(now(), maxSeconds)) {
|
|
840
|
+
capped = true;
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
const stepStart = now();
|
|
844
|
+
let gestureMs = 0;
|
|
845
|
+
const gesture = async (fn) => {
|
|
846
|
+
const t = now();
|
|
847
|
+
const out = await fn();
|
|
848
|
+
gestureMs += now() - t;
|
|
849
|
+
return out;
|
|
850
|
+
};
|
|
851
|
+
const skippedBefore = skipped.length;
|
|
852
|
+
const urlBefore = page.url();
|
|
853
|
+
let stepRect = null;
|
|
854
|
+
switch (step.do) {
|
|
855
|
+
case "wait":
|
|
856
|
+
await sleep(clampWait(step.ms, now(), maxSeconds));
|
|
857
|
+
break;
|
|
858
|
+
case "move":
|
|
859
|
+
await moveTo(step.x, step.y);
|
|
860
|
+
break;
|
|
861
|
+
case "hover": {
|
|
862
|
+
const rect = await boxOf(step.selector);
|
|
863
|
+
stepRect = rect;
|
|
864
|
+
if (rect) {
|
|
865
|
+
await gesture(() => moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2));
|
|
866
|
+
log(`hover ${step.selector}`);
|
|
867
|
+
await sleep(step.ms ?? 700);
|
|
868
|
+
} else
|
|
869
|
+
skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
case "click": {
|
|
873
|
+
const rect = await boxOf(step.selector);
|
|
874
|
+
stepRect = rect;
|
|
875
|
+
if (rect) {
|
|
876
|
+
await gesture(() => clickAt(rect));
|
|
877
|
+
log(`click ${step.selector}`);
|
|
878
|
+
const sinceChange = await gesture(() => waitSettled());
|
|
879
|
+
await sleep(holdLeftMs(settleMs(step), sinceChange));
|
|
880
|
+
} else
|
|
881
|
+
skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
case "type": {
|
|
885
|
+
const rect = await boxOf(step.selector);
|
|
886
|
+
stepRect = rect;
|
|
887
|
+
if (rect) {
|
|
888
|
+
if (step.focus !== false) await gesture(() => clickAt(rect));
|
|
889
|
+
const delay = step.delayMs ?? 40;
|
|
890
|
+
const ping = () => emit({
|
|
891
|
+
x: Math.round(rect.x + rect.w / 2),
|
|
892
|
+
y: Math.round(rect.y + rect.h / 2),
|
|
893
|
+
type: "key",
|
|
894
|
+
rect
|
|
895
|
+
});
|
|
896
|
+
let lastPing = -Infinity;
|
|
897
|
+
await clockTyping(
|
|
898
|
+
[...step.text],
|
|
899
|
+
delay,
|
|
900
|
+
async (ch) => {
|
|
901
|
+
if (now() - lastPing >= 350) {
|
|
902
|
+
lastPing = now();
|
|
903
|
+
ping();
|
|
904
|
+
}
|
|
905
|
+
await page.keyboard.type(ch);
|
|
906
|
+
},
|
|
907
|
+
clock
|
|
908
|
+
);
|
|
909
|
+
ping();
|
|
910
|
+
log(`type "${step.text}" into ${step.selector}`);
|
|
911
|
+
await gesture(() => sleep(settleMs(step)));
|
|
912
|
+
} else
|
|
913
|
+
skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
case "scroll": {
|
|
917
|
+
const total = Math.abs(step.dy);
|
|
918
|
+
const dir = Math.sign(step.dy);
|
|
919
|
+
for (let done = 0; done < total; done += 120) {
|
|
920
|
+
await page.mouse.wheel(0, dir * Math.min(120, total - done));
|
|
921
|
+
emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "scroll" });
|
|
922
|
+
await sleep(40);
|
|
923
|
+
}
|
|
924
|
+
await gesture(() => sleep(settleMs(step)));
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
case "drag": {
|
|
928
|
+
let start = null;
|
|
929
|
+
let rect;
|
|
930
|
+
if (step.selector) {
|
|
931
|
+
const box = await boxOf(step.selector);
|
|
932
|
+
stepRect = box;
|
|
933
|
+
if (box) {
|
|
934
|
+
rect = box;
|
|
935
|
+
start = { x: box.x + box.w / 2, y: box.y + box.h / 2 };
|
|
936
|
+
} else {
|
|
937
|
+
skipped.push({
|
|
938
|
+
step: stepIdx,
|
|
939
|
+
do: step.do,
|
|
940
|
+
selector: step.selector
|
|
941
|
+
});
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
} else if (typeof step.x === "number" && typeof step.y === "number") {
|
|
945
|
+
start = { x: step.x, y: step.y };
|
|
946
|
+
}
|
|
947
|
+
if (!start) break;
|
|
948
|
+
await gesture(() => moveTo(start.x, start.y));
|
|
949
|
+
await gesture(() => sleep(PRESS_LEAD_MS));
|
|
950
|
+
emit({
|
|
951
|
+
x: Math.round(cur.x),
|
|
952
|
+
y: Math.round(cur.y),
|
|
953
|
+
type: "down",
|
|
954
|
+
button: 0,
|
|
955
|
+
rect
|
|
956
|
+
});
|
|
957
|
+
await page.mouse.down();
|
|
958
|
+
await gesture(() => sleep(PRESS_HOLD_MS));
|
|
959
|
+
const dur = step.ms ?? 700;
|
|
960
|
+
const from = { ...cur };
|
|
961
|
+
await clockMotion(
|
|
962
|
+
dur,
|
|
963
|
+
async (u) => {
|
|
964
|
+
cur.x = from.x + (step.tx - from.x) * u;
|
|
965
|
+
cur.y = from.y + (step.ty - from.y) * u;
|
|
966
|
+
await page.mouse.move(cur.x, cur.y);
|
|
967
|
+
emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
|
|
968
|
+
},
|
|
969
|
+
clock
|
|
970
|
+
);
|
|
971
|
+
await gesture(() => sleep(settleMs({ do: "drag" })));
|
|
972
|
+
await page.mouse.up();
|
|
973
|
+
emit({
|
|
974
|
+
x: Math.round(cur.x),
|
|
975
|
+
y: Math.round(cur.y),
|
|
976
|
+
type: "up",
|
|
977
|
+
button: 0,
|
|
978
|
+
rect
|
|
979
|
+
});
|
|
980
|
+
log(
|
|
981
|
+
`drag ${step.selector ?? `${start.x},${start.y}`} \u2192 ${step.tx},${step.ty}`
|
|
982
|
+
);
|
|
983
|
+
await sleep(400);
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const stepInfo = step;
|
|
988
|
+
stepSpans.push({
|
|
989
|
+
step: stepIdx,
|
|
990
|
+
...stepInfo.id ? { id: stepInfo.id } : {},
|
|
991
|
+
do: step.do,
|
|
992
|
+
...stepInfo.selector ? { selector: stepInfo.selector } : {},
|
|
993
|
+
tStart: +(stepStart / 1e3).toFixed(3),
|
|
994
|
+
tEnd: +(now() / 1e3).toFixed(3),
|
|
995
|
+
...skipped.length > skippedBefore ? { skipped: true } : {},
|
|
996
|
+
...page.url() !== urlBefore ? { navigated: true } : {},
|
|
997
|
+
...stepRect ? {
|
|
998
|
+
rect: {
|
|
999
|
+
x: +stepRect.x.toFixed(1),
|
|
1000
|
+
y: +stepRect.y.toFixed(1),
|
|
1001
|
+
w: +stepRect.w.toFixed(1),
|
|
1002
|
+
h: +stepRect.h.toFixed(1)
|
|
1003
|
+
}
|
|
1004
|
+
} : {}
|
|
1005
|
+
});
|
|
1006
|
+
paces.push({
|
|
1007
|
+
step: stepIdx,
|
|
1008
|
+
do: step.do,
|
|
1009
|
+
askedMs: askedMs(step),
|
|
1010
|
+
gestureMs,
|
|
1011
|
+
wallMs: now() - stepStart
|
|
1012
|
+
});
|
|
1013
|
+
await scanExposures(stepIdx);
|
|
1014
|
+
}
|
|
1015
|
+
if (!capped) await sleep(TRAILING_HOLD_MS);
|
|
1016
|
+
if (capped || capReached(now(), maxSeconds)) {
|
|
1017
|
+
capped = true;
|
|
1018
|
+
log(` ${cappedLine(maxSeconds)}`);
|
|
1019
|
+
}
|
|
1020
|
+
const durationMs = capped ? Math.min(now(), maxSeconds * 1e3) : now();
|
|
1021
|
+
await cdp.send("Page.stopScreencast").catch(() => {
|
|
1022
|
+
});
|
|
1023
|
+
await sleep(200);
|
|
1024
|
+
const pageTitle = await page.title().catch(() => "");
|
|
1025
|
+
await context.close();
|
|
1026
|
+
await sinkChain;
|
|
1027
|
+
const meta = {
|
|
1028
|
+
dpr: 1,
|
|
1029
|
+
zoom: 1,
|
|
1030
|
+
t0,
|
|
1031
|
+
durationMs,
|
|
1032
|
+
width: vw,
|
|
1033
|
+
height: vh,
|
|
1034
|
+
fps: 30,
|
|
1035
|
+
hasAudio: false,
|
|
1036
|
+
captureWidth: first.dims?.w ?? vw,
|
|
1037
|
+
captureHeight: first.dims?.h ?? vh,
|
|
1038
|
+
captureSurface: "tab",
|
|
1039
|
+
pageUrl: url,
|
|
1040
|
+
pageTitle,
|
|
1041
|
+
platform: opts.platform ?? "linux",
|
|
1042
|
+
producer: "cli",
|
|
1043
|
+
steps: stepSpans,
|
|
1044
|
+
...wall ? { wall: { kind: wall.kind, asked: wall.asked, landed: wall.landed } } : {},
|
|
1045
|
+
...exposureLog.exposures().length ? { exposures: exposureLog.exposures() } : {},
|
|
1046
|
+
...masks.length ? { masks: exposureLog.masks(masks) } : {}
|
|
1047
|
+
};
|
|
1048
|
+
const freezes = [];
|
|
1049
|
+
for (let i = 1; i < frames.length; i++) {
|
|
1050
|
+
const gap = frames[i].tMs - frames[i - 1].tMs;
|
|
1051
|
+
if (gap >= 400) {
|
|
1052
|
+
freezes.push({
|
|
1053
|
+
from: +(frames[i - 1].tMs / 1e3).toFixed(2),
|
|
1054
|
+
to: +(frames[i].tMs / 1e3).toFixed(2),
|
|
1055
|
+
ms: gap
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const frozenMs = freezes.reduce((sum, f) => sum + f.ms, 0);
|
|
1060
|
+
const freezePct = durationMs > 0 ? Math.round(frozenMs / durationMs * 100) : 0;
|
|
1061
|
+
const dead = deadTime(stepSpans, frames, events, durationMs);
|
|
1062
|
+
if (deadWarns(dead)) log(` WARNING: ${deadLine(dead)}`);
|
|
1063
|
+
const pace = paceReport(paces);
|
|
1064
|
+
if (!dry) log(` ${paceLine(pace)}`);
|
|
1065
|
+
return {
|
|
1066
|
+
events,
|
|
1067
|
+
frames,
|
|
1068
|
+
meta,
|
|
1069
|
+
pace,
|
|
1070
|
+
skipped,
|
|
1071
|
+
navTimeout,
|
|
1072
|
+
wall,
|
|
1073
|
+
exposures: exposureLog.exposures(),
|
|
1074
|
+
masks: exposureLog.masks(masks),
|
|
1075
|
+
freezes,
|
|
1076
|
+
freezePct,
|
|
1077
|
+
dead,
|
|
1078
|
+
capped,
|
|
1079
|
+
actions: { ...actions, url }
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// src/record/actions.ts
|
|
1084
|
+
var VERBS2 = /* @__PURE__ */ new Set([
|
|
1085
|
+
"wait",
|
|
1086
|
+
"hover",
|
|
1087
|
+
"click",
|
|
1088
|
+
"type",
|
|
1089
|
+
"scroll",
|
|
1090
|
+
"move",
|
|
1091
|
+
"drag"
|
|
1092
|
+
]);
|
|
1093
|
+
function validateActions(value) {
|
|
1094
|
+
const errors = [];
|
|
1095
|
+
if (typeof value !== "object" || value === null)
|
|
1096
|
+
return ["actions file must be a JSON object"];
|
|
1097
|
+
const obj = value;
|
|
1098
|
+
if (obj.url !== void 0 && typeof obj.url !== "string")
|
|
1099
|
+
errors.push("url must be a string");
|
|
1100
|
+
if (obj.viewport !== void 0) {
|
|
1101
|
+
const v = obj.viewport;
|
|
1102
|
+
if (typeof v !== "object" || v === null) {
|
|
1103
|
+
errors.push("viewport must be { width, height }");
|
|
1104
|
+
} else {
|
|
1105
|
+
const vp = v;
|
|
1106
|
+
if (typeof vp.width !== "number" || typeof vp.height !== "number") {
|
|
1107
|
+
errors.push("viewport must be { width, height }");
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (obj.setup !== void 0) errors.push(...validateSetup(obj.setup));
|
|
1112
|
+
if (obj.mask !== void 0) {
|
|
1113
|
+
if (!Array.isArray(obj.mask)) {
|
|
1114
|
+
errors.push("mask must be an array of { selector, as?, text? }");
|
|
1115
|
+
} else {
|
|
1116
|
+
obj.mask.forEach((raw, i) => {
|
|
1117
|
+
const at = `mask[${i}]`;
|
|
1118
|
+
const m = raw ?? {};
|
|
1119
|
+
if (typeof m.selector !== "string" || !m.selector.trim())
|
|
1120
|
+
errors.push(`${at}: needs a selector`);
|
|
1121
|
+
if (m.as !== void 0 && m.as !== "blur" && m.as !== "text")
|
|
1122
|
+
errors.push(`${at}: as must be "blur" or "text"`);
|
|
1123
|
+
if (m.as === "text" && (typeof m.text !== "string" || !m.text))
|
|
1124
|
+
errors.push(`${at}: as "text" needs the text to show instead`);
|
|
1125
|
+
if (m.as !== "text" && m.text !== void 0)
|
|
1126
|
+
errors.push(`${at}: text is only read with as "text"`);
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (!Array.isArray(obj.steps) || obj.steps.length === 0) {
|
|
1131
|
+
errors.push("steps must be a non-empty array");
|
|
1132
|
+
return errors;
|
|
1133
|
+
}
|
|
1134
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1135
|
+
obj.steps.forEach((raw, i) => {
|
|
1136
|
+
const at = `steps[${i}]`;
|
|
1137
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1138
|
+
errors.push(`${at}: must be an object`);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const s = raw;
|
|
1142
|
+
if (typeof s.do !== "string" || !VERBS2.has(s.do)) {
|
|
1143
|
+
errors.push(`${at}: "do" must be one of ${[...VERBS2].join(", ")}`);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (s.id !== void 0) {
|
|
1147
|
+
if (typeof s.id !== "string" || !s.id.trim()) {
|
|
1148
|
+
errors.push(`${at}: id must be a non-empty string`);
|
|
1149
|
+
} else if (seenIds.has(s.id)) {
|
|
1150
|
+
errors.push(
|
|
1151
|
+
`${at}: duplicate id "${s.id}" \u2014 an anchor could not tell the two steps apart`
|
|
1152
|
+
);
|
|
1153
|
+
} else {
|
|
1154
|
+
seenIds.add(s.id);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
const needSelector = s.do === "hover" || s.do === "click" || s.do === "type";
|
|
1158
|
+
if (needSelector && typeof s.selector !== "string")
|
|
1159
|
+
errors.push(`${at}: ${s.do} needs a selector`);
|
|
1160
|
+
if (s.do === "wait" && typeof s.ms !== "number")
|
|
1161
|
+
errors.push(`${at}: wait needs ms`);
|
|
1162
|
+
if (s.ms !== void 0 && (typeof s.ms !== "number" || !Number.isFinite(s.ms) || s.ms < 0))
|
|
1163
|
+
errors.push(`${at}: ms must be a number of milliseconds \u2265 0`);
|
|
1164
|
+
if (s.do === "type" && typeof s.text !== "string")
|
|
1165
|
+
errors.push(`${at}: type needs text`);
|
|
1166
|
+
if (s.do === "type" && s.focus !== void 0 && typeof s.focus !== "boolean")
|
|
1167
|
+
errors.push(`${at}: type focus must be true or false`);
|
|
1168
|
+
if (s.do === "scroll" && typeof s.dy !== "number")
|
|
1169
|
+
errors.push(`${at}: scroll needs dy`);
|
|
1170
|
+
if (s.do === "move" && (typeof s.x !== "number" || typeof s.y !== "number")) {
|
|
1171
|
+
errors.push(`${at}: move needs x and y (CSS px)`);
|
|
1172
|
+
}
|
|
1173
|
+
if (s.do === "drag") {
|
|
1174
|
+
if (typeof s.tx !== "number" || typeof s.ty !== "number") {
|
|
1175
|
+
errors.push(`${at}: drag needs tx and ty (CSS px)`);
|
|
1176
|
+
}
|
|
1177
|
+
const hasStart = typeof s.selector === "string" || typeof s.x === "number" && typeof s.y === "number";
|
|
1178
|
+
if (!hasStart)
|
|
1179
|
+
errors.push(
|
|
1180
|
+
`${at}: drag needs a selector OR x and y as the start point`
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
return errors;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/record/encodePage.ts
|
|
1188
|
+
var MEDIABUNNY_URL = "https://esm.sh/mediabunny@1.55.7?target=es2022";
|
|
1189
|
+
function buildEncodePage(o) {
|
|
1190
|
+
const q = o.frameQuery ? `?${o.frameQuery}` : "";
|
|
1191
|
+
return `<!doctype html><html><head><meta charset="utf-8"></head><body>
|
|
1192
|
+
<script type="module">
|
|
1193
|
+
try {
|
|
1194
|
+
const MB = await import('${MEDIABUNNY_URL}')
|
|
1195
|
+
const frames = await (await fetch(${JSON.stringify(o.framesIndexUrl)})).json()
|
|
1196
|
+
const meta = await (await fetch(${JSON.stringify(o.metaUrl)})).json()
|
|
1197
|
+
const W = meta.captureWidth ?? meta.width, H = meta.captureHeight ?? meta.height
|
|
1198
|
+
const FPS = 30
|
|
1199
|
+
const durSec = meta.durationMs / 1000
|
|
1200
|
+
const cv = document.createElement('canvas')
|
|
1201
|
+
cv.width = W; cv.height = H
|
|
1202
|
+
const c2 = cv.getContext('2d')
|
|
1203
|
+
const output = new MB.Output({ format: new MB.WebMOutputFormat(), target: new MB.BufferTarget() })
|
|
1204
|
+
const src = new MB.CanvasSource(cv, { codec: 'vp9', bitrate: 8_000_000 })
|
|
1205
|
+
output.addVideoTrack(src, { frameRate: FPS })
|
|
1206
|
+
await output.start()
|
|
1207
|
+
let fi = -1
|
|
1208
|
+
const total = Math.ceil(durSec * FPS)
|
|
1209
|
+
for (let i = 0; i < total; i++) {
|
|
1210
|
+
const t = i / FPS
|
|
1211
|
+
let next = fi
|
|
1212
|
+
while (next + 1 < frames.length && frames[next + 1].tMs <= t * 1000) next++
|
|
1213
|
+
if (next < 0) next = 0
|
|
1214
|
+
if (next !== fi) {
|
|
1215
|
+
fi = next
|
|
1216
|
+
const blob = await (await fetch(${JSON.stringify(o.framesBaseUrl)} + frames[fi].file + ${JSON.stringify(q)})).blob()
|
|
1217
|
+
const bmp = await createImageBitmap(blob)
|
|
1218
|
+
c2.drawImage(bmp, 0, 0, W, H)
|
|
1219
|
+
bmp.close()
|
|
1220
|
+
}
|
|
1221
|
+
await src.add(t, 1 / FPS)
|
|
1222
|
+
window.__progress = i / total
|
|
1223
|
+
}
|
|
1224
|
+
await output.finalize()
|
|
1225
|
+
const buf = output.target.buffer
|
|
1226
|
+
const res = await fetch(${JSON.stringify(o.save.url)}, { method: ${JSON.stringify(o.save.method)}, headers: { 'content-type': 'video/webm' }, body: buf })
|
|
1227
|
+
if (!res.ok) throw new Error('save failed: ' + res.status)
|
|
1228
|
+
// Two hosts, two completion words: the CLI's take server polls __done,
|
|
1229
|
+
// the fleet's page runner polls __renderComplete.
|
|
1230
|
+
window.__done = { ok: true, bytes: buf.byteLength }
|
|
1231
|
+
window.__renderComplete = { success: true, size: buf.byteLength, uploaded: true }
|
|
1232
|
+
} catch (e) {
|
|
1233
|
+
window.__error = String(e && e.stack || e)
|
|
1234
|
+
window.__renderComplete = { success: false, error: String(e && e.message || e) }
|
|
1235
|
+
}
|
|
1236
|
+
</script></body></html>`;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/record/plan.ts
|
|
1240
|
+
import {
|
|
1241
|
+
DEFAULT_FRAME_STYLE,
|
|
1242
|
+
planAutoSpeed,
|
|
1243
|
+
planAutoZoom,
|
|
1244
|
+
projectFromArtifact,
|
|
1245
|
+
withBackdrop
|
|
1246
|
+
} from "@vosjs/studio-core";
|
|
1247
|
+
function planFreshTake(artifact, recordingName, opts = {}) {
|
|
1248
|
+
const ingest = opts.backdrop ? { frame: withBackdrop(DEFAULT_FRAME_STYLE, opts.backdrop) } : {};
|
|
1249
|
+
const doc = projectFromArtifact(artifact, recordingName, ingest).doc;
|
|
1250
|
+
doc.zoom = planAutoZoom(doc.source.cursor, {
|
|
1251
|
+
width: doc.source.meta.width,
|
|
1252
|
+
height: doc.source.meta.height,
|
|
1253
|
+
duration: doc.source.meta.durationMs / 1e3,
|
|
1254
|
+
style: doc.zoomStyle,
|
|
1255
|
+
params: doc.zoomParams
|
|
1256
|
+
});
|
|
1257
|
+
doc.speed = planAutoSpeed(doc.source.cursor, {
|
|
1258
|
+
durationMs: doc.source.meta.durationMs,
|
|
1259
|
+
params: doc.speedParams,
|
|
1260
|
+
activity: opts.activity
|
|
1261
|
+
});
|
|
1262
|
+
return doc;
|
|
1263
|
+
}
|
|
1264
|
+
export {
|
|
1265
|
+
DEAD_SHARE_WARN_PCT,
|
|
1266
|
+
DEAD_STRETCH_WARN_MS,
|
|
1267
|
+
EXPOSURE_ADVICE,
|
|
1268
|
+
EXPOSURE_MATCHERS_SRC,
|
|
1269
|
+
EXPOSURE_PROBE,
|
|
1270
|
+
ExposureLog,
|
|
1271
|
+
MOTION_TICK_MS,
|
|
1272
|
+
POINTER_MAX_MS,
|
|
1273
|
+
POINTER_MIN_MS,
|
|
1274
|
+
POINTER_MS_PER_PX,
|
|
1275
|
+
PRESS_HOLD_MS,
|
|
1276
|
+
PRESS_LEAD_MS,
|
|
1277
|
+
QUIET_MS,
|
|
1278
|
+
READ_BEAT_MS,
|
|
1279
|
+
RESPONSE_MS,
|
|
1280
|
+
SCROLL_SETTLE_MS,
|
|
1281
|
+
SETTLE_CAP_MS,
|
|
1282
|
+
SETTLE_MS,
|
|
1283
|
+
SetupEnvError,
|
|
1284
|
+
SetupError,
|
|
1285
|
+
TRAILING_HOLD_MS,
|
|
1286
|
+
WALL_PROBE,
|
|
1287
|
+
WallError,
|
|
1288
|
+
askedMs,
|
|
1289
|
+
base64Bytes,
|
|
1290
|
+
buildEncodePage,
|
|
1291
|
+
capReached,
|
|
1292
|
+
cappedLine,
|
|
1293
|
+
clampWait,
|
|
1294
|
+
clockMotion,
|
|
1295
|
+
clockTyping,
|
|
1296
|
+
deadLine,
|
|
1297
|
+
deadTime,
|
|
1298
|
+
deadWarns,
|
|
1299
|
+
easeInOutCubic,
|
|
1300
|
+
exposureLine,
|
|
1301
|
+
formatDurationCap,
|
|
1302
|
+
holdLeftMs,
|
|
1303
|
+
isIdpHost,
|
|
1304
|
+
isSignInPath,
|
|
1305
|
+
jpegDims,
|
|
1306
|
+
maskInitScript,
|
|
1307
|
+
paceLine,
|
|
1308
|
+
paceReport,
|
|
1309
|
+
parseHeaders,
|
|
1310
|
+
planFreshTake,
|
|
1311
|
+
pointerTravelMs,
|
|
1312
|
+
realSleep,
|
|
1313
|
+
recordTake,
|
|
1314
|
+
redirectedAway,
|
|
1315
|
+
resolveSetupText,
|
|
1316
|
+
runSetup,
|
|
1317
|
+
settleMs,
|
|
1318
|
+
settleVerdict,
|
|
1319
|
+
setupEnvNames,
|
|
1320
|
+
validateActions,
|
|
1321
|
+
validateSetup,
|
|
1322
|
+
wallVerdict
|
|
1323
|
+
};
|
|
1324
|
+
//# sourceMappingURL=index.js.map
|