@juangadm/pre-post 1.4.0 → 1.5.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/README.md +1 -0
- package/assets/labels/post.png +0 -0
- package/assets/labels/pre.png +0 -0
- package/dist/attach.d.ts +59 -0
- package/dist/attach.d.ts.map +1 -0
- package/dist/attach.js +137 -0
- package/dist/attach.js.map +1 -0
- package/dist/bin/cli.js +20 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/browser.d.ts +48 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +60 -31
- package/dist/browser.js.map +1 -1
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +14 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/pr.d.ts +5 -0
- package/dist/commands/pr.d.ts.map +1 -1
- package/dist/commands/pr.js +51 -19
- package/dist/commands/pr.js.map +1 -1
- package/dist/compose.d.ts +72 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +237 -0
- package/dist/compose.js.map +1 -0
- package/dist/ffmpeg.d.ts +35 -0
- package/dist/ffmpeg.d.ts.map +1 -0
- package/dist/ffmpeg.js +143 -0
- package/dist/ffmpeg.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/moments.d.ts +83 -0
- package/dist/moments.d.ts.map +1 -0
- package/dist/moments.js +170 -0
- package/dist/moments.js.map +1 -0
- package/dist/record.d.ts +69 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +223 -0
- package/dist/record.js.map +1 -0
- package/dist/report.d.ts +9 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +56 -3
- package/dist/report.js.map +1 -1
- package/dist/run.d.ts +2 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +2 -1
- package/dist/run.js.map +1 -1
- package/dist/timeline.d.ts +46 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +80 -0
- package/dist/timeline.js.map +1 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/video.d.ts +19 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +109 -0
- package/dist/video.js.map +1 -0
- package/package.json +4 -2
- package/skill/SKILL.md +24 -5
package/dist/compose.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre beside Post, in one clip.
|
|
3
|
+
*
|
|
4
|
+
* Two separate players cannot be kept in step — a reviewer presses play twice
|
|
5
|
+
* and compares by eye — so the two recordings are drawn side by side into one
|
|
6
|
+
* video, on the step-aligned timeline from `timeline.ts`. Drawn by the capture
|
|
7
|
+
* browser on a canvas, like the summary sheet: the bundled ffmpeg has no
|
|
8
|
+
* `hstack` and no text rendering, and the browser has both for free.
|
|
9
|
+
*
|
|
10
|
+
* On top of the frames: the PRE / Post lockup from the brand, the Moment's
|
|
11
|
+
* name and the step in progress (so someone who never saw the code can follow
|
|
12
|
+
* what is being done), a marker where each click lands, and a card over a
|
|
13
|
+
* side that could not do a step — the usual reason being that the feature is
|
|
14
|
+
* new in this PR.
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import { getBrowser } from './browser.js';
|
|
19
|
+
import { describeStep } from './moments.js';
|
|
20
|
+
import { alignTimeline, frameAt, timelineDuration } from './timeline.js';
|
|
21
|
+
import { ensureFfmpeg, fitSize, startEncoder } from './ffmpeg.js';
|
|
22
|
+
export const FPS = 30;
|
|
23
|
+
/** Widest a pane is drawn; a desktop page is scaled down to it, a phone is not. */
|
|
24
|
+
export const MAX_PANE_WIDTH = 800;
|
|
25
|
+
const PAD = 20;
|
|
26
|
+
const HEADER = 56;
|
|
27
|
+
const FOOTER = 44;
|
|
28
|
+
/** How long a click marker takes to fade, in output ms. */
|
|
29
|
+
const CLICK_MARK_MS = 450;
|
|
30
|
+
const JPEG_QUALITY = 0.9;
|
|
31
|
+
const even = (n) => Math.ceil(n / 2) * 2;
|
|
32
|
+
/** Canvas size for a viewport. Even on both axes, which VP8's 4:2:0 needs. */
|
|
33
|
+
export function layoutFor(viewport) {
|
|
34
|
+
const scale = Math.min(1, MAX_PANE_WIDTH / viewport.width);
|
|
35
|
+
const paneWidth = Math.round(viewport.width * scale);
|
|
36
|
+
const paneHeight = Math.round(viewport.height * scale);
|
|
37
|
+
return { width: even(PAD * 3 + paneWidth * 2), height: even(HEADER + paneHeight + FOOTER), paneWidth, paneHeight, scale };
|
|
38
|
+
}
|
|
39
|
+
function labelUri(name) {
|
|
40
|
+
try {
|
|
41
|
+
const file = fileURLToPath(new URL(`../assets/labels/${name}.png`, import.meta.url));
|
|
42
|
+
return `data:image/png;base64,${fs.readFileSync(file).toString('base64')}`;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
let labels;
|
|
49
|
+
/** What to draw for a tick's pointer marks, if anything. */
|
|
50
|
+
export function pointersFor(tick, steps, pre, post) {
|
|
51
|
+
if (tick.step < 0)
|
|
52
|
+
return [];
|
|
53
|
+
const step = steps[tick.step];
|
|
54
|
+
if (!step || (step.verb !== 'click' && step.verb !== 'hover' && step.verb !== 'type'))
|
|
55
|
+
return [];
|
|
56
|
+
const out = [];
|
|
57
|
+
const add = (side, marks, missing) => {
|
|
58
|
+
const mark = !missing && marks.find(m => m.step === tick.step);
|
|
59
|
+
if (!mark)
|
|
60
|
+
return;
|
|
61
|
+
if (step.verb === 'hover')
|
|
62
|
+
out.push({ side, x: mark.x, y: mark.y, kind: 'hover', p: 0 });
|
|
63
|
+
else if (tick.sinceStep < CLICK_MARK_MS)
|
|
64
|
+
out.push({ side, x: mark.x, y: mark.y, kind: 'click', p: tick.sinceStep / CLICK_MARK_MS });
|
|
65
|
+
};
|
|
66
|
+
add('a', pre, tick.aMissing);
|
|
67
|
+
add('b', post, tick.bMissing);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/** The line under the clip: which step of how many, in words. */
|
|
71
|
+
export function captionFor(tick, steps) {
|
|
72
|
+
if (tick.step < 0 || !steps.length)
|
|
73
|
+
return '';
|
|
74
|
+
let i = Math.min(tick.step, steps.length - 1);
|
|
75
|
+
// "Wait" says nothing to a viewer; keep showing what they are watching the result of.
|
|
76
|
+
while (i > 0 && steps[i].verb === 'wait')
|
|
77
|
+
i--;
|
|
78
|
+
if (steps[i].verb === 'wait')
|
|
79
|
+
return '';
|
|
80
|
+
return `${i + 1}/${steps.length} · ${describeStep(steps[i])}`;
|
|
81
|
+
}
|
|
82
|
+
/** In-page drawing code. Plain JS, run by the compositor page. */
|
|
83
|
+
const PAGE_SCRIPT = `
|
|
84
|
+
const cfg = window.__cfg;
|
|
85
|
+
const c = document.getElementById('c');
|
|
86
|
+
const g = c.getContext('2d');
|
|
87
|
+
// One decoded frame per side: ticks only move forward, so the frame being
|
|
88
|
+
// replaced is never needed again.
|
|
89
|
+
const shown = { a: null, b: null };
|
|
90
|
+
const labels = {};
|
|
91
|
+
const load = src => new Promise((res, rej) => { const i = new Image(); i.onload = () => res(i); i.onerror = rej; i.src = src; });
|
|
92
|
+
window.init = async () => {
|
|
93
|
+
if (cfg.labels.pre) labels.pre = await load(cfg.labels.pre);
|
|
94
|
+
if (cfg.labels.post) labels.post = await load(cfg.labels.post);
|
|
95
|
+
};
|
|
96
|
+
function rounded(x, y, w, h, r) { g.beginPath(); g.roundRect(x, y, w, h, r); }
|
|
97
|
+
function paneX(side) { return side === 'a' ? cfg.pad : cfg.pad * 2 + cfg.paneWidth; }
|
|
98
|
+
function drawLabel(side) {
|
|
99
|
+
const img = side === 'a' ? labels.pre : labels.post;
|
|
100
|
+
const x = paneX(side);
|
|
101
|
+
const baseline = cfg.header - 10;
|
|
102
|
+
if (img) {
|
|
103
|
+
const h = img.height / 2, w = img.width / 2;
|
|
104
|
+
// Each image carries its own padding; these offsets put both glyphs'
|
|
105
|
+
// baselines on one line (measured on the rendered labels).
|
|
106
|
+
g.drawImage(img, x - 4, baseline - h + (side === 'a' ? 13 : 5), w, h);
|
|
107
|
+
} else {
|
|
108
|
+
g.fillStyle = '#262626'; g.font = '600 20px system-ui, sans-serif';
|
|
109
|
+
g.fillText(side === 'a' ? 'PRE' : 'Post', x, baseline);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function drawPane(side, img, card) {
|
|
113
|
+
const x = paneX(side), y = cfg.header;
|
|
114
|
+
g.save();
|
|
115
|
+
rounded(x, y, cfg.paneWidth, cfg.paneHeight, 8);
|
|
116
|
+
g.fillStyle = '#fff'; g.fill();
|
|
117
|
+
g.clip();
|
|
118
|
+
if (img) g.drawImage(img, x, y, cfg.paneWidth, cfg.paneHeight);
|
|
119
|
+
if (card) {
|
|
120
|
+
g.fillStyle = img ? 'rgba(245,245,245,0.86)' : '#fafafa';
|
|
121
|
+
g.fillRect(x, y, cfg.paneWidth, cfg.paneHeight);
|
|
122
|
+
g.textAlign = 'center';
|
|
123
|
+
g.fillStyle = '#262626'; g.font = '600 17px system-ui, sans-serif';
|
|
124
|
+
g.fillText(card.title, x + cfg.paneWidth / 2, y + cfg.paneHeight / 2 - (card.detail ? 6 : -6));
|
|
125
|
+
if (card.detail) {
|
|
126
|
+
g.fillStyle = '#737373'; g.font = '14px system-ui, sans-serif';
|
|
127
|
+
g.fillText(card.detail, x + cfg.paneWidth / 2, y + cfg.paneHeight / 2 + 18);
|
|
128
|
+
}
|
|
129
|
+
g.textAlign = 'left';
|
|
130
|
+
}
|
|
131
|
+
g.restore();
|
|
132
|
+
g.save();
|
|
133
|
+
rounded(x + 0.5, y + 0.5, cfg.paneWidth - 1, cfg.paneHeight - 1, 8);
|
|
134
|
+
g.strokeStyle = '#e5e5e5'; g.lineWidth = 1; g.stroke();
|
|
135
|
+
g.restore();
|
|
136
|
+
}
|
|
137
|
+
function drawPointer(pt) {
|
|
138
|
+
const x = paneX(pt.side) + pt.x * cfg.scale, y = cfg.header + pt.y * cfg.scale;
|
|
139
|
+
g.save();
|
|
140
|
+
if (pt.kind === 'hover') {
|
|
141
|
+
g.beginPath(); g.arc(x, y, 7, 0, Math.PI * 2);
|
|
142
|
+
g.fillStyle = 'rgba(246, 105, 108, 0.45)'; g.fill();
|
|
143
|
+
g.strokeStyle = '#fff'; g.lineWidth = 2; g.stroke();
|
|
144
|
+
} else {
|
|
145
|
+
const a = 1 - pt.p;
|
|
146
|
+
g.beginPath(); g.arc(x, y, 8 + 16 * pt.p, 0, Math.PI * 2);
|
|
147
|
+
g.strokeStyle = 'rgba(246, 105, 108,' + a + ')'; g.lineWidth = 3; g.stroke();
|
|
148
|
+
g.beginPath(); g.arc(x, y, 5, 0, Math.PI * 2);
|
|
149
|
+
g.fillStyle = 'rgba(246, 105, 108,' + a + ')'; g.fill();
|
|
150
|
+
}
|
|
151
|
+
g.restore();
|
|
152
|
+
}
|
|
153
|
+
async function frame(side, data) {
|
|
154
|
+
if (data) shown[side] = await load('data:image/jpeg;base64,' + data);
|
|
155
|
+
return shown[side];
|
|
156
|
+
}
|
|
157
|
+
window.draw = async t => {
|
|
158
|
+
const [a, b] = await Promise.all([frame('a', t.a), frame('b', t.b)]);
|
|
159
|
+
g.fillStyle = '#f5f5f5'; g.fillRect(0, 0, c.width, c.height);
|
|
160
|
+
drawLabel('a'); drawLabel('b');
|
|
161
|
+
drawPane('a', a, t.aCard); drawPane('b', b, null);
|
|
162
|
+
for (const pt of t.pointers) drawPointer(pt);
|
|
163
|
+
const fy = cfg.header + cfg.paneHeight + cfg.footer / 2 + 5;
|
|
164
|
+
g.fillStyle = '#262626'; g.font = '600 15px system-ui, sans-serif';
|
|
165
|
+
g.fillText(cfg.name, cfg.pad, fy);
|
|
166
|
+
if (t.caption) {
|
|
167
|
+
g.textAlign = 'right'; g.fillStyle = '#737373'; g.font = '14px ui-monospace, Menlo, Consolas, monospace';
|
|
168
|
+
g.fillText(t.caption, c.width - cfg.pad, fy);
|
|
169
|
+
g.textAlign = 'left';
|
|
170
|
+
}
|
|
171
|
+
return c.toDataURL('image/jpeg', ${JPEG_QUALITY}).slice('data:image/jpeg;base64,'.length);
|
|
172
|
+
};
|
|
173
|
+
`;
|
|
174
|
+
/** Draw, encode and size-check one Moment's clip. */
|
|
175
|
+
export async function composeMoment(input) {
|
|
176
|
+
const { pre, post, steps } = input;
|
|
177
|
+
const layout = layoutFor(post.viewport);
|
|
178
|
+
const preTiming = pre ?? { start: post.start, end: post.start, marks: [] };
|
|
179
|
+
const ticks = alignTimeline(preTiming, post, FPS);
|
|
180
|
+
const timesA = pre?.frames.map(f => f.t) ?? [];
|
|
181
|
+
const timesB = post.frames.map(f => f.t);
|
|
182
|
+
const [ffmpeg, browser] = await Promise.all([ensureFfmpeg(), getBrowser()]);
|
|
183
|
+
const ctx = await browser.newContext({ viewport: { width: layout.width, height: layout.height }, deviceScaleFactor: 1 });
|
|
184
|
+
const encoder = startEncoder(ffmpeg, input.out, FPS);
|
|
185
|
+
let last = null;
|
|
186
|
+
try {
|
|
187
|
+
const page = await ctx.newPage();
|
|
188
|
+
const cfg = {
|
|
189
|
+
pad: PAD, header: HEADER, footer: FOOTER, name: input.name,
|
|
190
|
+
paneWidth: layout.paneWidth, paneHeight: layout.paneHeight, scale: layout.scale,
|
|
191
|
+
labels: labels ??= { pre: labelUri('pre'), post: labelUri('post') },
|
|
192
|
+
};
|
|
193
|
+
await page.setContent(`<!doctype html><body style="margin:0"><canvas id="c" width="${layout.width}" height="${layout.height}"></canvas>`
|
|
194
|
+
+ `<script>window.__cfg = ${JSON.stringify(cfg)};${PAGE_SCRIPT}</script>`);
|
|
195
|
+
await page.evaluate('window.init()');
|
|
196
|
+
let sentA = -1;
|
|
197
|
+
let sentB = -1;
|
|
198
|
+
let lastKey = '';
|
|
199
|
+
for (const tick of ticks) {
|
|
200
|
+
const ia = pre ? frameAt(timesA, tick.a) : -1;
|
|
201
|
+
const ib = frameAt(timesB, tick.b);
|
|
202
|
+
const aCard = (!pre || tick.aMissing) && input.preCard ? input.preCard : null;
|
|
203
|
+
const pointers = pointersFor(tick, steps, pre?.pointers ?? [], post.pointers);
|
|
204
|
+
const caption = captionFor(tick, steps);
|
|
205
|
+
// An unchanged picture is the same JPEG: most of a clip is a page
|
|
206
|
+
// holding still between steps, and those ticks cost nothing to draw.
|
|
207
|
+
const key = `${ia}|${ib}|${aCard ? 1 : 0}|${caption}|${pointers.map(p => `${p.side}${Math.round(p.p * 20)}`).join()}`;
|
|
208
|
+
if (key !== lastKey || !last) {
|
|
209
|
+
// Only a frame the page is not already showing crosses over.
|
|
210
|
+
const a = ia >= 0 && ia !== sentA ? pre.frames[ia].data.toString('base64') : null;
|
|
211
|
+
const b = ib >= 0 && ib !== sentB ? post.frames[ib].data.toString('base64') : null;
|
|
212
|
+
sentA = ia;
|
|
213
|
+
sentB = ib;
|
|
214
|
+
const jpeg = await page.evaluate(t => window.draw(t), {
|
|
215
|
+
a, b, aCard, pointers, caption,
|
|
216
|
+
});
|
|
217
|
+
last = Buffer.from(jpeg, 'base64');
|
|
218
|
+
lastKey = key;
|
|
219
|
+
}
|
|
220
|
+
await encoder.write(last);
|
|
221
|
+
}
|
|
222
|
+
await encoder.finish();
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
await encoder.finish().catch(() => undefined);
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
await ctx.close().catch(() => undefined);
|
|
230
|
+
}
|
|
231
|
+
if (last)
|
|
232
|
+
fs.writeFileSync(input.poster, last);
|
|
233
|
+
const durationMs = timelineDuration(ticks, FPS);
|
|
234
|
+
const bytes = await fitSize(ffmpeg, input.out, durationMs);
|
|
235
|
+
return { durationMs, bytes };
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=compose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"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"}
|
package/dist/ffmpeg.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encoding with the ffmpeg Playwright already knows how to install.
|
|
3
|
+
*
|
|
4
|
+
* Playwright ships a 2.5 MB ffmpeg for its own video recording, installed the
|
|
5
|
+
* same way as the headless browser, so nobody has to install anything. It is a
|
|
6
|
+
* minimal build (measured, revision 1011): libvpx VP8 encode and decode, mjpeg
|
|
7
|
+
* decode, image2pipe in, WebM out, pad/crop/scale — and no `hstack`, no
|
|
8
|
+
* drawtext, no `-` alias for stdin (it has to be `pipe:0`, as Playwright's own
|
|
9
|
+
* recorder writes it). That is why frames are composited in a canvas and piped
|
|
10
|
+
* in as JPEGs, and why the output is WebM.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* The largest clip that is sure to upload. GitHub takes 10 MB of video on a
|
|
14
|
+
* Free plan (100 MB on paid ones, but nothing here can tell which), so a clip
|
|
15
|
+
* over this is re-encoded smaller rather than refused at upload.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MAX_CLIP_BYTES: number;
|
|
18
|
+
/** Where ffmpeg is, without installing anything. For `doctor`. */
|
|
19
|
+
export declare function findFfmpeg(): string | null;
|
|
20
|
+
/** ffmpeg, installing Playwright's (one time, ~2 MB) when there is none. */
|
|
21
|
+
export declare function ensureFfmpeg(): Promise<string>;
|
|
22
|
+
export interface Encoder {
|
|
23
|
+
/** Queue one JPEG frame; resolves when ffmpeg can take more. */
|
|
24
|
+
write(jpeg: Buffer): Promise<void>;
|
|
25
|
+
/** Close the input and wait for the file. */
|
|
26
|
+
finish(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export declare function startEncoder(ffmpeg: string, out: string, fps: number): Encoder;
|
|
29
|
+
/**
|
|
30
|
+
* Bring a clip under `maxBytes` by re-encoding it smaller, once. Returns the
|
|
31
|
+
* final size. Re-encodes from the WebM itself, so nothing has to keep every
|
|
32
|
+
* frame in memory in case the clip turns out large.
|
|
33
|
+
*/
|
|
34
|
+
export declare function fitSize(ffmpeg: string, file: string, durationMs: number, maxBytes?: number): Promise<number>;
|
|
35
|
+
//# sourceMappingURL=ffmpeg.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg.d.ts","sourceRoot":"","sources":["../src/ffmpeg.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAYH;;;;GAIG;AACH,eAAO,MAAM,cAAc,QAAkB,CAAC;AA0B9C,kEAAkE;AAClE,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CAI1C;AAID,4EAA4E;AAC5E,wBAAgB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAW9C;AAED,MAAM,WAAW,OAAO;IACtB,gEAAgE;IAChE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,6CAA6C;IAC7C,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAuBD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAyB9E;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,SAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB1H"}
|
package/dist/ffmpeg.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encoding with the ffmpeg Playwright already knows how to install.
|
|
3
|
+
*
|
|
4
|
+
* Playwright ships a 2.5 MB ffmpeg for its own video recording, installed the
|
|
5
|
+
* same way as the headless browser, so nobody has to install anything. It is a
|
|
6
|
+
* minimal build (measured, revision 1011): libvpx VP8 encode and decode, mjpeg
|
|
7
|
+
* decode, image2pipe in, WebM out, pad/crop/scale — and no `hstack`, no
|
|
8
|
+
* drawtext, no `-` alias for stdin (it has to be `pipe:0`, as Playwright's own
|
|
9
|
+
* recorder writes it). That is why frames are composited in a canvas and piped
|
|
10
|
+
* in as JPEGs, and why the output is WebM.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { spawn, spawnSync } from 'child_process';
|
|
15
|
+
import { installBrowser, playwrightCacheDir } from './browser.js';
|
|
16
|
+
import { onPath } from './baseline.js';
|
|
17
|
+
const BINARY = process.platform === 'darwin' ? 'ffmpeg-mac'
|
|
18
|
+
: process.platform === 'win32' ? 'ffmpeg-win64.exe'
|
|
19
|
+
: 'ffmpeg-linux';
|
|
20
|
+
/**
|
|
21
|
+
* The largest clip that is sure to upload. GitHub takes 10 MB of video on a
|
|
22
|
+
* Free plan (100 MB on paid ones, but nothing here can tell which), so a clip
|
|
23
|
+
* over this is re-encoded smaller rather than refused at upload.
|
|
24
|
+
*/
|
|
25
|
+
export const MAX_CLIP_BYTES = 8 * 1024 * 1024;
|
|
26
|
+
/** Playwright's ffmpeg, newest revision first, when one is installed. */
|
|
27
|
+
function findCached() {
|
|
28
|
+
const dir = playwrightCacheDir();
|
|
29
|
+
let entries;
|
|
30
|
+
try {
|
|
31
|
+
entries = fs.readdirSync(dir).filter(e => /^ffmpeg-\d+$/.test(e));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
entries.sort((a, b) => Number(b.slice(7)) - Number(a.slice(7)));
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const full = path.join(dir, entry, BINARY);
|
|
39
|
+
if (fs.existsSync(full))
|
|
40
|
+
return full;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/** An ffmpeg on PATH that can encode VP8, the last resort. */
|
|
45
|
+
function findSystem() {
|
|
46
|
+
if (!onPath('ffmpeg'))
|
|
47
|
+
return null;
|
|
48
|
+
const probe = spawnSync('ffmpeg', ['-hide_banner', '-encoders'], { encoding: 'utf-8' });
|
|
49
|
+
return probe.status === 0 && /libvpx/.test(probe.stdout) ? 'ffmpeg' : null;
|
|
50
|
+
}
|
|
51
|
+
/** Where ffmpeg is, without installing anything. For `doctor`. */
|
|
52
|
+
export function findFfmpeg() {
|
|
53
|
+
const override = process.env.PRE_POST_FFMPEG;
|
|
54
|
+
if (override)
|
|
55
|
+
return fs.existsSync(override) ? override : null;
|
|
56
|
+
return findCached() ?? findSystem();
|
|
57
|
+
}
|
|
58
|
+
let ready = null;
|
|
59
|
+
/** ffmpeg, installing Playwright's (one time, ~2 MB) when there is none. */
|
|
60
|
+
export function ensureFfmpeg() {
|
|
61
|
+
ready ??= (async () => {
|
|
62
|
+
const found = findFfmpeg();
|
|
63
|
+
if (found)
|
|
64
|
+
return found;
|
|
65
|
+
if (await installBrowser('ffmpeg')) {
|
|
66
|
+
const installed = findCached();
|
|
67
|
+
if (installed)
|
|
68
|
+
return installed;
|
|
69
|
+
}
|
|
70
|
+
throw new Error('Video needs ffmpeg and it could not be installed. Run: npx playwright-core install ffmpeg');
|
|
71
|
+
})().catch(err => { ready = null; throw err; });
|
|
72
|
+
return ready;
|
|
73
|
+
}
|
|
74
|
+
const VP8 = ['-c:v', 'libvpx', '-qmin', '0', '-qmax', '50', '-deadline', 'realtime', '-cpu-used', '8', '-auto-alt-ref', '0'];
|
|
75
|
+
/**
|
|
76
|
+
* Stream JPEG frames at `fps` into a WebM at `out`.
|
|
77
|
+
*
|
|
78
|
+
* `-crf 10` is what sets the quality; the bitrate only caps it. Measured on a
|
|
79
|
+
* dropdown clip, 1M and 3M came out within 15% of each other and both kept
|
|
80
|
+
* 16px text readable in an 800px pane.
|
|
81
|
+
*/
|
|
82
|
+
/** Run ffmpeg; `closed` resolves when it exits cleanly, rejects with its last error line. */
|
|
83
|
+
function run(ffmpeg, args, stdin) {
|
|
84
|
+
const child = spawn(ffmpeg, ['-loglevel', 'error', '-y', ...args], { stdio: [stdin, 'ignore', 'pipe'] });
|
|
85
|
+
let stderr = '';
|
|
86
|
+
child.stderr.on('data', d => { stderr += d; });
|
|
87
|
+
const closed = new Promise((resolve, reject) => {
|
|
88
|
+
child.on('error', reject);
|
|
89
|
+
child.on('close', code => code === 0 ? resolve() : reject(new Error(`ffmpeg failed (exit ${code}): ${stderr.trim().split('\n').pop() ?? ''}`)));
|
|
90
|
+
});
|
|
91
|
+
return { child, closed };
|
|
92
|
+
}
|
|
93
|
+
export function startEncoder(ffmpeg, out, fps) {
|
|
94
|
+
const { child, closed } = run(ffmpeg, [
|
|
95
|
+
'-f', 'image2pipe', '-avioflags', 'direct', '-fpsprobesize', '0', '-probesize', '32', '-analyzeduration', '0',
|
|
96
|
+
'-framerate', String(fps), '-c:v', 'mjpeg', '-i', 'pipe:0',
|
|
97
|
+
'-an', ...VP8, '-crf', '10', '-b:v', '2M', out,
|
|
98
|
+
], 'pipe');
|
|
99
|
+
const stdin = child.stdin;
|
|
100
|
+
// stdin errors (EPIPE when ffmpeg dies early) surface through `closed`.
|
|
101
|
+
stdin.on('error', () => undefined);
|
|
102
|
+
let exited = false;
|
|
103
|
+
closed.catch(() => undefined).finally(() => { exited = true; });
|
|
104
|
+
return {
|
|
105
|
+
async write(jpeg) {
|
|
106
|
+
// A dead encoder never drains, so waiting on 'drain' alone would hang
|
|
107
|
+
// the run forever; the spike did exactly that.
|
|
108
|
+
if (exited)
|
|
109
|
+
return closed;
|
|
110
|
+
if (!stdin.write(jpeg)) {
|
|
111
|
+
await Promise.race([new Promise(r => stdin.once('drain', r)), closed]);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
async finish() {
|
|
115
|
+
stdin.end();
|
|
116
|
+
await closed;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Bring a clip under `maxBytes` by re-encoding it smaller, once. Returns the
|
|
122
|
+
* final size. Re-encodes from the WebM itself, so nothing has to keep every
|
|
123
|
+
* frame in memory in case the clip turns out large.
|
|
124
|
+
*/
|
|
125
|
+
export async function fitSize(ffmpeg, file, durationMs, maxBytes = MAX_CLIP_BYTES) {
|
|
126
|
+
const size = fs.statSync(file).size;
|
|
127
|
+
if (size <= maxBytes)
|
|
128
|
+
return size;
|
|
129
|
+
const seconds = Math.max(1, durationMs / 1000);
|
|
130
|
+
// 90% of the budget, leaving room for the container.
|
|
131
|
+
const kbps = Math.max(200, Math.floor((maxBytes * 8 * 0.9) / seconds / 1000));
|
|
132
|
+
const tmp = file.replace(/\.webm$/, '.fit.webm');
|
|
133
|
+
// Asynchronous: the other Moments are still being drawn and encoded, and a
|
|
134
|
+
// re-encode takes seconds.
|
|
135
|
+
await run(ffmpeg, [
|
|
136
|
+
'-i', file,
|
|
137
|
+
'-vf', 'scale=trunc(iw*0.75/2)*2:trunc(ih*0.75/2)*2',
|
|
138
|
+
'-an', ...VP8, '-crf', '30', '-b:v', `${kbps}k`, '-maxrate', `${kbps}k`, '-bufsize', `${kbps * 2}k`, tmp,
|
|
139
|
+
], 'ignore').closed.catch(err => { throw new Error(`Could not shrink the clip: ${err.message}`); });
|
|
140
|
+
fs.renameSync(tmp, file);
|
|
141
|
+
return fs.statSync(file).size;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=ffmpeg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg.js","sourceRoot":"","sources":["../src/ffmpeg.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,YAAY;IACzD,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,kBAAkB;QACjD,CAAC,CAAC,cAAc,CAAC;AAErB;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAE9C,yEAAyE;AACzE,SAAS,UAAU;IACjB,MAAM,GAAG,GAAG,kBAAkB,EAAE,CAAC;IACjC,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChE,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QAC3C,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACvC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8DAA8D;AAC9D,SAAS,UAAU;IACjB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,EAAE,CAAC,cAAc,EAAE,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;IACxF,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7E,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,UAAU;IACxB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;IAC7C,IAAI,QAAQ;QAAE,OAAO,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/D,OAAO,UAAU,EAAE,IAAI,UAAU,EAAE,CAAC;AACtC,CAAC;AAED,IAAI,KAAK,GAA2B,IAAI,CAAC;AAEzC,4EAA4E;AAC5E,MAAM,UAAU,YAAY;IAC1B,KAAK,KAAK,CAAC,KAAK,IAAI,EAAE;QACpB,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC;QAC3B,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;QACxB,IAAI,MAAM,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;YACnC,MAAM,SAAS,GAAG,UAAU,EAAE,CAAC;YAC/B,IAAI,SAAS;gBAAE,OAAO,SAAS,CAAC;QAClC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,2FAA2F,CAAC,CAAC;IAC/G,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,OAAO,KAAK,CAAC;AACf,CAAC;AASD,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,GAAG,EAAE,eAAe,EAAE,GAAG,CAAC,CAAC;AAE7H;;;;;;GAMG;AACH,6FAA6F;AAC7F,SAAS,GAAG,CAAC,MAAc,EAAE,IAAc,EAAE,KAAwB;IACnE,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACzG,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,CAAC,MAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACnD,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,uBAAuB,IAAI,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAClJ,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,GAAW,EAAE,GAAW;IACnE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE;QACpC,IAAI,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,kBAAkB,EAAE,GAAG;QAC7G,YAAY,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ;QAC1D,KAAK,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;KAC/C,EAAE,MAAM,CAAC,CAAC;IACX,MAAM,KAAK,GAAG,KAAK,CAAC,KAAM,CAAC;IAC3B,wEAAwE;IACxE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACnC,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAChE,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,IAAI;YACd,sEAAsE;YACtE,+CAA+C;YAC/C,IAAI,MAAM;gBAAE,OAAO,MAAM,CAAC;YAC1B,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvB,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;YAC/E,CAAC;QACH,CAAC;QACD,KAAK,CAAC,MAAM;YACV,KAAK,CAAC,GAAG,EAAE,CAAC;YACZ,MAAM,MAAM,CAAC;QACf,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,MAAc,EAAE,IAAY,EAAE,UAAkB,EAAE,QAAQ,GAAG,cAAc;IACvG,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;IACpC,IAAI,IAAI,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IAClC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC,CAAC;IAC/C,qDAAqD;IACrD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC;IAC9E,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IACjD,2EAA2E;IAC3E,2BAA2B;IAC3B,MAAM,GAAG,CAAC,MAAM,EAAE;QAChB,IAAI,EAAE,IAAI;QACV,KAAK,EAAE,6CAA6C;QACpD,KAAK,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,EAAE,UAAU,EAAE,GAAG,IAAI,GAAG,EAAE,UAAU,EAAE,GAAG,IAAI,GAAG,CAAC,GAAG,EAAE,GAAG;KACzG,EAAE,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,MAAM,IAAI,KAAK,CAAC,8BAA8B,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpG,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACzB,OAAO,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;AAChC,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,4 +22,8 @@ export { runLogin } from './commands/login.js';
|
|
|
22
22
|
export { runPrune } from './commands/prune.js';
|
|
23
23
|
export { runDoctor } from './commands/doctor.js';
|
|
24
24
|
export { runDetect } from './commands/detect.js';
|
|
25
|
+
export { recordMoments } from './video.js';
|
|
26
|
+
export type { RecordMomentsOptions } from './video.js';
|
|
27
|
+
export { parseMoments, loadMomentsFile, MomentError, MAX_MOMENTS } from './moments.js';
|
|
28
|
+
export type { Moment, Step } from './moments.js';
|
|
25
29
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,YAAY,CAAC;AAC3B,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACnH,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAClG,YAAY,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,YAAY,CAAC;AAC3B,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACnH,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAClG,YAAY,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,YAAY,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACvF,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -17,4 +17,6 @@ export { runLogin } from './commands/login.js';
|
|
|
17
17
|
export { runPrune } from './commands/prune.js';
|
|
18
18
|
export { runDoctor } from './commands/doctor.js';
|
|
19
19
|
export { runDetect } from './commands/detect.js';
|
|
20
|
+
export { recordMoments } from './video.js';
|
|
21
|
+
export { parseMoments, loadMomentsFile, MomentError, MAX_MOMENTS } from './moments.js';
|
|
20
22
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,YAAY,CAAC;AAE3B,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACnH,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAElG,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,YAAY,CAAC;AAE3B,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACnH,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAElG,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moments: short, named interactions recorded on Pre and Post and played side
|
|
3
|
+
* by side in one clip.
|
|
4
|
+
*
|
|
5
|
+
* A screenshot cannot show a menu opening or a hover state; a recording of a
|
|
6
|
+
* page sitting still shows nothing either (that is why the first GIF attempt
|
|
7
|
+
* was parked). So the unit of video is an interaction a person would do —
|
|
8
|
+
* "Open the account menu" — written as a few plain steps that target what the
|
|
9
|
+
* person sees, not CSS.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is pure: parsing, validation, choosing which Moments run,
|
|
12
|
+
* and the sentences a reviewer reads. The browser side is `record.ts`.
|
|
13
|
+
*/
|
|
14
|
+
declare const VERBS: readonly ["click", "hover", "type", "press", "scroll", "wait"];
|
|
15
|
+
export type StepVerb = typeof VERBS[number];
|
|
16
|
+
export type Step = {
|
|
17
|
+
verb: 'click' | 'hover';
|
|
18
|
+
target: string;
|
|
19
|
+
} | {
|
|
20
|
+
verb: 'type';
|
|
21
|
+
target: string;
|
|
22
|
+
text: string;
|
|
23
|
+
} | {
|
|
24
|
+
verb: 'press';
|
|
25
|
+
key: string;
|
|
26
|
+
} | {
|
|
27
|
+
verb: 'scroll';
|
|
28
|
+
target?: string;
|
|
29
|
+
px?: number;
|
|
30
|
+
} | {
|
|
31
|
+
verb: 'wait';
|
|
32
|
+
ms: number;
|
|
33
|
+
};
|
|
34
|
+
export interface Moment {
|
|
35
|
+
/** What a reviewer reads above the clip, e.g. "Open the account menu". */
|
|
36
|
+
name: string;
|
|
37
|
+
route: string;
|
|
38
|
+
/** Viewport preset or WxH. Default desktop. */
|
|
39
|
+
viewport: string;
|
|
40
|
+
steps: Step[];
|
|
41
|
+
}
|
|
42
|
+
/** As written in `.pre-post.json` or a `--moments` file. */
|
|
43
|
+
export interface MomentInput {
|
|
44
|
+
name?: unknown;
|
|
45
|
+
route?: unknown;
|
|
46
|
+
viewport?: unknown;
|
|
47
|
+
steps?: unknown;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Moments per run. A big PR gets video for the few changes a reviewer most
|
|
51
|
+
* needs to see move; every changed page still gets screenshots. Order is
|
|
52
|
+
* priority: the first three run, the rest are named in one line.
|
|
53
|
+
*/
|
|
54
|
+
export declare const MAX_MOMENTS = 3;
|
|
55
|
+
/**
|
|
56
|
+
* Past this a clip still ships, with a nudge to split it. Not a cap: length
|
|
57
|
+
* comes only from the steps someone wrote, so nothing records for minutes on
|
|
58
|
+
* its own, and a clip a reviewer can take in on one play is the goal.
|
|
59
|
+
*/
|
|
60
|
+
export declare const LONG_CLIP_MS = 30000;
|
|
61
|
+
export declare class MomentError extends Error {
|
|
62
|
+
}
|
|
63
|
+
/** Validate one Moment. `where` names it in errors, e.g. "Moment 2". */
|
|
64
|
+
export declare function parseMoment(raw: MomentInput, where: string): Moment;
|
|
65
|
+
/** Validate a list of Moments, as found in config or a file. */
|
|
66
|
+
export declare function parseMoments(raw: unknown, source: string): Moment[];
|
|
67
|
+
/** Read a `--moments` file. */
|
|
68
|
+
export declare function loadMomentsFile(file: string, cwd?: string): Moment[];
|
|
69
|
+
/** The Moments that run, in priority order, and the names of the rest. */
|
|
70
|
+
export declare function selectMoments(moments: Moment[]): {
|
|
71
|
+
run: Moment[];
|
|
72
|
+
skipped: string[];
|
|
73
|
+
};
|
|
74
|
+
/** A name as a reviewer reads it: “Open the account menu”. */
|
|
75
|
+
export declare function quote(s: string): string;
|
|
76
|
+
/** A step as a reviewer would say it: `Click “Save”`. Shown under the clip. */
|
|
77
|
+
export declare function describeStep(step: Step): string;
|
|
78
|
+
/** One line for a clip that ran long, or null. */
|
|
79
|
+
export declare function longClipNote(name: string, durationMs: number): string | null;
|
|
80
|
+
/** One line naming the Moments over the limit, or null. */
|
|
81
|
+
export declare function skippedNote(skipped: string[]): string | null;
|
|
82
|
+
export {};
|
|
83
|
+
//# sourceMappingURL=moments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moments.d.ts","sourceRoot":"","sources":["../src/moments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,QAAA,MAAM,KAAK,gEAAiE,CAAC;AAC7E,MAAM,MAAM,QAAQ,GAAG,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC;AAE5C,MAAM,MAAM,IAAI,GACZ;IAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC,MAAM,WAAW,MAAM;IACrB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AAED,4DAA4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B;;;;GAIG;AACH,eAAO,MAAM,YAAY,QAAS,CAAC;AAQnC,qBAAa,WAAY,SAAQ,KAAK;CAAG;AAmDzC,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAkBnE;AAED,gEAAgE;AAChE,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAKnE;AAED,+BAA+B;AAC/B,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAgB,GAAG,MAAM,EAAE,CAe3E;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG;IAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAErF;AAED,8DAA8D;AAC9D,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvC;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAS/C;AAED,kDAAkD;AAClD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG5E;AAED,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAG5D"}
|