{"_id":"playwright-recorder-plus","_rev":"4-e6e7f280711c2e4a8136eea4b3ba1f33","name":"playwright-recorder-plus","dist-tags":{"latest":"0.1.3"},"versions":{"0.1.0":{"name":"playwright-recorder-plus","version":"0.1.0","keywords":["playwright","video","recording","recordVideo","screencast","ffmpeg","mp4","h264","tutorial","demo"],"author":{"url":"https://github.com/MuTsunTsai","name":"Mu-Tsun Tsai","email":"don.m.t.tsai@gmail.com"},"license":"MIT","_id":"playwright-recorder-plus@0.1.0","maintainers":[{"name":"mutsuntsai","email":"don.m.t.tsai@gmail.com"}],"homepage":"https://github.com/MuTsunTsai/playwright-recorder-plus#readme","bugs":{"url":"https://github.com/MuTsunTsai/playwright-recorder-plus/issues"},"dist":{"shasum":"beb7d774f513da5033df81201eeb10a35f1937ea","tarball":"https://registry.npmjs.org/playwright-recorder-plus/-/playwright-recorder-plus-0.1.0.tgz","fileCount":7,"integrity":"sha512-s1LtFYujfFAX50QYPS1WDuHYOhRihqPIWUELckIvrUzEF84cz7sKG6nFx1fCW8wIrkiv9QHcdxWyThVPGDI9YA==","signatures":[{"sig":"MEUCIBdawI7xGt450j1BBfCU3Z24zRBfsBrMIE8+sc6EMGlUAiEAghIfFKfIBN0xnH4t+Psg1UahmCmo7HJmXPgA6+u2Jd8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":67007},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"b29fe29a1918f20674b1bc6d7108d2847ef5c619","scripts":{"dev":"rslib build --watch","lint":"eslint .","test":"playwright test","build":"rslib build","typecheck":"tsc --noEmit"},"_npmUser":{"name":"mutsuntsai","email":"don.m.t.tsai@gmail.com"},"repository":{"url":"git+https://github.com/MuTsunTsai/playwright-recorder-plus.git","type":"git"},"_npmVersion":"11.12.1","description":"A higher-quality, configurable alternative to Playwright's built-in recordVideo. Fixed two-pass pipeline (fast realtime capture + configurable transcode) with pause/resume, crop, multi-page contexts, and inline audio scheduling.","directories":{},"sideEffects":false,"_nodeVersion":"22.15.1","dependencies":{"ffmpeg-static":"^5.3.0"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^10.2.1","playwright":"^1.59.1","typescript":"^6.0.3","@rslib/core":"^0.21.3","@types/node":"^25.6.0","playwright-core":"^1.59.1","@playwright/test":"^1.59.1","@mutsuntsai/eslint":"^1.4.6","@microsoft/api-extractor":"^7.58.7"},"peerDependencies":{"playwright-core":"^1.59.0"},"peerDependenciesMeta":{"playwright-core":{"optional":false}},"_npmOperationalInternal":{"tmp":"tmp/playwright-recorder-plus_0.1.0_1777259234694_0.3581941065997736","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"playwright-recorder-plus","version":"0.1.1","keywords":["playwright","video","recording","recordVideo","screencast","ffmpeg","mp4","h264","tutorial","demo"],"author":{"url":"https://github.com/MuTsunTsai","name":"Mu-Tsun Tsai","email":"don.m.t.tsai@gmail.com"},"license":"MIT","_id":"playwright-recorder-plus@0.1.1","maintainers":[{"name":"mutsuntsai","email":"don.m.t.tsai@gmail.com"}],"homepage":"https://github.com/MuTsunTsai/playwright-recorder-plus#readme","bugs":{"url":"https://github.com/MuTsunTsai/playwright-recorder-plus/issues"},"dist":{"shasum":"f836c5715030b63fa48fb97137bbdbb6d391604e","tarball":"https://registry.npmjs.org/playwright-recorder-plus/-/playwright-recorder-plus-0.1.1.tgz","fileCount":7,"integrity":"sha512-kh6dvQA+kYLOu4TyrTY/NRUGOa24Ky33RVfxtPK3xd4W07hBtObz24DbDZEFxG59r+VUnSwefdaQ7Vmdbs7EDA==","signatures":[{"sig":"MEUCICYeGbIrZ863O1QSTPhg5GwRi1/rsqm1GibJ7pHpsHM7AiEA+UTmwsNWyz+ZQVNg6oktId6THsVY03VKUKII3PAzGiE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":67634},"main":"./dist/index.cjs","type":"module","_from":"file:playwright-recorder-plus-0.1.1.tgz","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"dev":"rslib build --watch","lint":"eslint .","test":"playwright test","build":"rslib build","typecheck":"tsc --noEmit"},"_npmUser":{"name":"mutsuntsai","email":"don.m.t.tsai@gmail.com"},"_resolved":"C:\\Users\\Donald\\AppData\\Local\\Temp\\1e8ac731fb63fb1dcb347241713b22a0\\playwright-recorder-plus-0.1.1.tgz","_integrity":"sha512-kh6dvQA+kYLOu4TyrTY/NRUGOa24Ky33RVfxtPK3xd4W07hBtObz24DbDZEFxG59r+VUnSwefdaQ7Vmdbs7EDA==","repository":{"url":"git+https://github.com/MuTsunTsai/playwright-recorder-plus.git","type":"git"},"_npmVersion":"11.12.1","description":"A higher-quality, configurable alternative to Playwright's built-in recordVideo. Fixed two-pass pipeline (fast realtime capture + configurable transcode) with pause/resume, crop, multi-page contexts, and inline audio scheduling.","directories":{},"sideEffects":false,"_nodeVersion":"22.15.1","dependencies":{"ffmpeg-static":"^5.3.0"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^10.2.1","playwright":"^1.59.1","typescript":"^6.0.3","@rslib/core":"^0.21.3","@types/node":"^25.6.0","playwright-core":"^1.59.1","@playwright/test":"^1.59.1","@mutsuntsai/eslint":"^1.4.6","@microsoft/api-extractor":"^7.58.7"},"peerDependencies":{"playwright-core":"^1.59.0"},"peerDependenciesMeta":{"playwright-core":{"optional":false}},"_npmOperationalInternal":{"tmp":"tmp/playwright-recorder-plus_0.1.1_1777259863643_0.5509379256943423","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"playwright-recorder-plus","version":"0.1.2","keywords":["playwright","video","recording","recordVideo","screencast","ffmpeg","mp4","h264","tutorial","demo"],"author":{"url":"https://github.com/MuTsunTsai","name":"Mu-Tsun Tsai","email":"don.m.t.tsai@gmail.com"},"license":"MIT","_id":"playwright-recorder-plus@0.1.2","maintainers":[{"name":"mutsuntsai","email":"don.m.t.tsai@gmail.com"}],"homepage":"https://github.com/MuTsunTsai/playwright-recorder-plus#readme","bugs":{"url":"https://github.com/MuTsunTsai/playwright-recorder-plus/issues"},"dist":{"shasum":"72628ba8188da5a0b3ad5ae976b0a8198251bd92","tarball":"https://registry.npmjs.org/playwright-recorder-plus/-/playwright-recorder-plus-0.1.2.tgz","fileCount":7,"integrity":"sha512-d2TfJzIOcx3XJj+6vzbCmsjdMY5Am32Amjw8yDPgBg558Y6RystVgoeEeejK16mbzVY+orx6vbBxn4tD/yETJw==","signatures":[{"sig":"MEYCIQDG4fF/1s6lWOS2oTH55PeYnKixLEHnPtA1m11fuKogngIhAMEX7D49K5wofJ1X3xfpGf8rTPWA6pdUcu7Fdr7CmBkc","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":69490},"main":"./dist/index.cjs","type":"module","_from":"file:playwright-recorder-plus-0.1.2.tgz","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"dev":"rslib build --watch","lint":"eslint .","test":"playwright test","build":"rslib build","typecheck":"tsc --noEmit"},"_npmUser":{"name":"mutsuntsai","email":"don.m.t.tsai@gmail.com"},"_resolved":"C:\\Users\\Donald\\AppData\\Local\\Temp\\2b4530bbfcb95b938a12822187f63f45\\playwright-recorder-plus-0.1.2.tgz","_integrity":"sha512-d2TfJzIOcx3XJj+6vzbCmsjdMY5Am32Amjw8yDPgBg558Y6RystVgoeEeejK16mbzVY+orx6vbBxn4tD/yETJw==","repository":{"url":"git+https://github.com/MuTsunTsai/playwright-recorder-plus.git","type":"git"},"_npmVersion":"11.12.1","description":"A higher-quality, configurable alternative to Playwright's built-in recordVideo. Fixed two-pass pipeline (fast realtime capture + configurable transcode) with pause/resume, crop, multi-page contexts, and inline audio scheduling.","directories":{},"sideEffects":false,"_nodeVersion":"22.15.1","dependencies":{"ffmpeg-static":"^5.3.0"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^10.3.0","playwright":"^1.59.1","typescript":"^6.0.3","@rslib/core":"^0.21.3","@types/node":"^25.6.0","playwright-core":"^1.59.1","@playwright/test":"^1.59.1","@mutsuntsai/eslint":"^1.4.6","@microsoft/api-extractor":"^7.58.7"},"peerDependencies":{"playwright-core":"^1.59.0"},"peerDependenciesMeta":{"playwright-core":{"optional":false}},"_npmOperationalInternal":{"tmp":"tmp/playwright-recorder-plus_0.1.2_1777856753234_0.7952552771772836","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"playwright-recorder-plus","version":"0.1.3","description":"A higher-quality, configurable alternative to Playwright's built-in recordVideo. Fixed two-pass pipeline (fast realtime capture + configurable transcode) with pause/resume, crop, multi-page contexts, and inline audio scheduling.","keywords":["playwright","video","recording","recordVideo","screencast","ffmpeg","mp4","h264","tutorial","demo"],"repository":{"type":"git","url":"git+https://github.com/MuTsunTsai/playwright-recorder-plus.git"},"license":"MIT","author":{"name":"Mu-Tsun Tsai","email":"don.m.t.tsai@gmail.com","url":"https://github.com/MuTsunTsai"},"sideEffects":false,"type":"module","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"main":"./dist/index.cjs","types":"./dist/index.d.ts","engines":{"node":">=18"},"dependencies":{"ffmpeg-static":"^5.3.0"},"peerDependencies":{"playwright-core":"^1.59.0"},"peerDependenciesMeta":{"playwright-core":{"optional":false}},"devDependencies":{"@microsoft/api-extractor":"^7.58.7","@mutsuntsai/eslint":"^1.4.6","@playwright/test":"^1.59.1","@rslib/core":"^0.21.3","@types/node":"^25.6.0","eslint":"^10.3.0","playwright":"^1.59.1","playwright-core":"^1.59.1","typescript":"^6.0.3"},"scripts":{"build":"rslib build","dev":"rslib build --watch","lint":"eslint .","test":"playwright test","typecheck":"tsc --noEmit"},"_id":"playwright-recorder-plus@0.1.3","bugs":{"url":"https://github.com/MuTsunTsai/playwright-recorder-plus/issues"},"homepage":"https://github.com/MuTsunTsai/playwright-recorder-plus#readme","_integrity":"sha512-s78vcWbWTiKLGE0omxQKG2uKYRf+wUWU7CU6hAw+Tgt637KGsd60+KAZNILHRILtMdy2B8GqLZaNfOB29cSmVg==","_resolved":"C:\\Users\\Donald\\AppData\\Local\\Temp\\fb225da4490911f61ef270150102c886\\playwright-recorder-plus-0.1.3.tgz","_from":"file:playwright-recorder-plus-0.1.3.tgz","_nodeVersion":"22.15.1","_npmVersion":"11.12.1","dist":{"integrity":"sha512-s78vcWbWTiKLGE0omxQKG2uKYRf+wUWU7CU6hAw+Tgt637KGsd60+KAZNILHRILtMdy2B8GqLZaNfOB29cSmVg==","shasum":"e071f4560a30c55eaa71315495164e2841bab7a0","tarball":"https://registry.npmjs.org/playwright-recorder-plus/-/playwright-recorder-plus-0.1.3.tgz","fileCount":7,"unpackedSize":70438,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIAisJEKcV7KjUrNLXmzn8TV+hMe52f1dmkiMTk5ludvsAiBJQ6S8gEyRW1KwNV8q+mxPpnvbesxpbFDaGOeZJsyBsA=="}]},"_npmUser":{"name":"mutsuntsai","email":"don.m.t.tsai@gmail.com"},"directories":{},"maintainers":[{"name":"mutsuntsai","email":"don.m.t.tsai@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/playwright-recorder-plus_0.1.3_1778373043591_0.8942155799853033"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-27T03:07:14.578Z","modified":"2026-05-10T00:30:43.845Z","0.1.0":"2026-04-27T03:07:14.842Z","0.1.1":"2026-04-27T03:17:43.789Z","0.1.2":"2026-05-04T01:05:53.386Z","0.1.3":"2026-05-10T00:30:43.741Z"},"bugs":{"url":"https://github.com/MuTsunTsai/playwright-recorder-plus/issues"},"author":{"name":"Mu-Tsun Tsai","email":"don.m.t.tsai@gmail.com","url":"https://github.com/MuTsunTsai"},"license":"MIT","homepage":"https://github.com/MuTsunTsai/playwright-recorder-plus#readme","keywords":["playwright","video","recording","recordVideo","screencast","ffmpeg","mp4","h264","tutorial","demo"],"repository":{"type":"git","url":"git+https://github.com/MuTsunTsai/playwright-recorder-plus.git"},"description":"A higher-quality, configurable alternative to Playwright's built-in recordVideo. Fixed two-pass pipeline (fast realtime capture + configurable transcode) with pause/resume, crop, multi-page contexts, and inline audio scheduling.","maintainers":[{"name":"mutsuntsai","email":"don.m.t.tsai@gmail.com"}],"readme":"# playwright-recorder-plus\n\n[![npm version](https://img.shields.io/npm/v/playwright-recorder-plus.svg)](https://www.npmjs.com/package/playwright-recorder-plus)\n[![license](https://img.shields.io/npm/l/playwright-recorder-plus.svg)](https://github.com/MuTsunTsai/playwright-recorder-plus/blob/main/LICENSE)\n[![playwright peer](https://img.shields.io/badge/playwright-%E2%89%A51.59-brightgreen.svg)](https://playwright.dev/)\n\nA higher-quality, configurable alternative to Playwright's built-in `recordVideo`. High quality video, pause/resume, crop, multi-page contexts, and inline audio scheduling.\n\n## Why\n\nPlaywright's `recordVideo` produces visibly compressed VP8 webm with mosquito noise around glyph edges. The ffmpeg arguments are hardcoded to a low-bitrate realtime preset -- fine for CI test artifacts, painful for tutorial recordings, demo videos, and bug reproductions.\n\nThe maintainers have repeatedly declined to expose tuning options ([#8683](https://github.com/microsoft/playwright/issues/8683), [#12056](https://github.com/microsoft/playwright/issues/12056), [#17217](https://github.com/microsoft/playwright/issues/17217), [#31424](https://github.com/microsoft/playwright/issues/31424)) on the grounds that ffmpeg is an internal implementation detail. This package wraps Playwright 1.59+'s public `page.screencast` API, pipes the raw JPEG frames into a separately-shipped ffmpeg, and gives you back full control over the encoder.\n\nWhat you get on top of `recordVideo`:\n\n- **Two-pass pipeline by default** -- first pass is always H.264 `ultrafast` so the encoder cannot fall behind realtime; second pass transcodes to your chosen codec/container and muxes any scheduled audio. You get small, sharp files without sacrificing capture fidelity.\n- **Built-in presets** -- `youtube` (H.264 / mp4) and `web` (VP9 / webm) cover the common targets. Auto-picked from the output extension; override with `preset` or fully customise via `ffmpegArgs`.\n- **Configurable encoder** -- swap to x265, AV1, or any ffmpeg invocation through `ffmpegArgs`.\n- **`pause()` / `resume()`** -- skip recording during long setup or build phases without producing a separate file.\n- **`autoStart: false`** -- defer recording until your page is actually presentable (useful for SPA / WASM warm-up).\n- **`crop`** -- record a sub-region; compatible with `Locator.boundingBox()` for element-level capture.\n- **Multi-page contexts** -- `attachRecorderForContext()` auto-attaches popups and `target=_blank` pages.\n- **Inline audio scheduling** -- `recorder.audio(path, { offset })` schedules clips at wall-clock offsets; ffmpeg muxes them into the second pass.\n\n## Compared to Playwright's built-in `recordVideo`\n\n|                              | Playwright `recordVideo`           | playwright-recorder-plus                                   |\n| ---------------------------- | ---------------------------------- | ---------------------------------------------------------- |\n| Codec                        | ⚠️ VP8 only (hardcoded)            | ✅ H.264 / VP9 / anything you can pass to ffmpeg           |\n| Container                    | ⚠️ webm only                       | ✅ mp4, webm, ogg, mov, ...                                |\n| Bitrate / quality control    | ❌ hardcoded 1 Mbps realtime       | ✅ `preset` (`youtube` / `web`) or full `ffmpegArgs` override |\n| Pause / resume               | ❌                                 | ✅ `pause()` / `resume()`                                  |\n| Defer recording start        | ❌                                 | ✅ `autoStart: false` + `start()`                          |\n| Crop to sub-region / element | ❌                                 | ✅ `crop` option, `Locator.boundingBox()`-compatible       |\n| Audio mux                    | ❌                                 | ✅ `recorder.audio(path, { offset })`                       |\n| Wall-clock-faithful timing   | ⚠️ varies; first-frame anchored    | ✅ `start()`-anchored, with same-slot dedup and tail padding |\n| ffmpeg build                 | ⚠️ libvpx-VP8-only build           | ✅ `ffmpeg-static` (full encoder set)                      |\n\n## Install\n\n```sh\npnpm add -D playwright-recorder-plus\n# or\nnpm install --save-dev playwright-recorder-plus\n```\n\nThe package depends on [`ffmpeg-static`](https://www.npmjs.com/package/ffmpeg-static), which downloads a platform-specific ffmpeg binary on install (~50 MB). No system ffmpeg required.\n\nRequires Node `>= 18` and Playwright `>= 1.59.0`.\n\n## Quick start\n\n```ts\nimport { chromium } from \"playwright\";\nimport { attachRecorder } from \"playwright-recorder-plus\";\n\nconst browser = await chromium.launch();\nconst context = await browser.newContext({ viewport: { width: 1280, height: 720 } });\nconst page = await context.newPage();\n\n// Attach our recorder\nconst recorder = await attachRecorder(page, { path: \"out.mp4\" });\ntry {\n\tawait page.goto(\"https://example.com\");\n\t// ... interactions ...\n} finally {\n\tawait recorder.stop();    // ends recording (returns fast)\n\tawait context.close();    // browser cleanup happens while the second pass runs in the background\n\tawait browser.close();\n\tawait recorder.finalized; // wait for the final file to land\n}\n```\n\nThe output extension picks the second-pass codec automatically: `.mp4` -> H.264, `.webm` -> VP9. Override with `preset: \"youtube\" | \"web\"` or supply your own `ffmpegArgs` for the second pass.\n\n## How the two-pass pipeline works\n\n```\n              attachRecorder()\n                      |\n                      v\n    page.screencast.start({ onFrame })\n                      |\n                      v\n+--------------------------------------------+\n|  Pass 1 (always running, can't be tuned)   |\n|  - libx264 -preset ultrafast -crf 18       |\n|  - writes <path>.intermediate.mp4          |\n|  - cannot fall behind realtime             |\n+--------------------------------------------+\n                      |\n             recorder.stop()  <-- \"recording stopped\" verb\n                      |        returns once pass 1 has flushed\n                      v\n+--------------------------------------------+\n|  Pass 2 (background, awaits via finalized) |\n|  - chosen by `preset` / `ffmpegArgs`       |\n|  - mux scheduled audio() clips             |\n|  - on success: deletes intermediate        |\n|  - on failure: keeps intermediate so the   |\n|    capture isn't lost                      |\n+--------------------------------------------+\n                      |\n                      v\n           await recorder.finalized\n                      |\n                      v\n              final file on disk\n```\n\n> Why a fixed first pass? Encoding speed must stay above realtime. If ffmpeg falls behind, stdin backpressure stalls the Node-side `onFrame` callback, which delays the wall-clock anchored timeline that ingestion uses to assign frame numbers. The result is a video shorter than reality. Pinning the first pass to H.264 ultrafast removes this whole class of bug and makes capture fidelity independent of how slow your final-format encode is.\n\n## API\n\n### `attachRecorder(page, options)`\n\nAttach a recorder to a single `Page`. Returns a `Recorder` controller.\n\n```ts\ninterface RecorderOptions {\n\t// Final output file. Container is determined by extension.\n\tpath: string;\n\n\t// Default: true. When false, the recorder waits until you call `recorder.start()`.\n\tautoStart?: boolean;\n\n\t// Frame size. Default: page.viewportSize().\n\tsize?: { width: number; height: number };\n\n\t// Constant output frame rate. Default: 25.\n\tfps?: number;\n\n\t// JPEG quality of the screencast frames before re-encoding. Default: 100.\n\tjpegQuality?: number;\n\n\t// Crop the recording to a sub-region. Compatible with Locator.boundingBox().\n\tcrop?: { x: number; y: number; width: number; height: number };\n\n\t// Built-in second-pass preset. Auto-detected from the path extension\n\t// when omitted: .mp4 -> \"youtube\", .webm/.ogg/.ogv -> \"web\".\n\t// Mutually exclusive with `ffmpegArgs`.\n\tpreset?: \"youtube\" | \"web\";\n\n\t// Replace the second-pass argv tail (codec / filter / container flags).\n\t// First-pass args are NOT user-configurable -- see \"How the two-pass\n\t// pipeline works\" above. Mutually exclusive with `preset`.\n\tffmpegArgs?: string[];\n\n\t// Where the H.264 ultrafast intermediate file is written.\n\t// Default: `<path-without-ext>.intermediate.mp4` next to `path`.\n\t// Kept on disk if the second pass fails.\n\tintermediatePath?: string;\n\n\t// Override the ffmpeg binary path. Default: the one shipped by ffmpeg-static.\n\tffmpegPath?: string;\n\n\t// Suppress runtime warnings about misuse (e.g. pause() before start()). Default: false.\n\tsilenceWarnings?: boolean;\n}\n\ninterface Recorder {\n\treadonly frameCount: number;\n\n\t// Resolves once the second pass + audio mux is complete and the final\n\t// file is on disk. Rejects on second-pass failure (the intermediate\n\t// is preserved at `options.intermediatePath` so the capture isn't lost).\n\treadonly finalized: Promise<StopResult>;\n\n\tstart(): Promise<void>;\n\tpause(): Promise<void>;\n\tresume(): Promise<void>;\n\t// Ends recording. Returns once the first-pass capture has flushed.\n\t// Does NOT wait for the second pass -- await `finalized` for that.\n\tstop(): Promise<StopResult>;\n\n\t// Schedule an audio file to be muxed at a chosen point on the video\n\t// timeline. With no `offset`, the clip is anchored to \"now\" (wall-clock\n\t// elapsed since `start()`). With `offset` only, the clip plays\n\t// `offset` seconds after \"now\". With `absolute: true`, `offset` is an\n\t// absolute video timestamp from t=0.\n\taudio(path: string, options?: { offset?: number; absolute?: boolean }): void;\n}\n```\n\n## Lifecycle\n\nBy default (`autoStart: true`) recording begins as soon as `attachRecorder()` resolves -- the simplest usage is `try { ... } finally { await rec.stop() }`. Pass `autoStart: false` to defer until you call `rec.start()` yourself.\n\n`pause()` and `resume()` are inverses; calling either at the wrong time is a no-op (with a `console.warn` you can suppress via `silenceWarnings`). `stop()` is a control verb -- it ends recording and returns once the first pass has flushed; the second pass runs in the background. Await `recorder.finalized` to block until the final file is on disk.\n\nCalls that don't make sense -- `pause()` before `start()`, anything after `stop()` -- are no-ops with a warning, so a defensive `await rec.stop()` in a `finally` block is always safe.\n\nIf `autoStart: false` was set and `start()` was never called, `stop()` produces no file at all (the result has `written: false`). This is intentional and supports conditional recording: always call `rec.stop()` in `finally`, and only call `rec.start()` when you actually want a recording.\n\n### `attachRecorderForContext(context, options)`\n\nAttach recorders to **every** `Page` in a `BrowserContext`, including pages opened later (popups, `target=_blank`).\n\n```ts\nimport { attachRecorderForContext } from \"playwright-recorder-plus\";\n\nconst recorders = await attachRecorderForContext(context, {\n\tpathTemplate: \"videos/page-{index}.mp4\",\n});\ntry {\n\tawait page.goto(\"https://opens-popup.example\");\n\t// popup pages are auto-attached as they appear\n} finally {\n\tconst results = await recorders.stopAll();\n\tconsole.log(`recorded ${results.length} pages`);\n}\n```\n\n`{index}` in `pathTemplate` is replaced with a 0-based per-page counter. All other options pass through unchanged.\n\n`stopAll()` waits for every recorder's `stop()` to flush. To wait for all final files to land on disk, additionally `await Promise.all(recorders.recorders.map(r => r.finalized))`.\n\n### Pause / resume\n\nSkip recording during long setup or build steps without producing a separate file:\n\n```ts\nconst rec = await attachRecorder(page, { path: \"tutorial.mp4\" });\nawait page.click(\"#start\");\nawait rec.pause();\n// 60 seconds of build output omitted from final video\nawait waitForBuildToComplete();\nawait rec.resume();\nawait page.click(\"#finish\");\nawait rec.stop();\nawait rec.finalized;\n```\n\nThe output appears as one continuous video with the paused interval simply absent.\n\n### Manual start (skip startup)\n\nFor tutorial recordings where the page takes time to become \"presentable\" (heavy SPA bundle, Pyodide / WASM load, etc.), use `autoStart: false`:\n\n```ts\nconst rec = await attachRecorder(page, { path: \"demo.mp4\", autoStart: false });\nawait page.goto(url);\nawait page.waitForLoadState(\"networkidle\");\nawait waitForAppReady(page);\nawait rec.start();           // recording begins here\nawait runDemoSteps(page);\nawait rec.stop();\nawait rec.finalized;\n```\n\n## Cropping\n\n```ts\nconst rec = await attachRecorder(page, {\n\tpath: \"panel.mp4\",\n\tcrop: { x: 100, y: 50, width: 800, height: 600 },\n});\n```\n\nCoordinates are in viewport CSS pixels. The shape matches what `Locator.boundingBox()` returns, so cropping to a specific element is one line:\n\n```ts\nconst box = await page.locator(\"#main-panel\").boundingBox();\nif (box) {\n\tconst rec = await attachRecorder(page, { path: \"panel.mp4\", crop: box });\n\t// ...\n}\n```\n\n## Audio mux\n\nThe recorder does **not** capture audio that the page itself plays -- see [Page audio is not captured](#page-audio-is-not-captured) below. Instead, you schedule audio inline as you drive the page; the second pass muxes it in:\n\n```ts\nconst rec = await attachRecorder(page, { path: \"tutorial.mp4\" });\n\nawait page.click(\"#step-1\");\nrec.audio(\"step-1-narration.mp3\");   // plays from this exact moment\n\nawait page.waitForSelector(\".step-1-done\");\nawait page.click(\"#step-2\");\nrec.audio(\"step-2-narration.mp3\");\n\n// Schedule a click sound 0.8 s in the future (e.g. for an animation\n// that hasn't fired yet but you know its timing):\nrec.audio(\"click.wav\", { offset: 0.8 });\n\n// Schedule at an absolute video timestamp (escape hatch):\nrec.audio(\"intro.mp3\", { offset: 0, absolute: true });\n\nawait rec.stop();\nawait rec.finalized;\n```\n\n`audio()` resolves the timeline position from `recorder.start()` using wall-clock elapsed (`performance.now() - startTime - pausedAccum`). It stays accurate across `pause()` / `resume()` -- paused intervals don't advance the clock. Calling `audio()` with no `offset` while paused logs a warning, since the resolved position is the moment you paused (rarely intended).\n\nMultiple `audio()` calls accumulate; ffmpeg combines them with `amix` during the second pass. If two scheduled tracks overlap on the timeline, ffmpeg averages them -- effectively ducking each. Choose call timings that don't overlap if you want each clip at full volume.\n\nIf `stop()` is called without any recording having taken place (autoStart off, never called `start()`), the scheduled audio is discarded along with the empty capture.\n\n## Presets and custom encoder\n\nThe second pass picks defaults from the output extension:\n\n| Output extension       | Default preset | Codec / container |\n| ---------------------- | -------------- | ----------------- |\n| `.mp4` (or anything else not below) | `youtube` | H.264 main profile, AAC, +faststart |\n| `.webm`, `.ogg`, `.ogv`             | `web`     | VP9 CRF 30, Opus  |\n\nOverride the choice explicitly:\n\n```ts\nawait attachRecorder(page, { path: \"out.webm\", preset: \"web\" });\n```\n\nFor full control, supply `ffmpegArgs` for the second pass. The argv tail goes between `-i <intermediate>` and the output path -- do not include `-i`, `-y`, or the output path; the recorder adds those:\n\n```ts\n// AV1 via libsvtav1\nawait attachRecorder(page, {\n\tpath: \"out.mp4\",\n\tffmpegArgs: [\n\t\t\"-c:v\", \"libsvtav1\",\n\t\t\"-preset\", \"8\",\n\t\t\"-crf\", \"30\",\n\t\t\"-pix_fmt\", \"yuv420p\",\n\t\t\"-movflags\", \"+faststart\",\n\t],\n});\n```\n\n`preset` and `ffmpegArgs` are mutually exclusive; passing both throws.\n\nThe first pass is fixed at H.264 `ultrafast` and is not user-configurable, so capture fidelity is independent of whatever the second pass costs.\n\n## Limitations\n\n### Page audio is not captured\n\nLike Playwright's built-in `recordVideo`, this package captures video frames only. CDP has no audio stream to attach to. If your page plays audio you want in the final file, you have two options:\n\n1. **Schedule audio externally** -- drive your own audio files (TTS narration, voice-over, sound effects) via `recorder.audio(path)`. This is the typical tutorial-recording workflow and is what `recorder.audio()` is designed for.\n2. **Capture page audio yourself** -- inject `getDisplayMedia({ audio: true })` plus `MediaRecorder` into the page, ship the encoded chunks back to Node via `page.exposeBinding`, and pass the resulting webm/mp3 to `recorder.audio()`. Out of scope for this package; see Chrome's [tabCapture](https://developer.chrome.com/docs/extensions/reference/api/tabCapture) docs for the underlying mechanism.\n\n### Attach order matters\n\n`attachRecorder` calls `page.screencast.start()` internally. Playwright's screencast server **locks the frame size to the first client's request**. If `context.tracing.start({ screenshots: true })` (or any other screencast consumer) starts before the recorder, the recorder gets a downscaled 800×450 stream regardless of what `size` it asked for.\n\nThe recorder verifies dimensions on the first frame and **throws** if the server delivered something other than the requested size. If you see this error, move `attachRecorder` ahead of any `tracing.start` call.\n\n### Frame rate\n\nCDP screencast is variable rate -- idle pages don't emit frames. The recorder converts this to a constant-rate stream: each incoming frame is assigned `frameNumber = floor((nowMs - startMs) * fps / 1000)`, gaps are filled with copies of the previous frame, and `stop()` pads up to wall-clock now so the file covers the full recording duration even if the last few seconds were idle.\n\nIf the page is still visually static when `start()` is called (typical for SPAs warming up Pyodide / WASM), CDP may not deliver its first frame for several seconds. The recorder back-fills those leading slots with the first frame it eventually receives -- so the encoded video starts at wall-clock t = 0, not t = first-frame-arrival.\n\n### Encoder must keep up with realtime\n\nThe first pass is always H.264 `ultrafast` for exactly this reason -- it runs comfortably above realtime on modern CPUs. The second pass can be slower without skewing the timeline because it reads from a finished file rather than a live frame pipe.\n\nIf you replace the second pass via `ffmpegArgs` with something extreme (libsvtav1 at preset 0, libaom-av1 at cpu-used 0, etc.), `stop()` still returns fast but `await recorder.finalized` may take a long time. That's the expected trade-off and is not a fidelity issue.\n\n### Cross-browser\n\nTested against Chromium, Firefox, and WebKit. `page.screencast` is implemented uniformly across all three since Playwright 1.59, and the smoke tests in `test/smoke.spec.ts` exercise each browser in CI.\n\n### License of the bundled ffmpeg\n\n`ffmpeg-static` ships a GPL ffmpeg build. Calling it via `child_process` (which this package does) does not propagate the GPL into your project. See [`ffmpeg-static`](https://github.com/eugeneware/ffmpeg-static#license) for the full discussion.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}