cbrowser 18.70.0 → 18.71.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.
Files changed (67) hide show
  1. package/dist/browser.d.ts +32 -0
  2. package/dist/browser.d.ts.map +1 -1
  3. package/dist/browser.js +46 -2
  4. package/dist/browser.js.map +1 -1
  5. package/dist/cli.js +1465 -24
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cognitive/index.d.ts +10 -0
  8. package/dist/cognitive/index.d.ts.map +1 -1
  9. package/dist/cognitive/index.js +8 -0
  10. package/dist/cognitive/index.js.map +1 -1
  11. package/dist/daemon.d.ts.map +1 -1
  12. package/dist/daemon.js +151 -3
  13. package/dist/daemon.js.map +1 -1
  14. package/dist/mcp-server.d.ts.map +1 -1
  15. package/dist/mcp-server.js +67 -0
  16. package/dist/mcp-server.js.map +1 -1
  17. package/dist/mcp-tools/base/capture-tools.d.ts +189 -0
  18. package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -0
  19. package/dist/mcp-tools/base/capture-tools.js +667 -0
  20. package/dist/mcp-tools/base/capture-tools.js.map +1 -0
  21. package/dist/mcp-tools/base/gif-tools.d.ts +19 -0
  22. package/dist/mcp-tools/base/gif-tools.d.ts.map +1 -1
  23. package/dist/mcp-tools/base/gif-tools.js +45 -27
  24. package/dist/mcp-tools/base/gif-tools.js.map +1 -1
  25. package/dist/mcp-tools/base/index.d.ts +2 -1
  26. package/dist/mcp-tools/base/index.d.ts.map +1 -1
  27. package/dist/mcp-tools/base/index.js +5 -1
  28. package/dist/mcp-tools/base/index.js.map +1 -1
  29. package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
  30. package/dist/mcp-tools/tool-categories.js +11 -0
  31. package/dist/mcp-tools/tool-categories.js.map +1 -1
  32. package/dist/recording/auto-capture.d.ts +72 -0
  33. package/dist/recording/auto-capture.d.ts.map +1 -0
  34. package/dist/recording/auto-capture.js +155 -0
  35. package/dist/recording/auto-capture.js.map +1 -0
  36. package/dist/recording/contact-sheet.d.ts +65 -0
  37. package/dist/recording/contact-sheet.d.ts.map +1 -0
  38. package/dist/recording/contact-sheet.js +204 -0
  39. package/dist/recording/contact-sheet.js.map +1 -0
  40. package/dist/recording/encoders.d.ts +101 -0
  41. package/dist/recording/encoders.d.ts.map +1 -0
  42. package/dist/recording/encoders.js +304 -0
  43. package/dist/recording/encoders.js.map +1 -0
  44. package/dist/recording/engine.d.ts +263 -0
  45. package/dist/recording/engine.d.ts.map +1 -0
  46. package/dist/recording/engine.js +890 -0
  47. package/dist/recording/engine.js.map +1 -0
  48. package/dist/recording/ssim.d.ts +35 -0
  49. package/dist/recording/ssim.d.ts.map +1 -0
  50. package/dist/recording/ssim.js +113 -0
  51. package/dist/recording/ssim.js.map +1 -0
  52. package/dist/recording/timing.d.ts +124 -0
  53. package/dist/recording/timing.d.ts.map +1 -0
  54. package/dist/recording/timing.js +251 -0
  55. package/dist/recording/timing.js.map +1 -0
  56. package/dist/recording/types.d.ts +213 -0
  57. package/dist/recording/types.d.ts.map +1 -0
  58. package/dist/recording/types.js +171 -0
  59. package/dist/recording/types.js.map +1 -0
  60. package/dist/testing/nl-test-suite.d.ts +10 -0
  61. package/dist/testing/nl-test-suite.d.ts.map +1 -1
  62. package/dist/testing/nl-test-suite.js +13 -1
  63. package/dist/testing/nl-test-suite.js.map +1 -1
  64. package/dist/types.d.ts +22 -0
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js.map +1 -1
  67. package/package.json +1 -1
@@ -0,0 +1,65 @@
1
+ /**
2
+ * CBrowser - Cognitive Browser Automation
3
+ * Copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com
4
+ * Learn more at https://cbrowser.ai - MIT License
5
+ */
6
+ /**
7
+ * Default byte budget, tracking the MCP response limit
8
+ * (`MAX_RESPONSE_SIZE` in src/mcp-tools/screenshot-utils.ts) so the two cannot
9
+ * drift apart.
10
+ */
11
+ export declare const MAX_CONTACT_SHEET_BYTES = 150000;
12
+ /** Default number of frames sampled into the sheet. */
13
+ export declare const DEFAULT_MAX_FRAMES = 12;
14
+ /**
15
+ * Longest edge of the assembled sheet before the compression ladder starts.
16
+ * Capping here keeps the ladder cheap: a 12-tile grid of 1280x720 frames would
17
+ * otherwise be re-encoded at 5120x2160 several times before shrinking.
18
+ */
19
+ export declare const MAX_SHEET_EDGE = 1600;
20
+ export interface ContactSheetOptions {
21
+ /** Paths of every frame in the recording, in capture order. */
22
+ framePaths: string[];
23
+ /** Where to write the sheet. A non-JPEG extension is rewritten to `.jpg`. */
24
+ outPath: string;
25
+ /** Grid columns. Defaults to `ceil(sqrt(frames sampled))`. */
26
+ columns?: number;
27
+ /** How many frames to sample. Default {@link DEFAULT_MAX_FRAMES}. */
28
+ maxFrames?: number;
29
+ /** Byte budget for the written file. Default {@link MAX_CONTACT_SHEET_BYTES}. */
30
+ maxBytes?: number;
31
+ /** Burn each frame's index into its tile. Default true. */
32
+ labelIndices?: boolean;
33
+ }
34
+ export interface ContactSheetResult {
35
+ /** Path actually written (may differ from `outPath` by extension). */
36
+ path: string;
37
+ /** Size of the written file in bytes; always <= the requested budget. */
38
+ bytes: number;
39
+ /** Indices into `framePaths` that were sampled, ascending. */
40
+ frameIndices: number[];
41
+ }
42
+ /**
43
+ * Pick `k` evenly spaced indices across `n` items.
44
+ *
45
+ * Always includes the first item, and the last item whenever more than one is
46
+ * requested — the ends of a recording are the two frames a reviewer always
47
+ * wants. Returns `min(n, k)` strictly increasing indices, so asking for more
48
+ * frames than exist yields every frame exactly once.
49
+ *
50
+ * @throws if `n` or `k` is not a positive integer.
51
+ */
52
+ export declare function pickEvenlySpaced(n: number, k: number): number[];
53
+ /**
54
+ * Build a contact sheet from a recording's frames.
55
+ *
56
+ * Samples with {@link pickEvenlySpaced}, tiles into a grid, optionally labels
57
+ * each tile with its frame index, then walks a scale x quality ladder until the
58
+ * encoded sheet fits `maxBytes`.
59
+ *
60
+ * @throws if `framePaths` is empty, if options are out of range, or if the
61
+ * sheet cannot be squeezed under the budget even at the floor of the
62
+ * ladder — the message names the smallest size achieved.
63
+ */
64
+ export declare function buildContactSheet(opts: ContactSheetOptions): Promise<ContactSheetResult>;
65
+ //# sourceMappingURL=contact-sheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contact-sheet.d.ts","sourceRoot":"","sources":["../../src/recording/contact-sheet.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoBH;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,SAAoB,CAAC;AAEzD,uDAAuD;AACvD,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;GAIG;AACH,eAAO,MAAM,cAAc,OAAO,CAAC;AAcnC,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAoB/D;AAgDD;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAkH9F"}
@@ -0,0 +1,204 @@
1
+ /**
2
+ * CBrowser - Cognitive Browser Automation
3
+ * Copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com
4
+ * Learn more at https://cbrowser.ai - MIT License
5
+ */
6
+ /**
7
+ * Contact sheet: a single tiled image summarising a recording.
8
+ *
9
+ * A recording can be hundreds of frames, which no MCP response can carry and no
10
+ * reviewer wants to page through. The contact sheet picks an evenly spaced
11
+ * sample, tiles it into a labelled grid, and compresses the result until it
12
+ * fits the MCP response budget — so one image answers "what happened here?".
13
+ *
14
+ * The byte budget is not advisory: an over-budget sheet is never returned, it
15
+ * throws. Silently handing back something the transport will truncate is worse
16
+ * than failing loudly.
17
+ */
18
+ import { mkdirSync, statSync, writeFileSync } from "node:fs";
19
+ import { dirname } from "node:path";
20
+ import sharp from "sharp";
21
+ import { MAX_RESPONSE_SIZE } from "../mcp-tools/screenshot-utils.js";
22
+ /**
23
+ * Default byte budget, tracking the MCP response limit
24
+ * (`MAX_RESPONSE_SIZE` in src/mcp-tools/screenshot-utils.ts) so the two cannot
25
+ * drift apart.
26
+ */
27
+ export const MAX_CONTACT_SHEET_BYTES = MAX_RESPONSE_SIZE;
28
+ /** Default number of frames sampled into the sheet. */
29
+ export const DEFAULT_MAX_FRAMES = 12;
30
+ /**
31
+ * Longest edge of the assembled sheet before the compression ladder starts.
32
+ * Capping here keeps the ladder cheap: a 12-tile grid of 1280x720 frames would
33
+ * otherwise be re-encoded at 5120x2160 several times before shrinking.
34
+ */
35
+ export const MAX_SHEET_EDGE = 1600;
36
+ /** JPEG quality ladder, mirroring `screenshotCompressed` in src/browser.ts. */
37
+ const QUALITY_STEPS = [85, 70, 55, 45, 35, 25];
38
+ /**
39
+ * Scale ladder. Runs one step lower than the screenshot ladder because a grid
40
+ * packs far more detail per pixel than a single capture.
41
+ */
42
+ const SCALE_STEPS = [1.0, 0.75, 0.5, 0.4, 0.3, 0.2];
43
+ /** Sheet background / letterbox colour. */
44
+ const BACKGROUND = { r: 12, g: 12, b: 14 };
45
+ /**
46
+ * Pick `k` evenly spaced indices across `n` items.
47
+ *
48
+ * Always includes the first item, and the last item whenever more than one is
49
+ * requested — the ends of a recording are the two frames a reviewer always
50
+ * wants. Returns `min(n, k)` strictly increasing indices, so asking for more
51
+ * frames than exist yields every frame exactly once.
52
+ *
53
+ * @throws if `n` or `k` is not a positive integer.
54
+ */
55
+ export function pickEvenlySpaced(n, k) {
56
+ if (!Number.isInteger(n) || n < 1) {
57
+ throw new Error(`pickEvenlySpaced: n must be a positive integer, got ${n}`);
58
+ }
59
+ if (!Number.isInteger(k) || k < 1) {
60
+ throw new Error(`pickEvenlySpaced: k must be a positive integer, got ${k}`);
61
+ }
62
+ const m = Math.min(n, k);
63
+ if (m === 1)
64
+ return [0];
65
+ const step = (n - 1) / (m - 1);
66
+ const picked = new Array(m);
67
+ for (let i = 0; i < m; i++) {
68
+ picked[i] = Math.round(i * step);
69
+ }
70
+ // The ends are pinned rather than derived, so rounding can never move them.
71
+ picked[0] = 0;
72
+ picked[m - 1] = n - 1;
73
+ return picked;
74
+ }
75
+ /**
76
+ * Overlay carrying every tile's index label.
77
+ *
78
+ * One SVG for the whole sheet rather than one per tile: a single composite
79
+ * layer instead of `k` of them. Text is rendered by sharp's SVG support, so no
80
+ * font or canvas dependency is required.
81
+ */
82
+ function buildLabelSvg(sheetWidth, sheetHeight, tiles, tileHeight) {
83
+ // Floored at 14px: below that, JPEG chroma subsampling at the low end of the
84
+ // quality ladder smears the glyphs into the badge and the label stops being
85
+ // readable, which defeats the point of burning it in.
86
+ const fontSize = Math.min(28, Math.max(14, Math.round(tileHeight * 0.14)));
87
+ const padding = Math.max(3, Math.round(fontSize * 0.25));
88
+ const boxHeight = fontSize + padding * 2;
89
+ const parts = tiles.map(({ left, top, label }) => {
90
+ const boxWidth = Math.round(fontSize * 0.7 * label.length) + padding * 2;
91
+ const x = left + padding;
92
+ const y = top + padding;
93
+ return (`<rect x="${x}" y="${y}" width="${boxWidth}" height="${boxHeight}" rx="${padding}" ` +
94
+ `fill="rgba(0,0,0,0.65)"/>` +
95
+ `<text x="${x + padding}" y="${y + fontSize + padding * 0.5}" ` +
96
+ `font-family="DejaVu Sans, Helvetica, Arial, sans-serif" font-size="${fontSize}" ` +
97
+ `fill="#ffffff">${label}</text>`);
98
+ });
99
+ return Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="${sheetWidth}" height="${sheetHeight}">` +
100
+ parts.join("") +
101
+ `</svg>`);
102
+ }
103
+ /** Force a JPEG extension, matching `screenshotCompressed`'s behaviour. */
104
+ function jpegPath(outPath) {
105
+ if (/\.jpe?g$/i.test(outPath))
106
+ return outPath;
107
+ return outPath.replace(/\.[^./\\]+$/, "") + ".jpg";
108
+ }
109
+ /**
110
+ * Build a contact sheet from a recording's frames.
111
+ *
112
+ * Samples with {@link pickEvenlySpaced}, tiles into a grid, optionally labels
113
+ * each tile with its frame index, then walks a scale x quality ladder until the
114
+ * encoded sheet fits `maxBytes`.
115
+ *
116
+ * @throws if `framePaths` is empty, if options are out of range, or if the
117
+ * sheet cannot be squeezed under the budget even at the floor of the
118
+ * ladder — the message names the smallest size achieved.
119
+ */
120
+ export async function buildContactSheet(opts) {
121
+ const { framePaths, outPath, columns, maxFrames = DEFAULT_MAX_FRAMES, maxBytes = MAX_CONTACT_SHEET_BYTES, labelIndices = true, } = opts;
122
+ if (framePaths.length === 0) {
123
+ throw new Error("buildContactSheet: framePaths must not be empty");
124
+ }
125
+ if (!outPath) {
126
+ throw new Error("buildContactSheet: outPath is required");
127
+ }
128
+ if (!Number.isInteger(maxFrames) || maxFrames < 1) {
129
+ throw new Error(`buildContactSheet: maxFrames must be a positive integer, got ${maxFrames}`);
130
+ }
131
+ if (columns !== undefined && (!Number.isInteger(columns) || columns < 1)) {
132
+ throw new Error(`buildContactSheet: columns must be a positive integer, got ${columns}`);
133
+ }
134
+ if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
135
+ throw new Error(`buildContactSheet: maxBytes must be positive, got ${maxBytes}`);
136
+ }
137
+ const frameIndices = pickEvenlySpaced(framePaths.length, maxFrames);
138
+ const selected = frameIndices.map((i) => framePaths[i]);
139
+ const cols = columns ?? Math.ceil(Math.sqrt(selected.length));
140
+ const rows = Math.ceil(selected.length / cols);
141
+ // Every frame of a recording shares the capture geometry, so the first frame
142
+ // sets the tile size for the whole sheet.
143
+ const meta = await sharp(selected[0]).metadata();
144
+ if (!meta.width || !meta.height) {
145
+ throw new Error(`buildContactSheet: could not read dimensions of ${selected[0]}`);
146
+ }
147
+ let baseWidth = meta.width;
148
+ let baseHeight = meta.height;
149
+ const longestEdge = Math.max(cols * baseWidth, rows * baseHeight);
150
+ if (longestEdge > MAX_SHEET_EDGE) {
151
+ const preScale = MAX_SHEET_EDGE / longestEdge;
152
+ baseWidth = Math.max(1, Math.round(baseWidth * preScale));
153
+ baseHeight = Math.max(1, Math.round(baseHeight * preScale));
154
+ }
155
+ const target = jpegPath(outPath);
156
+ mkdirSync(dirname(target), { recursive: true });
157
+ const render = async (scale, quality) => {
158
+ const tileWidth = Math.max(1, Math.round(baseWidth * scale));
159
+ const tileHeight = Math.max(1, Math.round(baseHeight * scale));
160
+ const sheetWidth = tileWidth * cols;
161
+ const sheetHeight = tileHeight * rows;
162
+ const placements = selected.map((_, i) => ({
163
+ left: (i % cols) * tileWidth,
164
+ top: Math.floor(i / cols) * tileHeight,
165
+ }));
166
+ const tiles = await Promise.all(selected.map((path) => sharp(path)
167
+ .resize(tileWidth, tileHeight, { fit: "contain", background: BACKGROUND })
168
+ .toBuffer()));
169
+ const layers = tiles.map((input, i) => ({
170
+ input,
171
+ left: placements[i].left,
172
+ top: placements[i].top,
173
+ }));
174
+ if (labelIndices) {
175
+ layers.push({
176
+ input: buildLabelSvg(sheetWidth, sheetHeight, placements.map((p, i) => ({ ...p, label: String(frameIndices[i]) })), tileHeight),
177
+ left: 0,
178
+ top: 0,
179
+ });
180
+ }
181
+ return sharp({
182
+ create: { width: sheetWidth, height: sheetHeight, channels: 3, background: BACKGROUND },
183
+ })
184
+ .composite(layers)
185
+ .jpeg({ quality, chromaSubsampling: "4:2:0" })
186
+ .toBuffer();
187
+ };
188
+ let smallest = Number.POSITIVE_INFINITY;
189
+ for (const scale of SCALE_STEPS) {
190
+ for (const quality of QUALITY_STEPS) {
191
+ const buffer = await render(scale, quality);
192
+ if (buffer.length < smallest)
193
+ smallest = buffer.length;
194
+ if (buffer.length <= maxBytes) {
195
+ writeFileSync(target, buffer);
196
+ return { path: target, bytes: statSync(target).size, frameIndices };
197
+ }
198
+ }
199
+ }
200
+ throw new Error(`buildContactSheet: cannot fit ${selected.length} frames within ${maxBytes} bytes — ` +
201
+ `smallest achieved ${smallest} bytes at scale ${SCALE_STEPS[SCALE_STEPS.length - 1]} / ` +
202
+ `quality ${QUALITY_STEPS[QUALITY_STEPS.length - 1]}. Lower maxFrames or raise maxBytes.`);
203
+ }
204
+ //# sourceMappingURL=contact-sheet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contact-sheet.js","sourceRoot":"","sources":["../../src/recording/contact-sheet.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAErE;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,iBAAiB,CAAC;AAEzD,uDAAuD;AACvD,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC;AAEnC,+EAA+E;AAC/E,MAAM,aAAa,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;AAE/C;;;GAGG;AACH,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAEpD,2CAA2C;AAC3C,MAAM,UAAU,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC;AA0B3C;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,CAAS,EAAE,CAAS;IACnD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,EAAE,CAAC,CAAC;IAC9E,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACzB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAExB,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,IAAI,KAAK,CAAS,CAAC,CAAC,CAAC;IACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACnC,CAAC;IACD,4EAA4E;IAC5E,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACd,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACtB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,aAAa,CACpB,UAAkB,EAClB,WAAmB,EACnB,KAAqD,EACrD,UAAkB;IAElB,6EAA6E;IAC7E,4EAA4E;IAC5E,sDAAsD;IACtD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IAC3E,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,CAAC,CAAC;IAEzC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE;QAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,OAAO,GAAG,CAAC,CAAC;QACzE,MAAM,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC;QACzB,MAAM,CAAC,GAAG,GAAG,GAAG,OAAO,CAAC;QACxB,OAAO,CACL,YAAY,CAAC,QAAQ,CAAC,YAAY,QAAQ,aAAa,SAAS,SAAS,OAAO,IAAI;YACpF,2BAA2B;YAC3B,YAAY,CAAC,GAAG,OAAO,QAAQ,CAAC,GAAG,QAAQ,GAAG,OAAO,GAAG,GAAG,IAAI;YAC/D,sEAAsE,QAAQ,IAAI;YAClF,kBAAkB,KAAK,SAAS,CACjC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,IAAI,CAChB,kDAAkD,UAAU,aAAa,WAAW,IAAI;QACtF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACd,QAAQ,CACX,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,SAAS,QAAQ,CAAC,OAAe;IAC/B,IAAI,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IAC9C,OAAO,OAAO,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC;AACrD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAyB;IAC/D,MAAM,EACJ,UAAU,EACV,OAAO,EACP,OAAO,EACP,SAAS,GAAG,kBAAkB,EAC9B,QAAQ,GAAG,uBAAuB,EAClC,YAAY,GAAG,IAAI,GACpB,GAAG,IAAI,CAAC;IAET,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IACD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC5D,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,gEAAgE,SAAS,EAAE,CAAC,CAAC;IAC/F,CAAC;IACD,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC,EAAE,CAAC;QACzE,MAAM,IAAI,KAAK,CAAC,8DAA8D,OAAO,EAAE,CAAC,CAAC;IAC3F,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,qDAAqD,QAAQ,EAAE,CAAC,CAAC;IACnF,CAAC;IAED,MAAM,YAAY,GAAG,gBAAgB,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACpE,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAE,CAAC,CAAC;IAEzD,MAAM,IAAI,GAAG,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAE/C,6EAA6E;IAC7E,0CAA0C;IAC1C,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;IAClD,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,mDAAmD,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpF,CAAC;IAED,IAAI,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC;IAC3B,IAAI,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;IAC7B,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,SAAS,EAAE,IAAI,GAAG,UAAU,CAAC,CAAC;IAClE,IAAI,WAAW,GAAG,cAAc,EAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,cAAc,GAAG,WAAW,CAAC;QAC9C,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC;QAC1D,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,QAAQ,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEhD,MAAM,MAAM,GAAG,KAAK,EAAE,KAAa,EAAE,OAAe,EAAmB,EAAE;QACvE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC;QAC7D,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC;QAC/D,MAAM,UAAU,GAAG,SAAS,GAAG,IAAI,CAAC;QACpC,MAAM,WAAW,GAAG,UAAU,GAAG,IAAI,CAAC;QAEtC,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;YACzC,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,SAAS;YAC5B,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,UAAU;SACvC,CAAC,CAAC,CAAC;QAEJ,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAC7B,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CACpB,KAAK,CAAC,IAAI,CAAC;aACR,MAAM,CAAC,SAAS,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;aACzE,QAAQ,EAAE,CACd,CACF,CAAC;QAEF,MAAM,MAAM,GAA2B,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;YAC9D,KAAK;YACL,IAAI,EAAE,UAAU,CAAC,CAAC,CAAE,CAAC,IAAI;YACzB,GAAG,EAAE,UAAU,CAAC,CAAC,CAAE,CAAC,GAAG;SACxB,CAAC,CAAC,CAAC;QAEJ,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK,EAAE,aAAa,CAClB,UAAU,EACV,WAAW,EACX,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EACpE,UAAU,CACX;gBACD,IAAI,EAAE,CAAC;gBACP,GAAG,EAAE,CAAC;aACP,CAAC,CAAC;QACL,CAAC;QAED,OAAO,KAAK,CAAC;YACX,MAAM,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE;SACxF,CAAC;aACC,SAAS,CAAC,MAAM,CAAC;aACjB,IAAI,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC;aAC7C,QAAQ,EAAE,CAAC;IAChB,CAAC,CAAC;IAEF,IAAI,QAAQ,GAAG,MAAM,CAAC,iBAAiB,CAAC;IACxC,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;QAChC,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;YACpC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YAC5C,IAAI,MAAM,CAAC,MAAM,GAAG,QAAQ;gBAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC;YACvD,IAAI,MAAM,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC;gBAC9B,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAC9B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC;YACtE,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CACb,iCAAiC,QAAQ,CAAC,MAAM,kBAAkB,QAAQ,WAAW;QACnF,qBAAqB,QAAQ,mBAAmB,WAAW,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK;QACxF,WAAW,aAAa,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC,sCAAsC,CAC3F,CAAC;AACJ,CAAC"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Recording encoders — turn a directory of captured frames into an animated
3
+ * GIF, an animated WebP, or a video.
4
+ *
5
+ * Two hard constraints shape this module:
6
+ *
7
+ * 1. Frames arrive from a CDP screencast, so they are JPEGs of *differing*
8
+ * byte length and occasionally differing pixel dimensions (a viewport
9
+ * change mid-capture re-sizes the stream). Every frame is therefore
10
+ * normalised to one locked output size before encoding. Do not "optimise"
11
+ * this into raw-buffer stacking — sharp's animated encoders need a joined
12
+ * input, and a stacked raw buffer silently produces a single tall page.
13
+ *
14
+ * 2. Capture is event-driven, so frames are *not* evenly spaced. Per-frame
15
+ * delays come from the caller (derived from frame timestamps), never from
16
+ * a constant cadence.
17
+ *
18
+ * @since v18.70.0
19
+ */
20
+ /** Result of a successful encode. */
21
+ export interface EncodeResult {
22
+ /** Absolute path to the encoded artifact. */
23
+ path: string;
24
+ /** Size of the artifact in bytes. */
25
+ bytes: number;
26
+ /** Number of frames encoded. */
27
+ frames: number;
28
+ /** Locked output width in pixels. */
29
+ width: number;
30
+ /** Locked output height in pixels. */
31
+ height: number;
32
+ }
33
+ export interface EncodeOptions {
34
+ /** Locked output width. Defaults to the first frame's width. */
35
+ width?: number;
36
+ /** Locked output height. Defaults to the first frame's height. */
37
+ height?: number;
38
+ /**
39
+ * Per-frame source crop, parallel to `frames`. Screencast frames are written
40
+ * to disk full-viewport so a capture can be re-cropped later, so region and
41
+ * element captures crop here at encode time rather than at capture time.
42
+ * A crop that falls outside its frame is trimmed to the frame.
43
+ */
44
+ crops?: {
45
+ x: number;
46
+ y: number;
47
+ width: number;
48
+ height: number;
49
+ }[];
50
+ /** Downscale factor applied to the locked size (0 < scale <= 1). */
51
+ scale?: number;
52
+ /** Loop count for animated formats (0 = infinite, the default). */
53
+ loop?: number;
54
+ }
55
+ /**
56
+ * Thrown when an encoder's external dependency is missing. Callers surface the
57
+ * message verbatim and exit non-zero — recording itself has already succeeded
58
+ * at this point, so this must never be swallowed into a silent partial result.
59
+ */
60
+ export declare class MissingEncoderError extends Error {
61
+ constructor(message: string);
62
+ }
63
+ /**
64
+ * Encode frames into an animated GIF with per-frame delays.
65
+ *
66
+ * A single-frame recording (a static page that emitted no screencast events)
67
+ * encodes to a one-page GIF rather than failing.
68
+ */
69
+ export declare function encodeGif(frames: string[], delays: number[], outPath: string, options?: EncodeOptions): Promise<EncodeResult>;
70
+ /** Encode frames into an animated WebP with per-frame delays. */
71
+ export declare function encodeWebp(frames: string[], delays: number[], outPath: string, options?: EncodeOptions): Promise<EncodeResult>;
72
+ /**
73
+ * Locate Playwright's bundled ffmpeg. A bare `ffmpeg` is never invoked: the
74
+ * PATH binary is not guaranteed to exist on deploy targets, and silently
75
+ * picking up an arbitrary system build makes output non-reproducible.
76
+ *
77
+ * Probe order: $PLAYWRIGHT_BROWSERS_PATH, then the per-user cache directories.
78
+ */
79
+ export declare function resolveFfmpegPath(): string | null;
80
+ /**
81
+ * Resample variable-delay frames onto a constant-rate timeline by repeating
82
+ * each frame for as long as it was on screen. Video containers assume a fixed
83
+ * frame rate; repeating is how event-driven capture maps onto one without
84
+ * inventing content that was never displayed.
85
+ */
86
+ export declare function resampleToConstantRate(frames: string[], delays: number[], fps: number): string[];
87
+ /** Encode frames into a WebM/VP8 video. Works with Playwright's bundled ffmpeg. */
88
+ export declare function encodeWebm(frames: string[], delays: number[], outPath: string, options?: EncodeOptions & {
89
+ fps?: number;
90
+ }): Promise<EncodeResult>;
91
+ /**
92
+ * Encode frames into an MP4.
93
+ *
94
+ * Requires an ffmpeg with the mp4 muxer and an H.264-family encoder. Playwright's
95
+ * bundled binary has neither, so this throws {@link MissingEncoderError} unless a
96
+ * fuller ffmpeg is installed.
97
+ */
98
+ export declare function encodeMp4(frames: string[], delays: number[], outPath: string, options?: EncodeOptions & {
99
+ fps?: number;
100
+ }): Promise<EncodeResult>;
101
+ //# sourceMappingURL=encoders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encoders.d.ts","sourceRoot":"","sources":["../../src/recording/encoders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAQH,qCAAqC;AACrC,MAAM,WAAW,YAAY;IAC3B,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,KAAK,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAClE,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAoGD;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EAAE,EAChB,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAUvB;AAED,iEAAiE;AACjE,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EAAE,EAChB,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAUvB;AAMD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAwBjD;AA+CD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAQhG;AAkFD,mFAAmF;AACnF,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EAAE,EAChB,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,aAAa,GAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7C,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EAAE,EAChB,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,aAAa,GAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7C,OAAO,CAAC,YAAY,CAAC,CAEvB"}