@humanjs/recorder 0.1.1 → 0.2.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 +38 -1
- package/dist/index.cjs +73 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -8
- package/dist/index.d.ts +46 -8
- package/dist/index.js +74 -33
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @humanjs/recorder
|
|
2
2
|
|
|
3
|
+
<p>
|
|
4
|
+
<a href="https://www.npmjs.com/package/@humanjs/recorder"><img alt="npm" src="https://img.shields.io/npm/v/@humanjs/recorder"></a>
|
|
5
|
+
<a href="https://www.npmjs.com/package/@humanjs/recorder"><img alt="downloads" src="https://img.shields.io/npm/dt/@humanjs/recorder"></a>
|
|
6
|
+
<a href="https://github.com/totigm/humanjs"><img alt="GitHub" src="https://img.shields.io/badge/GitHub-totigm%2Fhumanjs-181717?logo=github"></a>
|
|
7
|
+
<a href="https://github.com/totigm/humanjs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/totigm/humanjs/actions/workflows/ci.yml/badge.svg"></a>
|
|
8
|
+
<a href="https://github.com/totigm/humanjs/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/npm/l/@humanjs/recorder"></a>
|
|
9
|
+
<a href="https://humanjs.dev"><img alt="docs" src="https://img.shields.io/badge/docs-humanjs.dev-emerald"></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
3
12
|
One-call session recording for [HumanJS](https://humanjs.dev) — turn a humanized Playwright session into an mp4, an animated GIF, a structured JSON timeline, or any combination.
|
|
4
13
|
|
|
5
14
|
## Install
|
|
@@ -75,9 +84,12 @@ await record(
|
|
|
75
84
|
url: 'https://example.com', // optional — navigate before the callback
|
|
76
85
|
personality: 'careful', // any PersonalityConfig
|
|
77
86
|
seed: 'session-42', // deterministic when set
|
|
78
|
-
viewport: { width: 1920, height: 1080 },
|
|
87
|
+
viewport: { width: 1920, height: 1080 }, // ephemeral/persistent only — CDP uses the real window
|
|
79
88
|
headless: false, // defaults false so you can watch the recording
|
|
80
89
|
cursor: true, // auto-install visible cursor overlay (default true)
|
|
90
|
+
userDataDir: './.profile', // persistent profile — stay logged in across runs
|
|
91
|
+
cdpUrl: 'http://localhost:9222', // OR attach to a browser you launched (precedence)
|
|
92
|
+
channel: 'chrome', // launch installed Chrome instead of bundled Chromium
|
|
81
93
|
launch: { args: ['--no-sandbox'] }, // forwarded to chromium.launch()
|
|
82
94
|
context: { locale: 'en-US' }, // forwarded to browser.newContext()
|
|
83
95
|
},
|
|
@@ -93,6 +105,31 @@ await record(
|
|
|
93
105
|
|
|
94
106
|
**No-video mode**: omit `output` entirely (or call `record(fn)` with no options). No screenshot polling, no temp files, no encoding overhead. The structured timeline is still captured.
|
|
95
107
|
|
|
108
|
+
## Recording a logged-in flow
|
|
109
|
+
|
|
110
|
+
By default each `record()` call uses a fresh, signed-out browser. Two ways to record something behind a login:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// Persistent profile — sign in once (in a headed run), reuse it forever
|
|
114
|
+
await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {
|
|
115
|
+
await human.goto('https://app.example.com/dashboard');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Or attach to a browser you already launched (real logins/tabs)
|
|
119
|
+
// chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.humanjs-chrome"
|
|
120
|
+
await record({ output: 'flow.mp4', cdpUrl: 'http://localhost:9222' }, async (human) => {
|
|
121
|
+
await human.goto('https://app.example.com/dashboard');
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- **`userDataDir`** keeps cookies/logins across runs (a dedicated profile, starts empty).
|
|
126
|
+
- **`cdpUrl`** records a browser you launched yourself — its existing session. HumanJS **never closes** a browser it attached to; it only borrows it. Takes precedence over `userDataDir`.
|
|
127
|
+
- **`channel`** (`'chrome'` / `'msedge'`) swaps the binary but, on its own, still uses a fresh profile — pair it with `userDataDir` or `cdpUrl` for real logins.
|
|
128
|
+
|
|
129
|
+
> You can't attach to your everyday Chrome — it only exposes a CDP port when launched with `--remote-debugging-port`, and Chrome refuses that on the default profile. Use a dedicated `--user-data-dir` (you sign in once there), or `userDataDir` to let HumanJS manage one.
|
|
130
|
+
|
|
131
|
+
> **Recording resolution in CDP mode:** the attached browser's real window size wins — `viewport` is intentionally not applied. HumanJS is borrowing a window it doesn't own, and forcing a size would only *emulate* one (letterboxing the capture so the frames don't match what you see). Need a specific resolution? Launch the browser with `--window-size=1920,1080` to size the real window, or use the default/persistent modes where HumanJS owns the window and `viewport` applies directly.
|
|
132
|
+
|
|
96
133
|
## Quality presets
|
|
97
134
|
|
|
98
135
|
Pick the preset that matches the recording's purpose:
|
package/dist/index.cjs
CHANGED
|
@@ -24,50 +24,91 @@ async function record(optionsOrFn, maybeFn) {
|
|
|
24
24
|
viewport,
|
|
25
25
|
headless,
|
|
26
26
|
launch,
|
|
27
|
-
context,
|
|
27
|
+
context: contextOptions,
|
|
28
|
+
userDataDir,
|
|
29
|
+
cdpUrl,
|
|
30
|
+
channel,
|
|
28
31
|
cursor,
|
|
29
32
|
...createHumanOptions
|
|
30
33
|
} = options;
|
|
31
34
|
const resolvedQuality = quality ?? "high";
|
|
32
35
|
const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];
|
|
33
|
-
const resolvedViewport = viewport ??
|
|
36
|
+
const resolvedViewport = viewport ?? contextOptions?.viewport ?? browserPreset.viewport;
|
|
34
37
|
const wantsCapture = output !== void 0;
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
const { page, dispose } = await acquireRecordingContext({
|
|
39
|
+
cdpUrl,
|
|
40
|
+
userDataDir,
|
|
41
|
+
channel,
|
|
42
|
+
headless: headless ?? false,
|
|
43
|
+
viewport: resolvedViewport,
|
|
44
|
+
launch,
|
|
45
|
+
context: contextOptions,
|
|
46
|
+
cursor
|
|
38
47
|
});
|
|
39
48
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
if (url) await page.goto(url);
|
|
50
|
+
const human = await playwright.createHuman(page, createHumanOptions);
|
|
51
|
+
const recording = await human.record(
|
|
52
|
+
{ video: wantsCapture, quality: resolvedQuality },
|
|
53
|
+
() => fn(human, page)
|
|
54
|
+
);
|
|
55
|
+
if (wantsCapture && output) {
|
|
56
|
+
const ext = path.extname(output).toLowerCase();
|
|
57
|
+
if (ext === ".gif") {
|
|
58
|
+
await recording.toGif(output);
|
|
59
|
+
} else {
|
|
60
|
+
await recording.toVideo(output, { quality: resolvedQuality });
|
|
48
61
|
}
|
|
49
|
-
const page = await browserContext.newPage();
|
|
50
|
-
if (url) await page.goto(url);
|
|
51
|
-
const human = await playwright.createHuman(page, createHumanOptions);
|
|
52
|
-
const recording = await human.record(
|
|
53
|
-
{ video: wantsCapture, quality: resolvedQuality },
|
|
54
|
-
() => fn(human, page)
|
|
55
|
-
);
|
|
56
|
-
if (wantsCapture && output) {
|
|
57
|
-
const ext = path.extname(output).toLowerCase();
|
|
58
|
-
if (ext === ".gif") {
|
|
59
|
-
await recording.toGif(output);
|
|
60
|
-
} else {
|
|
61
|
-
await recording.toVideo(output, { quality: resolvedQuality });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return recording;
|
|
65
|
-
} finally {
|
|
66
|
-
await browserContext.close().catch(() => void 0);
|
|
67
62
|
}
|
|
63
|
+
return recording;
|
|
68
64
|
} finally {
|
|
69
|
-
await
|
|
65
|
+
await dispose();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function acquireRecordingContext(opts) {
|
|
69
|
+
const cursorOptions = typeof opts.cursor === "object" ? opts.cursor : void 0;
|
|
70
|
+
const installCursor = async (ctx) => {
|
|
71
|
+
if (opts.cursor !== false) await playwright.installMouseHelper(ctx, cursorOptions);
|
|
72
|
+
};
|
|
73
|
+
if (opts.cdpUrl) {
|
|
74
|
+
const browser2 = await playwright.chromium.connectOverCDP(opts.cdpUrl);
|
|
75
|
+
const context2 = browser2.contexts()[0] ?? await browser2.newContext({ ...opts.context });
|
|
76
|
+
await installCursor(context2);
|
|
77
|
+
const page2 = context2.pages()[0] ?? await context2.newPage();
|
|
78
|
+
return { page: page2, dispose: async () => void 0 };
|
|
70
79
|
}
|
|
80
|
+
if (opts.userDataDir) {
|
|
81
|
+
const context2 = await playwright.chromium.launchPersistentContext(opts.userDataDir, {
|
|
82
|
+
...opts.launch,
|
|
83
|
+
...opts.context,
|
|
84
|
+
channel: opts.channel ?? opts.launch?.channel,
|
|
85
|
+
headless: opts.headless,
|
|
86
|
+
viewport: opts.viewport
|
|
87
|
+
});
|
|
88
|
+
await installCursor(context2);
|
|
89
|
+
const page2 = context2.pages()[0] ?? await context2.newPage();
|
|
90
|
+
return {
|
|
91
|
+
page: page2,
|
|
92
|
+
dispose: async () => {
|
|
93
|
+
await context2.close().catch(() => void 0);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const browser = await playwright.chromium.launch({
|
|
98
|
+
...opts.launch,
|
|
99
|
+
channel: opts.channel ?? opts.launch?.channel,
|
|
100
|
+
headless: opts.headless
|
|
101
|
+
});
|
|
102
|
+
const context = await browser.newContext({ ...opts.context, viewport: opts.viewport });
|
|
103
|
+
await installCursor(context);
|
|
104
|
+
const page = await context.newPage();
|
|
105
|
+
return {
|
|
106
|
+
page,
|
|
107
|
+
dispose: async () => {
|
|
108
|
+
await context.close().catch(() => void 0);
|
|
109
|
+
await browser.close().catch(() => void 0);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
71
112
|
}
|
|
72
113
|
|
|
73
114
|
exports.record = record;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/record/index.ts"],"names":["chromium","installMouseHelper","createHuman","extname"],"mappings":"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","file":"index.cjs","sourcesContent":["import { extname } from 'node:path';\nimport {\n type BrowserContextOptions,\n type CreateHumanOptions,\n chromium,\n createHuman,\n type Human,\n type InstallMouseHelperOptions,\n installMouseHelper,\n type LaunchOptions,\n type Page,\n type Recording,\n type RecordingQuality,\n} from '@humanjs/playwright';\n\ninterface QualityBrowserPreset {\n readonly viewport: { readonly width: number; readonly height: number };\n}\n\nconst QUALITY_BROWSER_PRESETS: Record<RecordingQuality, QualityBrowserPreset> = {\n // 720p source — small files, fast encoding, good for iteration.\n fast: { viewport: { width: 1280, height: 720 } },\n // 1080p source — balanced default for tests and dashboards.\n standard: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, visually-lossless encoding (slow preset + animation tune).\n // Recommended for marketing / portfolio output.\n high: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, archival-quality encoding (very slow, low CRF).\n lossless: { viewport: { width: 1920, height: 1080 } },\n};\n\n/**\n * Options for {@link record}. Most fields are passed straight through to\n * Playwright's `chromium.launch()` and `browser.newContext()` so a one-call\n * recording can configure anything a full Playwright setup could.\n */\nexport interface RecordOptions extends CreateHumanOptions {\n /**\n * Output path. Extension determines format — `.mp4` / `.webm` for video,\n * or `.gif` for an animated GIF (palette-optimized, defaults to 15fps).\n * Omit to skip capture entirely; the returned {@link Recording} still has\n * the structured action timeline via `.toTimeline()` / `.timeline`.\n */\n readonly output?: string;\n /**\n * Quality preset. Picks both source viewport and ffmpeg encoding settings.\n * Defaults to `'high'` (visually-lossless 1080p).\n *\n * - `'fast'`: 720p, CRF 23, preset fast\n * - `'standard'`: 1080p, CRF 20, preset fast\n * - `'high'` (default): 1080p, CRF 18, preset slow, tune animation\n * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation\n */\n readonly quality?: RecordingQuality;\n /** Optional URL to navigate to before the callback runs. */\n readonly url?: string;\n /** Viewport dimensions. Overrides the quality preset's viewport. */\n readonly viewport?: { readonly width: number; readonly height: number };\n /** Run headless. Defaults to `false` so users can watch the recording happen. */\n readonly headless?: boolean;\n /** Forwarded to `chromium.launch()` (alongside `headless`). */\n readonly launch?: LaunchOptions;\n /** Forwarded to `browser.newContext()` (alongside `viewport`). */\n readonly context?: BrowserContextOptions;\n /**\n * Install the HumanJS visible cursor overlay so recorded videos show\n * mouse motion — Playwright's synthetic mouse doesn't render a cursor\n * by itself, so without this the recording would look like text and\n * UI changing on their own.\n *\n * - `true` (default): install with default styling (HumanJS amber, 22px)\n * - `false`: don't install — the user will install their own, or the\n * recording intentionally has no visible cursor\n * - {@link InstallMouseHelperOptions}: install with custom color / size /\n * click-ripple / halo settings\n *\n * The helper is installed on the context via `addInitScript` + a\n * DOMContentLoaded listener, so it persists across `page.setContent()`\n * and navigation inside the callback.\n */\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/** The callback shape both overloads of {@link record} accept. */\nexport type RecordCallback = (human: Human, page: Page) => Promise<void>;\n\n/**\n * One-call session recording. Launches a browser, opens a page, creates a\n * humanized session, runs `fn`, and returns a {@link Recording} you can\n * export to video, JSON timeline, or read in-memory.\n *\n * If `options.output` is set, the output file is written to that path before\n * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)\n * or `toGif` (`.gif`). The returned Recording is still useful for additional\n * exports via `toVideo` / `toGif` / `toTimeline` (all repeatable). If\n * `output` is omitted, frame capture is skipped entirely (no encoding\n * overhead) and only the timeline is captured.\n *\n * @example\n * ```ts\n * // Video + timeline\n * const rec = await record({ output: 'demo.mp4' }, async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * console.log(rec.durationMs, rec.timeline.events.length);\n * ```\n *\n * @example\n * ```ts\n * // Timeline only, no video overhead\n * const rec = await record(async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * ```\n *\n * For multi-page flows or recording a slice of a larger session, use\n * `human.record()` from `@humanjs/playwright` directly.\n */\nexport function record(fn: RecordCallback): Promise<Recording>;\nexport function record(options: RecordOptions, fn: RecordCallback): Promise<Recording>;\nexport async function record(\n optionsOrFn: RecordCallback | RecordOptions,\n maybeFn?: RecordCallback,\n): Promise<Recording> {\n const [options, fn] =\n typeof optionsOrFn === 'function'\n ? [{} as RecordOptions, optionsOrFn]\n : [optionsOrFn, maybeFn as RecordCallback];\n\n const {\n output,\n quality,\n url,\n viewport,\n headless,\n launch,\n context,\n cursor,\n ...createHumanOptions\n } = options;\n\n const resolvedQuality: RecordingQuality = quality ?? 'high';\n const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];\n const resolvedViewport = viewport ?? context?.viewport ?? browserPreset.viewport;\n const wantsCapture = output !== undefined;\n\n const browser = await chromium.launch({\n ...launch,\n headless: headless ?? false,\n });\n try {\n const browserContext = await browser.newContext({\n ...context,\n viewport: resolvedViewport,\n });\n try {\n // Install the visible-cursor overlay before any page is created so\n // the addInitScript runs on the initial page render — and on any\n // page.setContent() inside the callback too.\n if (cursor !== false) {\n const cursorOptions = typeof cursor === 'object' ? cursor : undefined;\n await installMouseHelper(browserContext, cursorOptions);\n }\n\n const page = await browserContext.newPage();\n if (url) await page.goto(url);\n\n const human = await createHuman(page, createHumanOptions);\n\n // Capture runs only when the caller asked for an output file — saves\n // the screenshot + disk-write overhead for timeline-only recordings.\n const recording = await human.record({ video: wantsCapture, quality: resolvedQuality }, () =>\n fn(human, page),\n );\n\n if (wantsCapture && output) {\n // Dispatch by extension: .gif goes through the GIF exporter (which\n // ignores `quality` — GIF doesn't have a CRF concept), everything\n // else flows into the mp4/webm encoder with the chosen preset.\n const ext = extname(output).toLowerCase();\n if (ext === '.gif') {\n await recording.toGif(output);\n } else {\n await recording.toVideo(output, { quality: resolvedQuality });\n }\n }\n\n return recording;\n } finally {\n await browserContext.close().catch(() => undefined);\n }\n } finally {\n await browser.close();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/record/index.ts"],"names":["createHuman","extname","installMouseHelper","browser","chromium","context","page"],"mappings":"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","file":"index.cjs","sourcesContent":["import { extname } from 'node:path';\nimport {\n type BrowserContext,\n type BrowserContextOptions,\n type CreateHumanOptions,\n chromium,\n createHuman,\n type Human,\n type InstallMouseHelperOptions,\n installMouseHelper,\n type LaunchOptions,\n type Page,\n type Recording,\n type RecordingQuality,\n} from '@humanjs/playwright';\n\ninterface QualityBrowserPreset {\n readonly viewport: { readonly width: number; readonly height: number };\n}\n\nconst QUALITY_BROWSER_PRESETS: Record<RecordingQuality, QualityBrowserPreset> = {\n // 720p source — small files, fast encoding, good for iteration.\n fast: { viewport: { width: 1280, height: 720 } },\n // 1080p source — balanced default for tests and dashboards.\n standard: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, visually-lossless encoding (slow preset + animation tune).\n // Recommended for marketing / portfolio output.\n high: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, archival-quality encoding (very slow, low CRF).\n lossless: { viewport: { width: 1920, height: 1080 } },\n};\n\n/**\n * Options for {@link record}. Most fields are passed straight through to\n * Playwright's `chromium.launch()` and `browser.newContext()` so a one-call\n * recording can configure anything a full Playwright setup could.\n */\nexport interface RecordOptions extends CreateHumanOptions {\n /**\n * Output path. Extension determines format — `.mp4` / `.webm` for video,\n * or `.gif` for an animated GIF (palette-optimized, defaults to 15fps).\n * Omit to skip capture entirely; the returned {@link Recording} still has\n * the structured action timeline via `.toTimeline()` / `.timeline`.\n */\n readonly output?: string;\n /**\n * Quality preset. Picks both source viewport and ffmpeg encoding settings.\n * Defaults to `'high'` (visually-lossless 1080p).\n *\n * - `'fast'`: 720p, CRF 23, preset fast\n * - `'standard'`: 1080p, CRF 20, preset fast\n * - `'high'` (default): 1080p, CRF 18, preset slow, tune animation\n * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation\n */\n readonly quality?: RecordingQuality;\n /** Optional URL to navigate to before the callback runs. */\n readonly url?: string;\n /**\n * Viewport dimensions. Overrides the quality preset's viewport. Applies to\n * the default and persistent (`userDataDir`) modes; ignored when attaching\n * over `cdpUrl`, where the real browser window's size wins (see `cdpUrl`).\n */\n readonly viewport?: { readonly width: number; readonly height: number };\n /** Run headless. Defaults to `false` so users can watch the recording happen. */\n readonly headless?: boolean;\n /** Forwarded to `chromium.launch()` (alongside `headless`). */\n readonly launch?: LaunchOptions;\n /** Forwarded to `browser.newContext()` (alongside `viewport`). */\n readonly context?: BrowserContextOptions;\n /**\n * Record in a **persistent profile** at this directory so logins and\n * cookies survive across runs — sign in once (in a headed run), and later\n * recordings start authenticated. Uses `launchPersistentContext` under the\n * hood (a single context); `headless`, `launch`, `channel`, and `viewport`\n * still apply. Starts empty the first time.\n */\n readonly userDataDir?: string;\n /**\n * Record by **attaching to a browser you already launched** over CDP (e.g.\n * `\"http://localhost:9222\"`). Reuses that browser's existing context — your\n * real logins, tabs, extensions. Start the browser yourself with\n * `--remote-debugging-port`. HumanJS never closes a browser it attached to;\n * it only borrows it. Takes precedence over `userDataDir`.\n *\n * `launch` / `headless` / `channel` / `viewport` don't apply here — you're\n * borrowing a window HumanJS doesn't own, so the browser's real window size\n * wins. Forcing a `viewport` would only *emulate* one and letterbox the\n * capture. For a fixed recording resolution, launch the browser yourself\n * with `--window-size=1920,1080`, or use the default / persistent modes.\n */\n readonly cdpUrl?: string;\n /**\n * Playwright browser channel — e.g. `'chrome'`, `'msedge'`. Launches that\n * installed browser's binary instead of bundled Chromium (applies to the\n * default and `userDataDir` modes). NOTE: a channel alone does NOT reuse\n * your existing profile — pair it with `userDataDir` (persistent) or\n * `cdpUrl` (attach) for real logins.\n */\n readonly channel?: string;\n /**\n * Install the HumanJS visible cursor overlay so recorded videos show\n * mouse motion — Playwright's synthetic mouse doesn't render a cursor\n * by itself, so without this the recording would look like text and\n * UI changing on their own.\n *\n * - `true` (default): install with default styling (HumanJS amber, 22px)\n * - `false`: don't install — the user will install their own, or the\n * recording intentionally has no visible cursor\n * - {@link InstallMouseHelperOptions}: install with custom color / size /\n * click-ripple / halo settings\n *\n * The helper is installed on the context via `addInitScript` + a\n * DOMContentLoaded listener, so it persists across `page.setContent()`\n * and navigation inside the callback.\n */\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/** The callback shape both overloads of {@link record} accept. */\nexport type RecordCallback = (human: Human, page: Page) => Promise<void>;\n\n/**\n * One-call session recording. Launches (or attaches to) a browser, opens a\n * page, creates a humanized session, runs `fn`, and returns a\n * {@link Recording} you can export to video, JSON timeline, or read in-memory.\n *\n * Browser source (default → ephemeral fresh profile):\n * - `userDataDir` — a persistent profile that keeps logins across runs.\n * - `cdpUrl` — attach to a browser you launched yourself (real logins/tabs);\n * never closed on finish, only released.\n *\n * If `options.output` is set, the output file is written to that path before\n * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)\n * or `toGif` (`.gif`). The returned Recording is still useful for additional\n * exports via `toVideo` / `toGif` / `toTimeline` (all repeatable). If\n * `output` is omitted, frame capture is skipped entirely (no encoding\n * overhead) and only the timeline is captured.\n *\n * @example\n * ```ts\n * // Video + timeline\n * const rec = await record({ output: 'demo.mp4' }, async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * console.log(rec.durationMs, rec.timeline.events.length);\n * ```\n *\n * @example\n * ```ts\n * // Stay signed in across runs (persistent profile)\n * await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {\n * await human.goto('https://app.example.com/dashboard');\n * });\n * ```\n *\n * For multi-page flows or recording a slice of a larger session, use\n * `human.record()` from `@humanjs/playwright` directly.\n */\nexport function record(fn: RecordCallback): Promise<Recording>;\nexport function record(options: RecordOptions, fn: RecordCallback): Promise<Recording>;\nexport async function record(\n optionsOrFn: RecordCallback | RecordOptions,\n maybeFn?: RecordCallback,\n): Promise<Recording> {\n const [options, fn] =\n typeof optionsOrFn === 'function'\n ? [{} as RecordOptions, optionsOrFn]\n : [optionsOrFn, maybeFn as RecordCallback];\n\n const {\n output,\n quality,\n url,\n viewport,\n headless,\n launch,\n context: contextOptions,\n userDataDir,\n cdpUrl,\n channel,\n cursor,\n ...createHumanOptions\n } = options;\n\n const resolvedQuality: RecordingQuality = quality ?? 'high';\n const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];\n const resolvedViewport = viewport ?? contextOptions?.viewport ?? browserPreset.viewport;\n const wantsCapture = output !== undefined;\n\n const { page, dispose } = await acquireRecordingContext({\n cdpUrl,\n userDataDir,\n channel,\n headless: headless ?? false,\n viewport: resolvedViewport,\n launch,\n context: contextOptions,\n cursor,\n });\n\n try {\n if (url) await page.goto(url);\n\n const human = await createHuman(page, createHumanOptions);\n\n // Capture runs only when the caller asked for an output file — saves\n // the screenshot + disk-write overhead for timeline-only recordings.\n const recording = await human.record({ video: wantsCapture, quality: resolvedQuality }, () =>\n fn(human, page),\n );\n\n if (wantsCapture && output) {\n // Dispatch by extension: .gif goes through the GIF exporter (which\n // ignores `quality` — GIF doesn't have a CRF concept), everything\n // else flows into the mp4/webm encoder with the chosen preset.\n const ext = extname(output).toLowerCase();\n if (ext === '.gif') {\n await recording.toGif(output);\n } else {\n await recording.toVideo(output, { quality: resolvedQuality });\n }\n }\n\n return recording;\n } finally {\n await dispose();\n }\n}\n\n/** Internal options for {@link acquireRecordingContext}. */\ninterface AcquireOptions {\n readonly cdpUrl?: string;\n readonly userDataDir?: string;\n readonly channel?: string;\n readonly headless: boolean;\n readonly viewport: { readonly width: number; readonly height: number };\n readonly launch?: LaunchOptions;\n readonly context?: BrowserContextOptions;\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/**\n * Resolves the browser source — CDP attach > persistent profile > ephemeral\n * — and returns the page to drive plus a `dispose` that tears down only what\n * we created. A CDP-attached browser is borrowed: `dispose` leaves it (and\n * its context) untouched so we never close the caller's real browser.\n */\nasync function acquireRecordingContext(\n opts: AcquireOptions,\n): Promise<{ page: Page; dispose: () => Promise<void> }> {\n const cursorOptions = typeof opts.cursor === 'object' ? opts.cursor : undefined;\n const installCursor = async (ctx: BrowserContext): Promise<void> => {\n if (opts.cursor !== false) await installMouseHelper(ctx, cursorOptions);\n };\n\n // Attach to a browser the caller launched. Reuse its existing context so we\n // record their real session; only make a fresh one if there's none. Never\n // close it on dispose — it's borrowed.\n if (opts.cdpUrl) {\n const browser = await chromium.connectOverCDP(opts.cdpUrl);\n const context = browser.contexts()[0] ?? (await browser.newContext({ ...opts.context }));\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return { page, dispose: async () => undefined };\n }\n\n // Persistent profile — the context owns its browser, so closing it on\n // dispose tears everything down.\n if (opts.userDataDir) {\n const context = await chromium.launchPersistentContext(opts.userDataDir, {\n ...opts.launch,\n ...opts.context,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n viewport: opts.viewport,\n });\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n },\n };\n }\n\n // Ephemeral (default) — a fresh throwaway browser + context.\n const browser = await chromium.launch({\n ...opts.launch,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n });\n const context = await browser.newContext({ ...opts.context, viewport: opts.viewport });\n await installCursor(context);\n const page = await context.newPage();\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n await browser.close().catch(() => undefined);\n },\n };\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -26,7 +26,11 @@ interface RecordOptions extends CreateHumanOptions {
|
|
|
26
26
|
readonly quality?: RecordingQuality;
|
|
27
27
|
/** Optional URL to navigate to before the callback runs. */
|
|
28
28
|
readonly url?: string;
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Viewport dimensions. Overrides the quality preset's viewport. Applies to
|
|
31
|
+
* the default and persistent (`userDataDir`) modes; ignored when attaching
|
|
32
|
+
* over `cdpUrl`, where the real browser window's size wins (see `cdpUrl`).
|
|
33
|
+
*/
|
|
30
34
|
readonly viewport?: {
|
|
31
35
|
readonly width: number;
|
|
32
36
|
readonly height: number;
|
|
@@ -37,6 +41,36 @@ interface RecordOptions extends CreateHumanOptions {
|
|
|
37
41
|
readonly launch?: LaunchOptions;
|
|
38
42
|
/** Forwarded to `browser.newContext()` (alongside `viewport`). */
|
|
39
43
|
readonly context?: BrowserContextOptions;
|
|
44
|
+
/**
|
|
45
|
+
* Record in a **persistent profile** at this directory so logins and
|
|
46
|
+
* cookies survive across runs — sign in once (in a headed run), and later
|
|
47
|
+
* recordings start authenticated. Uses `launchPersistentContext` under the
|
|
48
|
+
* hood (a single context); `headless`, `launch`, `channel`, and `viewport`
|
|
49
|
+
* still apply. Starts empty the first time.
|
|
50
|
+
*/
|
|
51
|
+
readonly userDataDir?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Record by **attaching to a browser you already launched** over CDP (e.g.
|
|
54
|
+
* `"http://localhost:9222"`). Reuses that browser's existing context — your
|
|
55
|
+
* real logins, tabs, extensions. Start the browser yourself with
|
|
56
|
+
* `--remote-debugging-port`. HumanJS never closes a browser it attached to;
|
|
57
|
+
* it only borrows it. Takes precedence over `userDataDir`.
|
|
58
|
+
*
|
|
59
|
+
* `launch` / `headless` / `channel` / `viewport` don't apply here — you're
|
|
60
|
+
* borrowing a window HumanJS doesn't own, so the browser's real window size
|
|
61
|
+
* wins. Forcing a `viewport` would only *emulate* one and letterbox the
|
|
62
|
+
* capture. For a fixed recording resolution, launch the browser yourself
|
|
63
|
+
* with `--window-size=1920,1080`, or use the default / persistent modes.
|
|
64
|
+
*/
|
|
65
|
+
readonly cdpUrl?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Playwright browser channel — e.g. `'chrome'`, `'msedge'`. Launches that
|
|
68
|
+
* installed browser's binary instead of bundled Chromium (applies to the
|
|
69
|
+
* default and `userDataDir` modes). NOTE: a channel alone does NOT reuse
|
|
70
|
+
* your existing profile — pair it with `userDataDir` (persistent) or
|
|
71
|
+
* `cdpUrl` (attach) for real logins.
|
|
72
|
+
*/
|
|
73
|
+
readonly channel?: string;
|
|
40
74
|
/**
|
|
41
75
|
* Install the HumanJS visible cursor overlay so recorded videos show
|
|
42
76
|
* mouse motion — Playwright's synthetic mouse doesn't render a cursor
|
|
@@ -58,9 +92,14 @@ interface RecordOptions extends CreateHumanOptions {
|
|
|
58
92
|
/** The callback shape both overloads of {@link record} accept. */
|
|
59
93
|
type RecordCallback = (human: Human, page: Page) => Promise<void>;
|
|
60
94
|
/**
|
|
61
|
-
* One-call session recording. Launches
|
|
62
|
-
* humanized session, runs `fn`, and returns a
|
|
63
|
-
* export to video, JSON timeline, or read in-memory.
|
|
95
|
+
* One-call session recording. Launches (or attaches to) a browser, opens a
|
|
96
|
+
* page, creates a humanized session, runs `fn`, and returns a
|
|
97
|
+
* {@link Recording} you can export to video, JSON timeline, or read in-memory.
|
|
98
|
+
*
|
|
99
|
+
* Browser source (default → ephemeral fresh profile):
|
|
100
|
+
* - `userDataDir` — a persistent profile that keeps logins across runs.
|
|
101
|
+
* - `cdpUrl` — attach to a browser you launched yourself (real logins/tabs);
|
|
102
|
+
* never closed on finish, only released.
|
|
64
103
|
*
|
|
65
104
|
* If `options.output` is set, the output file is written to that path before
|
|
66
105
|
* `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)
|
|
@@ -81,11 +120,10 @@ type RecordCallback = (human: Human, page: Page) => Promise<void>;
|
|
|
81
120
|
*
|
|
82
121
|
* @example
|
|
83
122
|
* ```ts
|
|
84
|
-
* //
|
|
85
|
-
*
|
|
86
|
-
* await human.
|
|
123
|
+
* // Stay signed in across runs (persistent profile)
|
|
124
|
+
* await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {
|
|
125
|
+
* await human.goto('https://app.example.com/dashboard');
|
|
87
126
|
* });
|
|
88
|
-
* await rec.toTimeline('demo.json');
|
|
89
127
|
* ```
|
|
90
128
|
*
|
|
91
129
|
* For multi-page flows or recording a slice of a larger session, use
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,11 @@ interface RecordOptions extends CreateHumanOptions {
|
|
|
26
26
|
readonly quality?: RecordingQuality;
|
|
27
27
|
/** Optional URL to navigate to before the callback runs. */
|
|
28
28
|
readonly url?: string;
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Viewport dimensions. Overrides the quality preset's viewport. Applies to
|
|
31
|
+
* the default and persistent (`userDataDir`) modes; ignored when attaching
|
|
32
|
+
* over `cdpUrl`, where the real browser window's size wins (see `cdpUrl`).
|
|
33
|
+
*/
|
|
30
34
|
readonly viewport?: {
|
|
31
35
|
readonly width: number;
|
|
32
36
|
readonly height: number;
|
|
@@ -37,6 +41,36 @@ interface RecordOptions extends CreateHumanOptions {
|
|
|
37
41
|
readonly launch?: LaunchOptions;
|
|
38
42
|
/** Forwarded to `browser.newContext()` (alongside `viewport`). */
|
|
39
43
|
readonly context?: BrowserContextOptions;
|
|
44
|
+
/**
|
|
45
|
+
* Record in a **persistent profile** at this directory so logins and
|
|
46
|
+
* cookies survive across runs — sign in once (in a headed run), and later
|
|
47
|
+
* recordings start authenticated. Uses `launchPersistentContext` under the
|
|
48
|
+
* hood (a single context); `headless`, `launch`, `channel`, and `viewport`
|
|
49
|
+
* still apply. Starts empty the first time.
|
|
50
|
+
*/
|
|
51
|
+
readonly userDataDir?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Record by **attaching to a browser you already launched** over CDP (e.g.
|
|
54
|
+
* `"http://localhost:9222"`). Reuses that browser's existing context — your
|
|
55
|
+
* real logins, tabs, extensions. Start the browser yourself with
|
|
56
|
+
* `--remote-debugging-port`. HumanJS never closes a browser it attached to;
|
|
57
|
+
* it only borrows it. Takes precedence over `userDataDir`.
|
|
58
|
+
*
|
|
59
|
+
* `launch` / `headless` / `channel` / `viewport` don't apply here — you're
|
|
60
|
+
* borrowing a window HumanJS doesn't own, so the browser's real window size
|
|
61
|
+
* wins. Forcing a `viewport` would only *emulate* one and letterbox the
|
|
62
|
+
* capture. For a fixed recording resolution, launch the browser yourself
|
|
63
|
+
* with `--window-size=1920,1080`, or use the default / persistent modes.
|
|
64
|
+
*/
|
|
65
|
+
readonly cdpUrl?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Playwright browser channel — e.g. `'chrome'`, `'msedge'`. Launches that
|
|
68
|
+
* installed browser's binary instead of bundled Chromium (applies to the
|
|
69
|
+
* default and `userDataDir` modes). NOTE: a channel alone does NOT reuse
|
|
70
|
+
* your existing profile — pair it with `userDataDir` (persistent) or
|
|
71
|
+
* `cdpUrl` (attach) for real logins.
|
|
72
|
+
*/
|
|
73
|
+
readonly channel?: string;
|
|
40
74
|
/**
|
|
41
75
|
* Install the HumanJS visible cursor overlay so recorded videos show
|
|
42
76
|
* mouse motion — Playwright's synthetic mouse doesn't render a cursor
|
|
@@ -58,9 +92,14 @@ interface RecordOptions extends CreateHumanOptions {
|
|
|
58
92
|
/** The callback shape both overloads of {@link record} accept. */
|
|
59
93
|
type RecordCallback = (human: Human, page: Page) => Promise<void>;
|
|
60
94
|
/**
|
|
61
|
-
* One-call session recording. Launches
|
|
62
|
-
* humanized session, runs `fn`, and returns a
|
|
63
|
-
* export to video, JSON timeline, or read in-memory.
|
|
95
|
+
* One-call session recording. Launches (or attaches to) a browser, opens a
|
|
96
|
+
* page, creates a humanized session, runs `fn`, and returns a
|
|
97
|
+
* {@link Recording} you can export to video, JSON timeline, or read in-memory.
|
|
98
|
+
*
|
|
99
|
+
* Browser source (default → ephemeral fresh profile):
|
|
100
|
+
* - `userDataDir` — a persistent profile that keeps logins across runs.
|
|
101
|
+
* - `cdpUrl` — attach to a browser you launched yourself (real logins/tabs);
|
|
102
|
+
* never closed on finish, only released.
|
|
64
103
|
*
|
|
65
104
|
* If `options.output` is set, the output file is written to that path before
|
|
66
105
|
* `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)
|
|
@@ -81,11 +120,10 @@ type RecordCallback = (human: Human, page: Page) => Promise<void>;
|
|
|
81
120
|
*
|
|
82
121
|
* @example
|
|
83
122
|
* ```ts
|
|
84
|
-
* //
|
|
85
|
-
*
|
|
86
|
-
* await human.
|
|
123
|
+
* // Stay signed in across runs (persistent profile)
|
|
124
|
+
* await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {
|
|
125
|
+
* await human.goto('https://app.example.com/dashboard');
|
|
87
126
|
* });
|
|
88
|
-
* await rec.toTimeline('demo.json');
|
|
89
127
|
* ```
|
|
90
128
|
*
|
|
91
129
|
* For multi-page flows or recording a slice of a larger session, use
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { extname } from 'path';
|
|
2
|
-
import { chromium, installMouseHelper
|
|
2
|
+
import { createHuman, chromium, installMouseHelper } from '@humanjs/playwright';
|
|
3
3
|
|
|
4
4
|
// src/record/index.ts
|
|
5
5
|
var QUALITY_BROWSER_PRESETS = {
|
|
@@ -22,50 +22,91 @@ async function record(optionsOrFn, maybeFn) {
|
|
|
22
22
|
viewport,
|
|
23
23
|
headless,
|
|
24
24
|
launch,
|
|
25
|
-
context,
|
|
25
|
+
context: contextOptions,
|
|
26
|
+
userDataDir,
|
|
27
|
+
cdpUrl,
|
|
28
|
+
channel,
|
|
26
29
|
cursor,
|
|
27
30
|
...createHumanOptions
|
|
28
31
|
} = options;
|
|
29
32
|
const resolvedQuality = quality ?? "high";
|
|
30
33
|
const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];
|
|
31
|
-
const resolvedViewport = viewport ??
|
|
34
|
+
const resolvedViewport = viewport ?? contextOptions?.viewport ?? browserPreset.viewport;
|
|
32
35
|
const wantsCapture = output !== void 0;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
const { page, dispose } = await acquireRecordingContext({
|
|
37
|
+
cdpUrl,
|
|
38
|
+
userDataDir,
|
|
39
|
+
channel,
|
|
40
|
+
headless: headless ?? false,
|
|
41
|
+
viewport: resolvedViewport,
|
|
42
|
+
launch,
|
|
43
|
+
context: contextOptions,
|
|
44
|
+
cursor
|
|
36
45
|
});
|
|
37
46
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
if (url) await page.goto(url);
|
|
48
|
+
const human = await createHuman(page, createHumanOptions);
|
|
49
|
+
const recording = await human.record(
|
|
50
|
+
{ video: wantsCapture, quality: resolvedQuality },
|
|
51
|
+
() => fn(human, page)
|
|
52
|
+
);
|
|
53
|
+
if (wantsCapture && output) {
|
|
54
|
+
const ext = extname(output).toLowerCase();
|
|
55
|
+
if (ext === ".gif") {
|
|
56
|
+
await recording.toGif(output);
|
|
57
|
+
} else {
|
|
58
|
+
await recording.toVideo(output, { quality: resolvedQuality });
|
|
46
59
|
}
|
|
47
|
-
const page = await browserContext.newPage();
|
|
48
|
-
if (url) await page.goto(url);
|
|
49
|
-
const human = await createHuman(page, createHumanOptions);
|
|
50
|
-
const recording = await human.record(
|
|
51
|
-
{ video: wantsCapture, quality: resolvedQuality },
|
|
52
|
-
() => fn(human, page)
|
|
53
|
-
);
|
|
54
|
-
if (wantsCapture && output) {
|
|
55
|
-
const ext = extname(output).toLowerCase();
|
|
56
|
-
if (ext === ".gif") {
|
|
57
|
-
await recording.toGif(output);
|
|
58
|
-
} else {
|
|
59
|
-
await recording.toVideo(output, { quality: resolvedQuality });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return recording;
|
|
63
|
-
} finally {
|
|
64
|
-
await browserContext.close().catch(() => void 0);
|
|
65
60
|
}
|
|
61
|
+
return recording;
|
|
66
62
|
} finally {
|
|
67
|
-
await
|
|
63
|
+
await dispose();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function acquireRecordingContext(opts) {
|
|
67
|
+
const cursorOptions = typeof opts.cursor === "object" ? opts.cursor : void 0;
|
|
68
|
+
const installCursor = async (ctx) => {
|
|
69
|
+
if (opts.cursor !== false) await installMouseHelper(ctx, cursorOptions);
|
|
70
|
+
};
|
|
71
|
+
if (opts.cdpUrl) {
|
|
72
|
+
const browser2 = await chromium.connectOverCDP(opts.cdpUrl);
|
|
73
|
+
const context2 = browser2.contexts()[0] ?? await browser2.newContext({ ...opts.context });
|
|
74
|
+
await installCursor(context2);
|
|
75
|
+
const page2 = context2.pages()[0] ?? await context2.newPage();
|
|
76
|
+
return { page: page2, dispose: async () => void 0 };
|
|
68
77
|
}
|
|
78
|
+
if (opts.userDataDir) {
|
|
79
|
+
const context2 = await chromium.launchPersistentContext(opts.userDataDir, {
|
|
80
|
+
...opts.launch,
|
|
81
|
+
...opts.context,
|
|
82
|
+
channel: opts.channel ?? opts.launch?.channel,
|
|
83
|
+
headless: opts.headless,
|
|
84
|
+
viewport: opts.viewport
|
|
85
|
+
});
|
|
86
|
+
await installCursor(context2);
|
|
87
|
+
const page2 = context2.pages()[0] ?? await context2.newPage();
|
|
88
|
+
return {
|
|
89
|
+
page: page2,
|
|
90
|
+
dispose: async () => {
|
|
91
|
+
await context2.close().catch(() => void 0);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const browser = await chromium.launch({
|
|
96
|
+
...opts.launch,
|
|
97
|
+
channel: opts.channel ?? opts.launch?.channel,
|
|
98
|
+
headless: opts.headless
|
|
99
|
+
});
|
|
100
|
+
const context = await browser.newContext({ ...opts.context, viewport: opts.viewport });
|
|
101
|
+
await installCursor(context);
|
|
102
|
+
const page = await context.newPage();
|
|
103
|
+
return {
|
|
104
|
+
page,
|
|
105
|
+
dispose: async () => {
|
|
106
|
+
await context.close().catch(() => void 0);
|
|
107
|
+
await browser.close().catch(() => void 0);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
69
110
|
}
|
|
70
111
|
|
|
71
112
|
export { record };
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/record/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { extname } from 'node:path';\nimport {\n type BrowserContextOptions,\n type CreateHumanOptions,\n chromium,\n createHuman,\n type Human,\n type InstallMouseHelperOptions,\n installMouseHelper,\n type LaunchOptions,\n type Page,\n type Recording,\n type RecordingQuality,\n} from '@humanjs/playwright';\n\ninterface QualityBrowserPreset {\n readonly viewport: { readonly width: number; readonly height: number };\n}\n\nconst QUALITY_BROWSER_PRESETS: Record<RecordingQuality, QualityBrowserPreset> = {\n // 720p source — small files, fast encoding, good for iteration.\n fast: { viewport: { width: 1280, height: 720 } },\n // 1080p source — balanced default for tests and dashboards.\n standard: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, visually-lossless encoding (slow preset + animation tune).\n // Recommended for marketing / portfolio output.\n high: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, archival-quality encoding (very slow, low CRF).\n lossless: { viewport: { width: 1920, height: 1080 } },\n};\n\n/**\n * Options for {@link record}. Most fields are passed straight through to\n * Playwright's `chromium.launch()` and `browser.newContext()` so a one-call\n * recording can configure anything a full Playwright setup could.\n */\nexport interface RecordOptions extends CreateHumanOptions {\n /**\n * Output path. Extension determines format — `.mp4` / `.webm` for video,\n * or `.gif` for an animated GIF (palette-optimized, defaults to 15fps).\n * Omit to skip capture entirely; the returned {@link Recording} still has\n * the structured action timeline via `.toTimeline()` / `.timeline`.\n */\n readonly output?: string;\n /**\n * Quality preset. Picks both source viewport and ffmpeg encoding settings.\n * Defaults to `'high'` (visually-lossless 1080p).\n *\n * - `'fast'`: 720p, CRF 23, preset fast\n * - `'standard'`: 1080p, CRF 20, preset fast\n * - `'high'` (default): 1080p, CRF 18, preset slow, tune animation\n * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation\n */\n readonly quality?: RecordingQuality;\n /** Optional URL to navigate to before the callback runs. */\n readonly url?: string;\n /** Viewport dimensions. Overrides the quality preset's viewport. */\n readonly viewport?: { readonly width: number; readonly height: number };\n /** Run headless. Defaults to `false` so users can watch the recording happen. */\n readonly headless?: boolean;\n /** Forwarded to `chromium.launch()` (alongside `headless`). */\n readonly launch?: LaunchOptions;\n /** Forwarded to `browser.newContext()` (alongside `viewport`). */\n readonly context?: BrowserContextOptions;\n /**\n * Install the HumanJS visible cursor overlay so recorded videos show\n * mouse motion — Playwright's synthetic mouse doesn't render a cursor\n * by itself, so without this the recording would look like text and\n * UI changing on their own.\n *\n * - `true` (default): install with default styling (HumanJS amber, 22px)\n * - `false`: don't install — the user will install their own, or the\n * recording intentionally has no visible cursor\n * - {@link InstallMouseHelperOptions}: install with custom color / size /\n * click-ripple / halo settings\n *\n * The helper is installed on the context via `addInitScript` + a\n * DOMContentLoaded listener, so it persists across `page.setContent()`\n * and navigation inside the callback.\n */\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/** The callback shape both overloads of {@link record} accept. */\nexport type RecordCallback = (human: Human, page: Page) => Promise<void>;\n\n/**\n * One-call session recording. Launches a browser, opens a page, creates a\n * humanized session, runs `fn`, and returns a {@link Recording} you can\n * export to video, JSON timeline, or read in-memory.\n *\n * If `options.output` is set, the output file is written to that path before\n * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)\n * or `toGif` (`.gif`). The returned Recording is still useful for additional\n * exports via `toVideo` / `toGif` / `toTimeline` (all repeatable). If\n * `output` is omitted, frame capture is skipped entirely (no encoding\n * overhead) and only the timeline is captured.\n *\n * @example\n * ```ts\n * // Video + timeline\n * const rec = await record({ output: 'demo.mp4' }, async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * console.log(rec.durationMs, rec.timeline.events.length);\n * ```\n *\n * @example\n * ```ts\n * // Timeline only, no video overhead\n * const rec = await record(async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * ```\n *\n * For multi-page flows or recording a slice of a larger session, use\n * `human.record()` from `@humanjs/playwright` directly.\n */\nexport function record(fn: RecordCallback): Promise<Recording>;\nexport function record(options: RecordOptions, fn: RecordCallback): Promise<Recording>;\nexport async function record(\n optionsOrFn: RecordCallback | RecordOptions,\n maybeFn?: RecordCallback,\n): Promise<Recording> {\n const [options, fn] =\n typeof optionsOrFn === 'function'\n ? [{} as RecordOptions, optionsOrFn]\n : [optionsOrFn, maybeFn as RecordCallback];\n\n const {\n output,\n quality,\n url,\n viewport,\n headless,\n launch,\n context,\n cursor,\n ...createHumanOptions\n } = options;\n\n const resolvedQuality: RecordingQuality = quality ?? 'high';\n const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];\n const resolvedViewport = viewport ?? context?.viewport ?? browserPreset.viewport;\n const wantsCapture = output !== undefined;\n\n const browser = await chromium.launch({\n ...launch,\n headless: headless ?? false,\n });\n try {\n const browserContext = await browser.newContext({\n ...context,\n viewport: resolvedViewport,\n });\n try {\n // Install the visible-cursor overlay before any page is created so\n // the addInitScript runs on the initial page render — and on any\n // page.setContent() inside the callback too.\n if (cursor !== false) {\n const cursorOptions = typeof cursor === 'object' ? cursor : undefined;\n await installMouseHelper(browserContext, cursorOptions);\n }\n\n const page = await browserContext.newPage();\n if (url) await page.goto(url);\n\n const human = await createHuman(page, createHumanOptions);\n\n // Capture runs only when the caller asked for an output file — saves\n // the screenshot + disk-write overhead for timeline-only recordings.\n const recording = await human.record({ video: wantsCapture, quality: resolvedQuality }, () =>\n fn(human, page),\n );\n\n if (wantsCapture && output) {\n // Dispatch by extension: .gif goes through the GIF exporter (which\n // ignores `quality` — GIF doesn't have a CRF concept), everything\n // else flows into the mp4/webm encoder with the chosen preset.\n const ext = extname(output).toLowerCase();\n if (ext === '.gif') {\n await recording.toGif(output);\n } else {\n await recording.toVideo(output, { quality: resolvedQuality });\n }\n }\n\n return recording;\n } finally {\n await browserContext.close().catch(() => undefined);\n }\n } finally {\n await browser.close();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/record/index.ts"],"names":["browser","context","page"],"mappings":"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","file":"index.js","sourcesContent":["import { extname } from 'node:path';\nimport {\n type BrowserContext,\n type BrowserContextOptions,\n type CreateHumanOptions,\n chromium,\n createHuman,\n type Human,\n type InstallMouseHelperOptions,\n installMouseHelper,\n type LaunchOptions,\n type Page,\n type Recording,\n type RecordingQuality,\n} from '@humanjs/playwright';\n\ninterface QualityBrowserPreset {\n readonly viewport: { readonly width: number; readonly height: number };\n}\n\nconst QUALITY_BROWSER_PRESETS: Record<RecordingQuality, QualityBrowserPreset> = {\n // 720p source — small files, fast encoding, good for iteration.\n fast: { viewport: { width: 1280, height: 720 } },\n // 1080p source — balanced default for tests and dashboards.\n standard: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, visually-lossless encoding (slow preset + animation tune).\n // Recommended for marketing / portfolio output.\n high: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, archival-quality encoding (very slow, low CRF).\n lossless: { viewport: { width: 1920, height: 1080 } },\n};\n\n/**\n * Options for {@link record}. Most fields are passed straight through to\n * Playwright's `chromium.launch()` and `browser.newContext()` so a one-call\n * recording can configure anything a full Playwright setup could.\n */\nexport interface RecordOptions extends CreateHumanOptions {\n /**\n * Output path. Extension determines format — `.mp4` / `.webm` for video,\n * or `.gif` for an animated GIF (palette-optimized, defaults to 15fps).\n * Omit to skip capture entirely; the returned {@link Recording} still has\n * the structured action timeline via `.toTimeline()` / `.timeline`.\n */\n readonly output?: string;\n /**\n * Quality preset. Picks both source viewport and ffmpeg encoding settings.\n * Defaults to `'high'` (visually-lossless 1080p).\n *\n * - `'fast'`: 720p, CRF 23, preset fast\n * - `'standard'`: 1080p, CRF 20, preset fast\n * - `'high'` (default): 1080p, CRF 18, preset slow, tune animation\n * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation\n */\n readonly quality?: RecordingQuality;\n /** Optional URL to navigate to before the callback runs. */\n readonly url?: string;\n /**\n * Viewport dimensions. Overrides the quality preset's viewport. Applies to\n * the default and persistent (`userDataDir`) modes; ignored when attaching\n * over `cdpUrl`, where the real browser window's size wins (see `cdpUrl`).\n */\n readonly viewport?: { readonly width: number; readonly height: number };\n /** Run headless. Defaults to `false` so users can watch the recording happen. */\n readonly headless?: boolean;\n /** Forwarded to `chromium.launch()` (alongside `headless`). */\n readonly launch?: LaunchOptions;\n /** Forwarded to `browser.newContext()` (alongside `viewport`). */\n readonly context?: BrowserContextOptions;\n /**\n * Record in a **persistent profile** at this directory so logins and\n * cookies survive across runs — sign in once (in a headed run), and later\n * recordings start authenticated. Uses `launchPersistentContext` under the\n * hood (a single context); `headless`, `launch`, `channel`, and `viewport`\n * still apply. Starts empty the first time.\n */\n readonly userDataDir?: string;\n /**\n * Record by **attaching to a browser you already launched** over CDP (e.g.\n * `\"http://localhost:9222\"`). Reuses that browser's existing context — your\n * real logins, tabs, extensions. Start the browser yourself with\n * `--remote-debugging-port`. HumanJS never closes a browser it attached to;\n * it only borrows it. Takes precedence over `userDataDir`.\n *\n * `launch` / `headless` / `channel` / `viewport` don't apply here — you're\n * borrowing a window HumanJS doesn't own, so the browser's real window size\n * wins. Forcing a `viewport` would only *emulate* one and letterbox the\n * capture. For a fixed recording resolution, launch the browser yourself\n * with `--window-size=1920,1080`, or use the default / persistent modes.\n */\n readonly cdpUrl?: string;\n /**\n * Playwright browser channel — e.g. `'chrome'`, `'msedge'`. Launches that\n * installed browser's binary instead of bundled Chromium (applies to the\n * default and `userDataDir` modes). NOTE: a channel alone does NOT reuse\n * your existing profile — pair it with `userDataDir` (persistent) or\n * `cdpUrl` (attach) for real logins.\n */\n readonly channel?: string;\n /**\n * Install the HumanJS visible cursor overlay so recorded videos show\n * mouse motion — Playwright's synthetic mouse doesn't render a cursor\n * by itself, so without this the recording would look like text and\n * UI changing on their own.\n *\n * - `true` (default): install with default styling (HumanJS amber, 22px)\n * - `false`: don't install — the user will install their own, or the\n * recording intentionally has no visible cursor\n * - {@link InstallMouseHelperOptions}: install with custom color / size /\n * click-ripple / halo settings\n *\n * The helper is installed on the context via `addInitScript` + a\n * DOMContentLoaded listener, so it persists across `page.setContent()`\n * and navigation inside the callback.\n */\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/** The callback shape both overloads of {@link record} accept. */\nexport type RecordCallback = (human: Human, page: Page) => Promise<void>;\n\n/**\n * One-call session recording. Launches (or attaches to) a browser, opens a\n * page, creates a humanized session, runs `fn`, and returns a\n * {@link Recording} you can export to video, JSON timeline, or read in-memory.\n *\n * Browser source (default → ephemeral fresh profile):\n * - `userDataDir` — a persistent profile that keeps logins across runs.\n * - `cdpUrl` — attach to a browser you launched yourself (real logins/tabs);\n * never closed on finish, only released.\n *\n * If `options.output` is set, the output file is written to that path before\n * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)\n * or `toGif` (`.gif`). The returned Recording is still useful for additional\n * exports via `toVideo` / `toGif` / `toTimeline` (all repeatable). If\n * `output` is omitted, frame capture is skipped entirely (no encoding\n * overhead) and only the timeline is captured.\n *\n * @example\n * ```ts\n * // Video + timeline\n * const rec = await record({ output: 'demo.mp4' }, async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * console.log(rec.durationMs, rec.timeline.events.length);\n * ```\n *\n * @example\n * ```ts\n * // Stay signed in across runs (persistent profile)\n * await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {\n * await human.goto('https://app.example.com/dashboard');\n * });\n * ```\n *\n * For multi-page flows or recording a slice of a larger session, use\n * `human.record()` from `@humanjs/playwright` directly.\n */\nexport function record(fn: RecordCallback): Promise<Recording>;\nexport function record(options: RecordOptions, fn: RecordCallback): Promise<Recording>;\nexport async function record(\n optionsOrFn: RecordCallback | RecordOptions,\n maybeFn?: RecordCallback,\n): Promise<Recording> {\n const [options, fn] =\n typeof optionsOrFn === 'function'\n ? [{} as RecordOptions, optionsOrFn]\n : [optionsOrFn, maybeFn as RecordCallback];\n\n const {\n output,\n quality,\n url,\n viewport,\n headless,\n launch,\n context: contextOptions,\n userDataDir,\n cdpUrl,\n channel,\n cursor,\n ...createHumanOptions\n } = options;\n\n const resolvedQuality: RecordingQuality = quality ?? 'high';\n const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];\n const resolvedViewport = viewport ?? contextOptions?.viewport ?? browserPreset.viewport;\n const wantsCapture = output !== undefined;\n\n const { page, dispose } = await acquireRecordingContext({\n cdpUrl,\n userDataDir,\n channel,\n headless: headless ?? false,\n viewport: resolvedViewport,\n launch,\n context: contextOptions,\n cursor,\n });\n\n try {\n if (url) await page.goto(url);\n\n const human = await createHuman(page, createHumanOptions);\n\n // Capture runs only when the caller asked for an output file — saves\n // the screenshot + disk-write overhead for timeline-only recordings.\n const recording = await human.record({ video: wantsCapture, quality: resolvedQuality }, () =>\n fn(human, page),\n );\n\n if (wantsCapture && output) {\n // Dispatch by extension: .gif goes through the GIF exporter (which\n // ignores `quality` — GIF doesn't have a CRF concept), everything\n // else flows into the mp4/webm encoder with the chosen preset.\n const ext = extname(output).toLowerCase();\n if (ext === '.gif') {\n await recording.toGif(output);\n } else {\n await recording.toVideo(output, { quality: resolvedQuality });\n }\n }\n\n return recording;\n } finally {\n await dispose();\n }\n}\n\n/** Internal options for {@link acquireRecordingContext}. */\ninterface AcquireOptions {\n readonly cdpUrl?: string;\n readonly userDataDir?: string;\n readonly channel?: string;\n readonly headless: boolean;\n readonly viewport: { readonly width: number; readonly height: number };\n readonly launch?: LaunchOptions;\n readonly context?: BrowserContextOptions;\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/**\n * Resolves the browser source — CDP attach > persistent profile > ephemeral\n * — and returns the page to drive plus a `dispose` that tears down only what\n * we created. A CDP-attached browser is borrowed: `dispose` leaves it (and\n * its context) untouched so we never close the caller's real browser.\n */\nasync function acquireRecordingContext(\n opts: AcquireOptions,\n): Promise<{ page: Page; dispose: () => Promise<void> }> {\n const cursorOptions = typeof opts.cursor === 'object' ? opts.cursor : undefined;\n const installCursor = async (ctx: BrowserContext): Promise<void> => {\n if (opts.cursor !== false) await installMouseHelper(ctx, cursorOptions);\n };\n\n // Attach to a browser the caller launched. Reuse its existing context so we\n // record their real session; only make a fresh one if there's none. Never\n // close it on dispose — it's borrowed.\n if (opts.cdpUrl) {\n const browser = await chromium.connectOverCDP(opts.cdpUrl);\n const context = browser.contexts()[0] ?? (await browser.newContext({ ...opts.context }));\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return { page, dispose: async () => undefined };\n }\n\n // Persistent profile — the context owns its browser, so closing it on\n // dispose tears everything down.\n if (opts.userDataDir) {\n const context = await chromium.launchPersistentContext(opts.userDataDir, {\n ...opts.launch,\n ...opts.context,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n viewport: opts.viewport,\n });\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n },\n };\n }\n\n // Ephemeral (default) — a fresh throwaway browser + context.\n const browser = await chromium.launch({\n ...opts.launch,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n });\n const context = await browser.newContext({ ...opts.context, viewport: opts.viewport });\n await installCursor(context);\n const page = await context.newPage();\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n await browser.close().catch(() => undefined);\n },\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanjs/recorder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "One-call session recording for HumanJS — capture a humanized Playwright session as mp4, webm, gif, or a structured JSON timeline.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"humanjs",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"node": ">=20"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@humanjs/playwright": "0.
|
|
52
|
+
"@humanjs/playwright": "0.6.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"playwright": ">=1.40.0"
|