screencast-axi 0.1.0
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/LICENSE +21 -0
- package/README.md +555 -0
- package/dist/bin/screencast-axi.d.ts +2 -0
- package/dist/bin/screencast-axi.js +16 -0
- package/dist/bin/screencast-axi.js.map +1 -0
- package/dist/src/auth/strategies.d.ts +54 -0
- package/dist/src/auth/strategies.js +137 -0
- package/dist/src/auth/strategies.js.map +1 -0
- package/dist/src/auth/types.d.ts +65 -0
- package/dist/src/auth/types.js +2 -0
- package/dist/src/auth/types.js.map +1 -0
- package/dist/src/browser.d.ts +100 -0
- package/dist/src/browser.js +168 -0
- package/dist/src/browser.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +160 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/commands/auth.d.ts +4 -0
- package/dist/src/commands/auth.js +207 -0
- package/dist/src/commands/auth.js.map +1 -0
- package/dist/src/commands/check.d.ts +12 -0
- package/dist/src/commands/check.js +103 -0
- package/dist/src/commands/check.js.map +1 -0
- package/dist/src/commands/doctor.d.ts +15 -0
- package/dist/src/commands/doctor.js +131 -0
- package/dist/src/commands/doctor.js.map +1 -0
- package/dist/src/commands/guide.d.ts +6 -0
- package/dist/src/commands/guide.js +257 -0
- package/dist/src/commands/guide.js.map +1 -0
- package/dist/src/commands/home.d.ts +12 -0
- package/dist/src/commands/home.js +78 -0
- package/dist/src/commands/home.js.map +1 -0
- package/dist/src/commands/list.d.ts +6 -0
- package/dist/src/commands/list.js +128 -0
- package/dist/src/commands/list.js.map +1 -0
- package/dist/src/commands/record.d.ts +7 -0
- package/dist/src/commands/record.js +266 -0
- package/dist/src/commands/record.js.map +1 -0
- package/dist/src/commands/scaffold.d.ts +4 -0
- package/dist/src/commands/scaffold.js +122 -0
- package/dist/src/commands/scaffold.js.map +1 -0
- package/dist/src/commands/setup.d.ts +14 -0
- package/dist/src/commands/setup.js +137 -0
- package/dist/src/commands/setup.js.map +1 -0
- package/dist/src/config.d.ts +126 -0
- package/dist/src/config.js +275 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/director.d.ts +217 -0
- package/dist/src/director.js +415 -0
- package/dist/src/director.js.map +1 -0
- package/dist/src/duration.d.ts +33 -0
- package/dist/src/duration.js +68 -0
- package/dist/src/duration.js.map +1 -0
- package/dist/src/encode.d.ts +67 -0
- package/dist/src/encode.js +225 -0
- package/dist/src/encode.js.map +1 -0
- package/dist/src/errors.d.ts +19 -0
- package/dist/src/errors.js +27 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/flags.d.ts +39 -0
- package/dist/src/flags.js +130 -0
- package/dist/src/flags.js.map +1 -0
- package/dist/src/forensics.d.ts +51 -0
- package/dist/src/forensics.js +107 -0
- package/dist/src/forensics.js.map +1 -0
- package/dist/src/hooks.d.ts +9 -0
- package/dist/src/hooks.js +20 -0
- package/dist/src/hooks.js.map +1 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/inventory.d.ts +45 -0
- package/dist/src/inventory.js +89 -0
- package/dist/src/inventory.js.map +1 -0
- package/dist/src/manifest.d.ts +89 -0
- package/dist/src/manifest.js +137 -0
- package/dist/src/manifest.js.map +1 -0
- package/dist/src/output.d.ts +8 -0
- package/dist/src/output.js +2 -0
- package/dist/src/output.js.map +1 -0
- package/dist/src/overlay.d.ts +105 -0
- package/dist/src/overlay.js +302 -0
- package/dist/src/overlay.js.map +1 -0
- package/dist/src/reference.d.ts +1 -0
- package/dist/src/reference.js +108 -0
- package/dist/src/reference.js.map +1 -0
- package/dist/src/run.d.ts +82 -0
- package/dist/src/run.js +321 -0
- package/dist/src/run.js.map +1 -0
- package/dist/src/skill.d.ts +12 -0
- package/dist/src/skill.js +69 -0
- package/dist/src/skill.js.map +1 -0
- package/dist/src/toolchain.d.ts +62 -0
- package/dist/src/toolchain.js +127 -0
- package/dist/src/toolchain.js.map +1 -0
- package/dist/src/types.d.ts +88 -0
- package/dist/src/types.js +18 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/version.d.ts +10 -0
- package/dist/src/version.js +11 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +93 -0
- package/skills/screencast-axi/SKILL.md +49 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/** Default ceiling on the post-navigation settle wait. */
|
|
2
|
+
export const DEFAULT_SETTLE_MS = 2500;
|
|
3
|
+
const EASE = (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2);
|
|
4
|
+
/**
|
|
5
|
+
* The scenario-facing API. Every method is written so the resulting video is
|
|
6
|
+
* *watchable*: the pointer travels rather than teleports, clicks are preceded
|
|
7
|
+
* by a beat, and typing is per-character.
|
|
8
|
+
*
|
|
9
|
+
* Raw Playwright is still available via `director.page` for anything the
|
|
10
|
+
* helpers do not cover - but prefer adding a helper here over reaching past it,
|
|
11
|
+
* so all clips share one visual language.
|
|
12
|
+
*/
|
|
13
|
+
export class Director {
|
|
14
|
+
page;
|
|
15
|
+
opts;
|
|
16
|
+
contextCreatedAt;
|
|
17
|
+
pointer = { x: 0, y: 0 };
|
|
18
|
+
/** ms since the context was created, at the moment the clip proper began. */
|
|
19
|
+
clipStartedAt = null;
|
|
20
|
+
/** Indices passed to {@link step}, in the order the take showed them. */
|
|
21
|
+
shown = [];
|
|
22
|
+
/**
|
|
23
|
+
* Milliseconds spent in pauses this class controls.
|
|
24
|
+
*
|
|
25
|
+
* A take is `fixed + pace x scalable`: the app's own waits - navigation, a
|
|
26
|
+
* network round trip, an animation - do not get slower because the recorder
|
|
27
|
+
* does. Tracking the scalable half is what lets `--duration` solve for a
|
|
28
|
+
* pace exactly rather than assuming the whole clip scales.
|
|
29
|
+
*/
|
|
30
|
+
pausedMs = 0;
|
|
31
|
+
/**
|
|
32
|
+
* Everything the scenario did, in order.
|
|
33
|
+
*
|
|
34
|
+
* A scenario is arbitrary code driving a real browser, often a signed-in
|
|
35
|
+
* one, so "what will this actually do" is a fair question to want answered
|
|
36
|
+
* before trusting it. Watching a headed run answers it once; this answers it
|
|
37
|
+
* in a form that can be read, diffed and kept.
|
|
38
|
+
*/
|
|
39
|
+
actions = [];
|
|
40
|
+
constructor(page, opts, contextCreatedAt) {
|
|
41
|
+
this.page = page;
|
|
42
|
+
this.opts = opts;
|
|
43
|
+
this.contextCreatedAt = contextCreatedAt;
|
|
44
|
+
}
|
|
45
|
+
/** Seconds of dead air at the head of the raw video, for the encoder to trim. */
|
|
46
|
+
get trimStartSeconds() {
|
|
47
|
+
if (this.clipStartedAt === null)
|
|
48
|
+
return 0;
|
|
49
|
+
return Math.max(0, (this.clipStartedAt - this.contextCreatedAt) / 1000);
|
|
50
|
+
}
|
|
51
|
+
/** Marks the end of setup. Everything before this is trimmed off the clip. */
|
|
52
|
+
markClipStart() {
|
|
53
|
+
this.clipStartedAt = Date.now();
|
|
54
|
+
}
|
|
55
|
+
scaled(ms) {
|
|
56
|
+
return Math.max(0, Math.round(ms * this.opts.pace));
|
|
57
|
+
}
|
|
58
|
+
/** Pace-scaled pause. Every wait this class owns goes through here. */
|
|
59
|
+
async pause(ms) {
|
|
60
|
+
const scaled = this.scaled(ms);
|
|
61
|
+
this.pausedMs += scaled;
|
|
62
|
+
await this.page.waitForTimeout(scaled);
|
|
63
|
+
}
|
|
64
|
+
/** How much of the take so far was pause this class controls, in ms. */
|
|
65
|
+
get scaledPauseMs() {
|
|
66
|
+
return this.pausedMs;
|
|
67
|
+
}
|
|
68
|
+
/** Everything the scenario did, in order. */
|
|
69
|
+
get performed() {
|
|
70
|
+
return this.actions;
|
|
71
|
+
}
|
|
72
|
+
record(kind, target, detail) {
|
|
73
|
+
this.actions.push({
|
|
74
|
+
atMs: Date.now() - this.contextCreatedAt,
|
|
75
|
+
kind,
|
|
76
|
+
...(target !== undefined ? { target: describeTarget(target) } : {}),
|
|
77
|
+
...(detail !== undefined ? { detail } : {}),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** A deliberate pause so the viewer can read what just happened. */
|
|
81
|
+
async beat(ms = 700) {
|
|
82
|
+
await this.pause(ms);
|
|
83
|
+
}
|
|
84
|
+
async goto(path) {
|
|
85
|
+
const url = path.startsWith("http") ? path : new URL(path, this.opts.baseUrl).toString();
|
|
86
|
+
this.record("goto", url);
|
|
87
|
+
await this.page.goto(url, { waitUntil: "domcontentloaded" });
|
|
88
|
+
// Bounded on purpose: see `settleMs`. A page that has not gone quiet in a
|
|
89
|
+
// couple of seconds is not going to, and every extra second is recorded.
|
|
90
|
+
await this.page
|
|
91
|
+
.waitForLoadState("networkidle", { timeout: this.opts.settleMs ?? DEFAULT_SETTLE_MS })
|
|
92
|
+
.catch(() => undefined);
|
|
93
|
+
}
|
|
94
|
+
/** Which script lines this take put on screen, in order. */
|
|
95
|
+
get shownSteps() {
|
|
96
|
+
return this.shown;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Puts line `index` of the scenario's `steps` on screen.
|
|
100
|
+
*
|
|
101
|
+
* Scenarios narrate through this rather than through {@link caption} so the
|
|
102
|
+
* text has one home: the same array becomes the burnt-in caption, the
|
|
103
|
+
* manifest's step list and the text the landing page renders beside the clip.
|
|
104
|
+
* `record.ts` checks afterwards that the take used every line exactly once,
|
|
105
|
+
* in order, which is what stops the written workflow from drifting away from
|
|
106
|
+
* the recorded one.
|
|
107
|
+
*/
|
|
108
|
+
async step(index, holdMs = 0) {
|
|
109
|
+
const line = this.opts.steps?.[index];
|
|
110
|
+
if (line === undefined) {
|
|
111
|
+
throw new Error(`step(${index}): the scenario declares ${this.opts.steps?.length ?? 0} step(s)`);
|
|
112
|
+
}
|
|
113
|
+
this.shown.push(index);
|
|
114
|
+
this.record("step", undefined, line);
|
|
115
|
+
await this.caption(line, holdMs);
|
|
116
|
+
}
|
|
117
|
+
/** Shows a caption at the bottom of the frame. Pass `null` to clear it. */
|
|
118
|
+
async caption(text, holdMs = 0) {
|
|
119
|
+
await this.page.evaluate((t) => {
|
|
120
|
+
const api = window
|
|
121
|
+
.__screencast;
|
|
122
|
+
api?.caption(t);
|
|
123
|
+
}, text);
|
|
124
|
+
if (holdMs)
|
|
125
|
+
await this.beat(holdMs);
|
|
126
|
+
}
|
|
127
|
+
locator(target) {
|
|
128
|
+
if (typeof target === "string")
|
|
129
|
+
return this.page.locator(target).first();
|
|
130
|
+
if ("x" in target)
|
|
131
|
+
throw new Error("locator() needs a selector, not a point");
|
|
132
|
+
return target;
|
|
133
|
+
}
|
|
134
|
+
/** Whether this target is a field whose value should never be logged. */
|
|
135
|
+
async isSecret(target) {
|
|
136
|
+
if (typeof target === "object" && "x" in target)
|
|
137
|
+
return false;
|
|
138
|
+
try {
|
|
139
|
+
const type = await this.locator(target).getAttribute("type");
|
|
140
|
+
if (type?.toLowerCase() === "password")
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Not resolvable as an element - fall through to the name check, which
|
|
145
|
+
// is the only signal left.
|
|
146
|
+
}
|
|
147
|
+
const described = typeof target === "string" ? target : String(target);
|
|
148
|
+
return /pass(word|wd)?|secret|token|otp|\bpin\b/i.test(described);
|
|
149
|
+
}
|
|
150
|
+
async pointOf(target) {
|
|
151
|
+
if (typeof target === "object" && "x" in target)
|
|
152
|
+
return target;
|
|
153
|
+
const locator = this.locator(target);
|
|
154
|
+
await locator.waitFor({ state: "visible" });
|
|
155
|
+
await locator.scrollIntoViewIfNeeded();
|
|
156
|
+
const box = await locator.boundingBox();
|
|
157
|
+
if (!box)
|
|
158
|
+
throw new Error(`Target has no bounding box: ${String(target)}`);
|
|
159
|
+
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
160
|
+
}
|
|
161
|
+
/** Glides the pointer to `target` along an eased path. */
|
|
162
|
+
async moveTo(target, { steps = 26 } = {}) {
|
|
163
|
+
const to = await this.pointOf(target);
|
|
164
|
+
await this.glide(to, steps);
|
|
165
|
+
}
|
|
166
|
+
async glide(to, steps) {
|
|
167
|
+
const from = this.pointer;
|
|
168
|
+
const first = from.x === 0 && from.y === 0;
|
|
169
|
+
const count = first ? 1 : Math.max(1, steps);
|
|
170
|
+
for (let i = 1; i <= count; i++) {
|
|
171
|
+
const t = EASE(i / count);
|
|
172
|
+
await this.page.mouse.move(from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t);
|
|
173
|
+
if (!first)
|
|
174
|
+
await this.pause(12);
|
|
175
|
+
}
|
|
176
|
+
this.pointer = to;
|
|
177
|
+
}
|
|
178
|
+
/** Move, settle, then click - the pause is what makes the click legible. */
|
|
179
|
+
async click(target, { settleMs = 260 } = {}) {
|
|
180
|
+
this.record("click", target);
|
|
181
|
+
await this.performClick(target, settleMs);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The click itself, without logging it.
|
|
185
|
+
*
|
|
186
|
+
* `type` focuses its field by clicking it, which is one action from the
|
|
187
|
+
* scenario's point of view. Logging the inner click too would put a
|
|
188
|
+
* phantom step in the record of what the take did.
|
|
189
|
+
*/
|
|
190
|
+
async performClick(target, settleMs) {
|
|
191
|
+
await this.moveTo(target);
|
|
192
|
+
await this.beat(settleMs);
|
|
193
|
+
await this.page.mouse.down();
|
|
194
|
+
await this.pause(70);
|
|
195
|
+
await this.page.mouse.up();
|
|
196
|
+
await this.beat(320);
|
|
197
|
+
}
|
|
198
|
+
async doubleClick(target) {
|
|
199
|
+
this.record("doubleClick", target);
|
|
200
|
+
await this.moveTo(target);
|
|
201
|
+
await this.beat(220);
|
|
202
|
+
await this.page.mouse.dblclick(this.pointer.x, this.pointer.y);
|
|
203
|
+
await this.beat(320);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Clicks the field, then types character by character. `clear` selects the
|
|
207
|
+
* existing value first, so typing replaces it instead of appending - what a
|
|
208
|
+
* person does to a field that already holds a default.
|
|
209
|
+
*/
|
|
210
|
+
async type(target, text, { delay = 55, clear = false } = {}) {
|
|
211
|
+
// The action log exists to be read by someone deciding whether a scenario
|
|
212
|
+
// is safe to run, so it must not be the thing that leaks a credential into
|
|
213
|
+
// a terminal, a CI log or a screenshot of one. A password field records
|
|
214
|
+
// its length and nothing else.
|
|
215
|
+
this.record("type", target, (await this.isSecret(target)) ? maskOf(text) : text);
|
|
216
|
+
await this.performClick(target, 180);
|
|
217
|
+
if (clear) {
|
|
218
|
+
await this.page.keyboard.press("ControlOrMeta+A");
|
|
219
|
+
await this.beat(160);
|
|
220
|
+
}
|
|
221
|
+
const perChar = this.scaled(delay);
|
|
222
|
+
this.pausedMs += perChar * text.length;
|
|
223
|
+
await this.page.keyboard.type(text, { delay: perChar });
|
|
224
|
+
await this.beat(300);
|
|
225
|
+
}
|
|
226
|
+
async press(key) {
|
|
227
|
+
this.record("press", undefined, key);
|
|
228
|
+
await this.page.keyboard.press(key);
|
|
229
|
+
await this.beat(400);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Press-move-release drag. Deliberately slow and eased: this is the shot the
|
|
233
|
+
* whole harness exists for (reordering a tree row, moving a kanban card), and
|
|
234
|
+
* a fast drag reads as a teleport once it is re-encoded at 30fps.
|
|
235
|
+
*/
|
|
236
|
+
async drag(from, to, { steps = 34, liftMs = 320, dropMs = 420, } = {}) {
|
|
237
|
+
this.record("drag", from, `to ${describeTarget(to)}`);
|
|
238
|
+
await this.moveTo(from);
|
|
239
|
+
await this.beat(260);
|
|
240
|
+
await this.page.mouse.down();
|
|
241
|
+
await this.beat(liftMs);
|
|
242
|
+
const target = await this.pointOf(to);
|
|
243
|
+
const start = { ...this.pointer };
|
|
244
|
+
for (let i = 1; i <= steps; i++) {
|
|
245
|
+
const t = EASE(i / steps);
|
|
246
|
+
await this.page.mouse.move(start.x + (target.x - start.x) * t, start.y + (target.y - start.y) * t, { steps: 2 });
|
|
247
|
+
await this.pause(16);
|
|
248
|
+
}
|
|
249
|
+
this.pointer = target;
|
|
250
|
+
await this.beat(dropMs);
|
|
251
|
+
await this.page.mouse.up();
|
|
252
|
+
await this.beat(600);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Drag for the parts of the product that move things with the HTML5
|
|
256
|
+
* drag-and-drop API (`draggable` + `dragstart`/`dragover`/`drop`) rather than
|
|
257
|
+
* with raw pointer events: the process-mapper components palette, the kanban
|
|
258
|
+
* board.
|
|
259
|
+
*
|
|
260
|
+
* Chromium does not synthesise those events from `mouse.down`/`move`/`up`, so
|
|
261
|
+
* {@link drag} silently does nothing there. This dispatches the protocol
|
|
262
|
+
* itself - one `DataTransfer` shared by every event of the gesture, which is
|
|
263
|
+
* what carries the payload the source sets in `dragstart` to the drop
|
|
264
|
+
* handler - while still moving the real pointer, so the overlay cursor shows
|
|
265
|
+
* a hand carrying the thing across the screen.
|
|
266
|
+
*
|
|
267
|
+
* The mouse button is deliberately *not* pressed. A real `mouse.down` on a
|
|
268
|
+
* `draggable` element makes Chromium open its own native drag session, which
|
|
269
|
+
* then swallows every following `mousemove` - the overlay pointer freezes at
|
|
270
|
+
* the source and the clip shows a thing teleporting. Without the press the
|
|
271
|
+
* pointer travels normally, and `setDragging` puts a ghost chip under it so
|
|
272
|
+
* the gesture still reads as carrying something.
|
|
273
|
+
*/
|
|
274
|
+
async dragHtml5(from, to, { steps = 30, liftMs = 340, dropMs = 460, } = {}) {
|
|
275
|
+
this.record("drag", from, `to ${describeTarget(to)} (html5)`);
|
|
276
|
+
const source = this.locator(from);
|
|
277
|
+
await this.moveTo(from);
|
|
278
|
+
await this.beat(260);
|
|
279
|
+
await source.evaluate((el, at) => {
|
|
280
|
+
const dt = new DataTransfer();
|
|
281
|
+
window.__screencastDnd = { dt, source: el, over: null };
|
|
282
|
+
el.dispatchEvent(new DragEvent("dragstart", {
|
|
283
|
+
bubbles: true,
|
|
284
|
+
cancelable: true,
|
|
285
|
+
composed: true,
|
|
286
|
+
clientX: at.x,
|
|
287
|
+
clientY: at.y,
|
|
288
|
+
dataTransfer: dt,
|
|
289
|
+
}));
|
|
290
|
+
}, this.pointer);
|
|
291
|
+
await this.setDragging(true);
|
|
292
|
+
await this.beat(liftMs);
|
|
293
|
+
const target = await this.pointOf(to);
|
|
294
|
+
const start = { ...this.pointer };
|
|
295
|
+
for (let i = 1; i <= steps; i++) {
|
|
296
|
+
const t = EASE(i / steps);
|
|
297
|
+
const at = { x: start.x + (target.x - start.x) * t, y: start.y + (target.y - start.y) * t };
|
|
298
|
+
await this.page.mouse.move(at.x, at.y, { steps: 2 });
|
|
299
|
+
await this.dndEvent("dragover", at);
|
|
300
|
+
await this.pause(16);
|
|
301
|
+
}
|
|
302
|
+
this.pointer = target;
|
|
303
|
+
await this.beat(dropMs);
|
|
304
|
+
await this.dndEvent("drop", target);
|
|
305
|
+
await this.setDragging(false);
|
|
306
|
+
await this.beat(600);
|
|
307
|
+
}
|
|
308
|
+
async setDragging(dragging) {
|
|
309
|
+
await this.page.evaluate((on) => {
|
|
310
|
+
const api = window
|
|
311
|
+
.__screencast;
|
|
312
|
+
api?.setDragging(on);
|
|
313
|
+
}, dragging);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Dispatches one drag event at `at`, on whatever is under that point.
|
|
317
|
+
*
|
|
318
|
+
* `dragenter`/`dragleave` are kept in step with the element under the pointer
|
|
319
|
+
* because a drop zone that only arms itself on `dragenter` would otherwise
|
|
320
|
+
* never accept the drop. `drop` also ends the gesture with `dragend` on the
|
|
321
|
+
* source, which is where a well-behaved source cleans up its drag state.
|
|
322
|
+
*/
|
|
323
|
+
async dndEvent(type, at) {
|
|
324
|
+
await this.page.evaluate((arg) => {
|
|
325
|
+
const state = window.__screencastDnd;
|
|
326
|
+
if (!state)
|
|
327
|
+
throw new Error("dragHtml5: no drag in progress");
|
|
328
|
+
const el = document.elementFromPoint(arg.x, arg.y);
|
|
329
|
+
if (!el)
|
|
330
|
+
return;
|
|
331
|
+
const init = {
|
|
332
|
+
bubbles: true,
|
|
333
|
+
cancelable: true,
|
|
334
|
+
composed: true,
|
|
335
|
+
clientX: arg.x,
|
|
336
|
+
clientY: arg.y,
|
|
337
|
+
dataTransfer: state.dt,
|
|
338
|
+
};
|
|
339
|
+
if (el !== state.over) {
|
|
340
|
+
if (state.over)
|
|
341
|
+
state.over.dispatchEvent(new DragEvent("dragleave", init));
|
|
342
|
+
el.dispatchEvent(new DragEvent("dragenter", init));
|
|
343
|
+
state.over = el;
|
|
344
|
+
}
|
|
345
|
+
el.dispatchEvent(new DragEvent(arg.type, init));
|
|
346
|
+
if (arg.type === "drop") {
|
|
347
|
+
state.source.dispatchEvent(new DragEvent("dragend", init));
|
|
348
|
+
window.__screencastDnd = null;
|
|
349
|
+
}
|
|
350
|
+
}, { x: at.x, y: at.y, type });
|
|
351
|
+
}
|
|
352
|
+
/** Smooth wheel scroll, in the page or in whatever is under the pointer. */
|
|
353
|
+
async scrollBy(deltaY, { steps = 24 } = {}) {
|
|
354
|
+
this.record("scroll", undefined, `${Math.round(deltaY)}px`);
|
|
355
|
+
const per = deltaY / steps;
|
|
356
|
+
for (let i = 0; i < steps; i++) {
|
|
357
|
+
await this.page.mouse.wheel(0, per);
|
|
358
|
+
await this.pause(16);
|
|
359
|
+
}
|
|
360
|
+
await this.beat(400);
|
|
361
|
+
}
|
|
362
|
+
async waitFor(target) {
|
|
363
|
+
this.record("waitFor", target);
|
|
364
|
+
await this.locator(target).waitFor({ state: "visible" });
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* A multi-page walkthrough: go somewhere, narrate it, let it breathe, move on.
|
|
368
|
+
*
|
|
369
|
+
* This is the shape most requests actually have ("a walkthrough of the five
|
|
370
|
+
* main pages"), and writing it by hand is fifty lines of goto/caption/scroll
|
|
371
|
+
* whose only real content is the paths. Going through one helper also keeps
|
|
372
|
+
* the rhythm identical between stops, which is most of what makes a tour
|
|
373
|
+
* watchable rather than a run of jump cuts.
|
|
374
|
+
*
|
|
375
|
+
* Every pause here is pace-scaled like any other, so `--duration` re-cuts a
|
|
376
|
+
* tour without the scenario changing.
|
|
377
|
+
*/
|
|
378
|
+
async tour(stops, { dwellMs = 1400 } = {}) {
|
|
379
|
+
for (const stop of stops) {
|
|
380
|
+
await this.goto(stop.path);
|
|
381
|
+
if (stop.waitFor)
|
|
382
|
+
await this.waitFor(stop.waitFor);
|
|
383
|
+
if (stop.step !== undefined)
|
|
384
|
+
await this.step(stop.step);
|
|
385
|
+
else if (stop.caption)
|
|
386
|
+
await this.caption(stop.caption);
|
|
387
|
+
await this.beat(stop.dwellMs ?? dwellMs);
|
|
388
|
+
if (stop.scroll) {
|
|
389
|
+
// A value of 1 or less is a share of the viewport, which reads the
|
|
390
|
+
// same on a phone and a desktop; anything larger is pixels.
|
|
391
|
+
const viewport = this.page.viewportSize();
|
|
392
|
+
const amount = stop.scroll <= 1 && viewport ? Math.round(viewport.height * stop.scroll) : stop.scroll;
|
|
393
|
+
await this.scrollBy(amount);
|
|
394
|
+
await this.beat(stop.dwellMs ?? dwellMs);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** What a secret value looks like in the log: its shape, never its content. */
|
|
400
|
+
function maskOf(text) {
|
|
401
|
+
return `${"\u2022".repeat(Math.min(8, text.length))} (${text.length} chars)`;
|
|
402
|
+
}
|
|
403
|
+
/** A target as a short readable string, for the action log. */
|
|
404
|
+
function describeTarget(target) {
|
|
405
|
+
if (typeof target === "string")
|
|
406
|
+
return target;
|
|
407
|
+
if ("x" in target)
|
|
408
|
+
return `(${Math.round(target.x)}, ${Math.round(target.y)})`;
|
|
409
|
+
// Playwright locators stringify to something like `locator('.foo')`.
|
|
410
|
+
return String(target)
|
|
411
|
+
.replace(/^locator\(/, "")
|
|
412
|
+
.replace(/\)$/, "")
|
|
413
|
+
.replace(/^['"]|['"]$/g, "");
|
|
414
|
+
}
|
|
415
|
+
//# sourceMappingURL=director.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"director.js","sourceRoot":"","sources":["../../src/director.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning "around 30 seconds" into a pace.
|
|
3
|
+
*
|
|
4
|
+
* Every deliberate pause in a take is multiplied by `pace`, so a clip's length
|
|
5
|
+
* is roughly linear in it. Roughly, not exactly: Playwright's own waits - a
|
|
6
|
+
* navigation, a network round trip, an animation settling - are not scaled,
|
|
7
|
+
* because slowing the recorder down does not slow the app down. So the
|
|
8
|
+
* resolved pace is measured rather than assumed, and what actually came out is
|
|
9
|
+
* reported next to what was asked for.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MIN_PACE = 0.4;
|
|
12
|
+
export declare const MAX_PACE = 2.5;
|
|
13
|
+
/** Parses `30s`, `1m30s`, `45`, `2m`. Returns milliseconds. */
|
|
14
|
+
export declare function parseDuration(input: string): number;
|
|
15
|
+
export interface PaceSolution {
|
|
16
|
+
readonly pace: number;
|
|
17
|
+
readonly clamped: boolean;
|
|
18
|
+
/** What a pace-1 take measured, in ms. */
|
|
19
|
+
readonly naturalMs: number;
|
|
20
|
+
readonly targetMs: number;
|
|
21
|
+
/** Human note when the target could not be met. */
|
|
22
|
+
readonly warning?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The pace that should land a take near `targetMs`, given a measured run.
|
|
26
|
+
*
|
|
27
|
+
* Clamped, because there is a point past which a clip stops being watchable:
|
|
28
|
+
* below 0.4 the pointer teleports and typed text is a blur, above 2.5 it is
|
|
29
|
+
* slower than anyone will sit through. Hitting a clamp is reported rather than
|
|
30
|
+
* silently obeyed - the honest answer is "this scenario has too much in it for
|
|
31
|
+
* 30 seconds", not a clip nobody can follow.
|
|
32
|
+
*/
|
|
33
|
+
export declare function solvePace(naturalMs: number, targetMs: number, scaledPauseMs?: number): PaceSolution;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ScreencastError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Turning "around 30 seconds" into a pace.
|
|
4
|
+
*
|
|
5
|
+
* Every deliberate pause in a take is multiplied by `pace`, so a clip's length
|
|
6
|
+
* is roughly linear in it. Roughly, not exactly: Playwright's own waits - a
|
|
7
|
+
* navigation, a network round trip, an animation settling - are not scaled,
|
|
8
|
+
* because slowing the recorder down does not slow the app down. So the
|
|
9
|
+
* resolved pace is measured rather than assumed, and what actually came out is
|
|
10
|
+
* reported next to what was asked for.
|
|
11
|
+
*/
|
|
12
|
+
export const MIN_PACE = 0.4;
|
|
13
|
+
export const MAX_PACE = 2.5;
|
|
14
|
+
/** Parses `30s`, `1m30s`, `45`, `2m`. Returns milliseconds. */
|
|
15
|
+
export function parseDuration(input) {
|
|
16
|
+
const text = input.trim().toLowerCase();
|
|
17
|
+
const clock = /^(\d+)m(?:in)?(?:\s*(\d+)s?)?$/.exec(text);
|
|
18
|
+
if (clock) {
|
|
19
|
+
return (Number(clock[1]) * 60 + Number(clock[2] ?? 0)) * 1000;
|
|
20
|
+
}
|
|
21
|
+
const seconds = /^(\d+(?:\.\d+)?)s(?:ec)?$/.exec(text);
|
|
22
|
+
if (seconds)
|
|
23
|
+
return Math.round(Number(seconds[1]) * 1000);
|
|
24
|
+
const bare = /^(\d+(?:\.\d+)?)$/.exec(text);
|
|
25
|
+
if (bare)
|
|
26
|
+
return Math.round(Number(bare[1]) * 1000);
|
|
27
|
+
throw new ScreencastError(`Cannot read a duration from \`${input}\``, "VALIDATION_ERROR", [
|
|
28
|
+
"Use seconds (`30s`), minutes and seconds (`1m30s`), or a bare number of seconds (`30`)",
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The pace that should land a take near `targetMs`, given a measured run.
|
|
33
|
+
*
|
|
34
|
+
* Clamped, because there is a point past which a clip stops being watchable:
|
|
35
|
+
* below 0.4 the pointer teleports and typed text is a blur, above 2.5 it is
|
|
36
|
+
* slower than anyone will sit through. Hitting a clamp is reported rather than
|
|
37
|
+
* silently obeyed - the honest answer is "this scenario has too much in it for
|
|
38
|
+
* 30 seconds", not a clip nobody can follow.
|
|
39
|
+
*/
|
|
40
|
+
export function solvePace(naturalMs, targetMs, scaledPauseMs = naturalMs) {
|
|
41
|
+
if (naturalMs <= 0 || scaledPauseMs <= 0) {
|
|
42
|
+
return { pace: 1, clamped: false, naturalMs, targetMs };
|
|
43
|
+
}
|
|
44
|
+
// A take is `fixed + pace x scalable`, measured at pace 1. Solving with the
|
|
45
|
+
// real split lands close; assuming the whole clip scales undershoots by
|
|
46
|
+
// whatever the app itself spent loading, which on a real site is seconds.
|
|
47
|
+
const fixedMs = Math.max(0, naturalMs - scaledPauseMs);
|
|
48
|
+
const ideal = (targetMs - fixedMs) / scaledPauseMs;
|
|
49
|
+
const pace = Math.min(MAX_PACE, Math.max(MIN_PACE, ideal));
|
|
50
|
+
const clamped = Math.abs(pace - ideal) > 1e-9;
|
|
51
|
+
if (!clamped) {
|
|
52
|
+
return { pace: Number(pace.toFixed(3)), clamped, naturalMs, targetMs };
|
|
53
|
+
}
|
|
54
|
+
const reachableMs = Math.round(fixedMs + scaledPauseMs * pace);
|
|
55
|
+
return {
|
|
56
|
+
pace: Number(pace.toFixed(3)),
|
|
57
|
+
clamped,
|
|
58
|
+
naturalMs,
|
|
59
|
+
targetMs,
|
|
60
|
+
warning: `target ${(targetMs / 1000).toFixed(0)}s needs pace ${ideal.toFixed(2)}, ` +
|
|
61
|
+
`which is outside the watchable range - clamped to ${pace} for about ` +
|
|
62
|
+
`${(reachableMs / 1000).toFixed(0)}s. ` +
|
|
63
|
+
(ideal < MIN_PACE
|
|
64
|
+
? "Drop a step or a tour stop to make it shorter."
|
|
65
|
+
: "Add a beat or another stop to fill the time."),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=duration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.js","sourceRoot":"","sources":["../../src/duration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;;GASG;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAG,CAAC;AAC5B,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAG,CAAC;AAE5B,+DAA+D;AAC/D,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAExC,MAAM,KAAK,GAAG,gCAAgC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1D,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAChE,CAAC;IAED,MAAM,OAAO,GAAG,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,IAAI,OAAO;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAE1D,MAAM,IAAI,GAAG,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAEpD,MAAM,IAAI,eAAe,CAAC,iCAAiC,KAAK,IAAI,EAAE,kBAAkB,EAAE;QACxF,wFAAwF;KACzF,CAAC,CAAC;AACL,CAAC;AAYD;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CACvB,SAAiB,EACjB,QAAgB,EAChB,aAAa,GAAG,SAAS;IAEzB,IAAI,SAAS,IAAI,CAAC,IAAI,aAAa,IAAI,CAAC,EAAE,CAAC;QACzC,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;IAC1D,CAAC;IAED,4EAA4E;IAC5E,wEAAwE;IACxE,0EAA0E;IAC1E,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,GAAG,aAAa,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,CAAC,QAAQ,GAAG,OAAO,CAAC,GAAG,aAAa,CAAC;IACnD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;IAC3D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC;IAE9C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;IACzE,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,aAAa,GAAG,IAAI,CAAC,CAAC;IAC/D,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC7B,OAAO;QACP,SAAS;QACT,QAAQ;QACR,OAAO,EACL,UAAU,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;YAC1E,qDAAqD,IAAI,aAAa;YACtE,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK;YACvC,CAAC,KAAK,GAAG,QAAQ;gBACf,CAAC,CAAC,gDAAgD;gBAClD,CAAC,CAAC,8CAA8C,CAAC;KACtD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type PosterEncoder, type Toolchain } from "./toolchain.js";
|
|
2
|
+
export interface EncodeSettings {
|
|
3
|
+
/** Output width in px; height follows the source aspect ratio. */
|
|
4
|
+
readonly width: number;
|
|
5
|
+
/** Frame rate of the mp4 and webm deliverables. */
|
|
6
|
+
readonly fps: number;
|
|
7
|
+
readonly mp4: {
|
|
8
|
+
readonly crf: number;
|
|
9
|
+
readonly preset: string;
|
|
10
|
+
readonly profile: string;
|
|
11
|
+
};
|
|
12
|
+
readonly webm: {
|
|
13
|
+
readonly crf: number;
|
|
14
|
+
};
|
|
15
|
+
readonly poster: {
|
|
16
|
+
readonly quality: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Looping images, for the places a `<video>` does not render: a README, an
|
|
20
|
+
* npm page, an email. Both are opt-in and both are far heavier than the mp4
|
|
21
|
+
* - measured on a real 16.7s screencast at 800px/15fps, the mp4 was 182 KB,
|
|
22
|
+
* animated WebP 944 KB and GIF 2,034 KB.
|
|
23
|
+
*
|
|
24
|
+
* Prefer WebP: same content, roughly half the bytes, and full colour rather
|
|
25
|
+
* than a 256-entry palette. GIF is the fallback for somewhere that will not
|
|
26
|
+
* render WebP.
|
|
27
|
+
*/
|
|
28
|
+
readonly gif: boolean;
|
|
29
|
+
readonly animatedWebp: boolean;
|
|
30
|
+
readonly gifWidth: number;
|
|
31
|
+
readonly gifFps: number;
|
|
32
|
+
/** Quality for the animated WebP, 0-100. */
|
|
33
|
+
readonly animatedWebpQuality: number;
|
|
34
|
+
}
|
|
35
|
+
export declare const DEFAULT_ENCODE_SETTINGS: EncodeSettings;
|
|
36
|
+
export interface EncodeOptions extends EncodeSettings {
|
|
37
|
+
/** Raw Playwright capture (VP8 webm). */
|
|
38
|
+
readonly input: string;
|
|
39
|
+
/** Directory the deliverables are written to. */
|
|
40
|
+
readonly outDir: string;
|
|
41
|
+
/** File stem, e.g. `task-create`. */
|
|
42
|
+
readonly id: string;
|
|
43
|
+
/** Seconds to cut off the head: setup, navigation, first paint. */
|
|
44
|
+
readonly trimStart: number;
|
|
45
|
+
/** Pre-detected toolchain, so a batch does not re-probe per clip. */
|
|
46
|
+
readonly toolchain?: Toolchain;
|
|
47
|
+
}
|
|
48
|
+
export interface EncodeResult {
|
|
49
|
+
readonly mp4: string;
|
|
50
|
+
readonly webm: string;
|
|
51
|
+
readonly poster: string;
|
|
52
|
+
readonly gif?: string;
|
|
53
|
+
readonly animatedWebp?: string;
|
|
54
|
+
/** Byte size per deliverable, keyed by file name. */
|
|
55
|
+
readonly sizes: Readonly<Record<string, number>>;
|
|
56
|
+
/** Which poster path was taken, for the CLI to report. */
|
|
57
|
+
readonly posterEncoder: PosterEncoder;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Turns the raw Playwright capture into web deliverables.
|
|
61
|
+
*
|
|
62
|
+
* - mp4 (h264, yuv420p, faststart) - the one a page plays.
|
|
63
|
+
* - webm (vp9) - smaller, served first via `<source>`.
|
|
64
|
+
* - poster - the frame shown before the video decodes, so nothing pops in.
|
|
65
|
+
* - gif - optional, opt-in.
|
|
66
|
+
*/
|
|
67
|
+
export declare function encode(opts: EncodeOptions): Promise<EncodeResult>;
|