rollberry 0.1.5

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/CHANGELOG.md ADDED
@@ -0,0 +1,59 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on Keep a Changelog and the project stays on the `v0.x.x`
6
+ line until the CLI surface and capture behavior settle.
7
+
8
+ ## [0.1.5] - 2026-03-18
9
+
10
+ ### Changed
11
+
12
+ - Reduced default auto-scroll speed from 1800 px/s to 800 px/s for better
13
+ readability during capture.
14
+ - Republished under new maintainer account.
15
+
16
+ ## [0.1.3] - 2026-03-15
17
+
18
+ ### Changed
19
+
20
+ - README now includes a dedicated `Using With npx` section with version-pinned
21
+ examples and common usage patterns.
22
+
23
+ ## [0.1.2] - 2026-03-15
24
+
25
+ ### Added
26
+
27
+ - `SECURITY.md`, `CONTRIBUTING.md`, GitHub issue forms, and a pull request
28
+ template.
29
+ - Contact guidance pointing users to https://co-r-e.com/contact where private
30
+ contact is required.
31
+
32
+ ### Changed
33
+
34
+ - MIT copyright holder is now `CORe Inc.`.
35
+ - Branch protection policy now also covers `develop` and `release/*`, with
36
+ `mokuwaki` as the only bypass user.
37
+
38
+ ## [0.1.1] - 2026-03-15
39
+
40
+ ### Added
41
+
42
+ - MIT license metadata and `LICENSE` file for open source distribution.
43
+ - GitHub Actions trusted publishing workflow for npm releases on `v*` tags.
44
+ - Maintainer release process documentation for `npx rollberry ...` distribution.
45
+
46
+ ### Changed
47
+
48
+ - README rewritten around real `npx` usage, common capture examples, and
49
+ operational troubleshooting.
50
+ - Package metadata now includes repository, homepage, bug tracker, and keywords.
51
+
52
+ ## [0.1.0] - 2026-03-15
53
+
54
+ ### Added
55
+
56
+ - Initial public CLI release for smooth top-to-bottom page capture.
57
+ - `localhost`, `127.0.0.1`, and self-signed `https://localhost` support.
58
+ - Sidecar manifest and JSONL log output.
59
+ - Regression suite runner and fixture-backed integration tests.
@@ -0,0 +1,53 @@
1
+ # Contributing to Rollberry
2
+
3
+ Thank you for contributing to Rollberry.
4
+
5
+ ## Before You Start
6
+
7
+ - Open an issue for bugs, feature requests, or larger changes before starting
8
+ implementation.
9
+ - For security issues, do not open a public issue. Use
10
+ https://co-r-e.com/contact instead.
11
+ - By contributing, you agree that your changes will be distributed under the
12
+ MIT license used by this repository.
13
+
14
+ ## Development Setup
15
+
16
+ ```bash
17
+ corepack pnpm install
18
+ corepack pnpm exec playwright install chromium
19
+ corepack pnpm check
20
+ corepack pnpm test
21
+ ```
22
+
23
+ ## Pull Request Expectations
24
+
25
+ - Keep changes focused and avoid mixing unrelated work.
26
+ - Update docs when CLI behavior or release/operational workflow changes.
27
+ - Add or update tests when behavior changes.
28
+ - Keep `README.md`, `CHANGELOG.md`, and sample config files aligned with the
29
+ code when relevant.
30
+
31
+ ## Branch Protection and Reviews
32
+
33
+ The protected branches are:
34
+
35
+ - `main`
36
+ - `develop`
37
+ - `release/*`
38
+
39
+ For contributors other than `mokuwaki`:
40
+
41
+ - direct push is not allowed on protected branches
42
+ - merge requires at least one approval
43
+ - the latest push must still be approved by someone else
44
+
45
+ `mokuwaki` is the only configured bypass user for these protected branches.
46
+
47
+ ## Release Notes
48
+
49
+ Rollberry remains on the `v0.x.x` line for now. Release prep should update:
50
+
51
+ - `package.json`
52
+ - `CHANGELOG.md`
53
+ - any user-facing docs affected by the release
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CORe Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # Rollberry
2
+
3
+ Rollberry is an MIT-licensed open source CLI for turning web pages into
4
+ smooth top-to-bottom scroll videos. It captures one or more URLs into a single
5
+ MP4. It is built for real browser capture, works with normal URLs and
6
+ `localhost`, and is published for direct `npx` usage.
7
+
8
+ Maintained by CORe Inc.
9
+
10
+ ## Quick Start
11
+
12
+ Requirements:
13
+
14
+ - Node.js `24.12.0+`
15
+ - `ffmpeg` and `ffprobe` available on `PATH`
16
+
17
+ Install nothing globally. Run it directly:
18
+
19
+ ```bash
20
+ npx rollberry capture http://localhost:3000 \
21
+ --out ./artifacts/demo.mp4 \
22
+ --viewport 1440x900 \
23
+ --fps 60 \
24
+ --duration auto \
25
+ --wait-for selector:body \
26
+ --hide-selector '#cookie-banner'
27
+ ```
28
+
29
+ On the first run, if Playwright Chromium is missing, Rollberry installs it
30
+ automatically. `ffmpeg` is not auto-installed.
31
+
32
+ ## Using With npx
33
+
34
+ The normal way to run Rollberry is `npx`.
35
+
36
+ Use the latest published version:
37
+
38
+ ```bash
39
+ npx rollberry capture http://localhost:3000 --out ./artifacts/demo.mp4
40
+ ```
41
+
42
+ Pin a specific version:
43
+
44
+ ```bash
45
+ npx rollberry@0.1.2 capture http://localhost:3000 --out ./artifacts/demo.mp4
46
+ ```
47
+
48
+ Capture a local development server:
49
+
50
+ ```bash
51
+ npx rollberry capture http://localhost:3000 \
52
+ --out ./artifacts/local.mp4 \
53
+ --wait-for selector:body
54
+ ```
55
+
56
+ Capture a public page:
57
+
58
+ ```bash
59
+ npx rollberry capture https://playwright.dev \
60
+ --out ./artifacts/playwright.mp4 \
61
+ --duration 8
62
+ ```
63
+
64
+ Notes:
65
+
66
+ - `npx` downloads the published CLI package automatically
67
+ - on the first run, Rollberry installs Playwright Chromium if needed
68
+ - `ffmpeg` and `ffprobe` must already be available on your machine
69
+ - if you want reproducible automation, pin the package version with
70
+ `npx rollberry@<version> ...`
71
+
72
+ ## What You Get
73
+
74
+ Each run writes:
75
+
76
+ - `video.mp4`: the rendered capture
77
+ - `video.manifest.json`: environment, options, result, and failure details
78
+ - `video.log.jsonl`: structured operational logs
79
+
80
+ You can override the sidecar paths with `--manifest` and `--log-file`.
81
+
82
+ ## Common Examples
83
+
84
+ Capture a development site:
85
+
86
+ ```bash
87
+ npx rollberry capture http://localhost:3000 --out ./artifacts/local.mp4
88
+ ```
89
+
90
+ Capture a public site at a fixed duration:
91
+
92
+ ```bash
93
+ npx rollberry capture https://playwright.dev \
94
+ --out ./artifacts/playwright.mp4 \
95
+ --duration 8 \
96
+ --fps 60
97
+ ```
98
+
99
+ Wait for a selector and hide overlays:
100
+
101
+ ```bash
102
+ npx rollberry capture https://example.com \
103
+ --wait-for selector:main \
104
+ --hide-selector '#cookie-banner' \
105
+ --hide-selector '.intercom-lightweight-app'
106
+ ```
107
+
108
+ Capture multiple pages into a single video:
109
+
110
+ ```bash
111
+ npx rollberry capture \
112
+ https://example.com \
113
+ https://example.com/about \
114
+ https://example.com/contact \
115
+ --out ./artifacts/multi-page.mp4
116
+ ```
117
+
118
+ Add a pause between pages (the last frame of each page is held):
119
+
120
+ ```bash
121
+ npx rollberry capture \
122
+ https://example.com \
123
+ https://example.com/about \
124
+ --page-gap 1.5 \
125
+ --out ./artifacts/with-gap.mp4
126
+ ```
127
+
128
+ Dump raw frames for debugging:
129
+
130
+ ```bash
131
+ npx rollberry capture http://localhost:3000 \
132
+ --out ./artifacts/debug.mp4 \
133
+ --debug-frames-dir ./artifacts/debug-frames
134
+ ```
135
+
136
+ ## CLI Options
137
+
138
+ ```text
139
+ rollberry capture <url...>
140
+
141
+ --out <file> Output MP4 path
142
+ --viewport <WxH> Viewport size, example: 1440x900
143
+ --fps <n> Frames per second
144
+ --duration <seconds|auto> Explicit seconds or auto
145
+ --motion <curve> ease-in-out-sine | linear
146
+ --timeout <ms> Navigation timeout
147
+ --wait-for <mode> load | selector:<css> | ms:<n>
148
+ --hide-selector <css> Hide CSS selector before capture
149
+ --page-gap <seconds> Pause between pages (default: 0)
150
+ --debug-frames-dir <dir> Save raw PNG frames
151
+ --manifest <file> Manifest JSON output path
152
+ --log-file <file> Log JSONL output path
153
+ ```
154
+
155
+ ## Localhost Behavior
156
+
157
+ - Supports `http://localhost:*`, `https://localhost:*`,
158
+ `http://127.0.0.1:*`, and `http://[::1]:*`
159
+ - Retries connection-refused errors until `--timeout`
160
+ - Accepts self-signed certificates for localhost targets only
161
+ - Does not start your dev server for you
162
+
163
+ ## Troubleshooting
164
+
165
+ If `ffmpeg` is missing:
166
+
167
+ ```bash
168
+ brew install ffmpeg
169
+ ```
170
+
171
+ If capture fails, inspect:
172
+
173
+ - `*.manifest.json` for final status and error details
174
+ - `*.log.jsonl` for per-step structured logs
175
+
176
+ If a site keeps shifting during capture:
177
+
178
+ - wait for a stable selector with `--wait-for selector:...`
179
+ - hide chat widgets, cookie banners, and sticky overlays with `--hide-selector`
180
+ - keep dynamic dev-only overlays out of the page when possible
181
+
182
+ ## Support and Contact
183
+
184
+ - General contact: https://co-r-e.com/contact
185
+ - Security issues: see [SECURITY.md](./SECURITY.md)
186
+ - Contribution guide: see [CONTRIBUTING.md](./CONTRIBUTING.md)
187
+
188
+ ## Local Development
189
+
190
+ ```bash
191
+ corepack pnpm install
192
+ corepack pnpm exec playwright install chromium
193
+ corepack pnpm check
194
+ corepack pnpm test
195
+ corepack pnpm build
196
+ ```
197
+
198
+ Run from the repository:
199
+
200
+ ```bash
201
+ corepack pnpm dev -- capture http://localhost:3000 --out ./artifacts/demo.mp4
202
+ ```
203
+
204
+ Run the regression suite:
205
+
206
+ ```bash
207
+ cp regression.sample.json regression.sites.json
208
+ corepack pnpm regression -- --config ./regression.sites.json
209
+ ```
210
+
211
+ ## Release Flow
212
+
213
+ Rollberry stays on the `v0.x.x` line for now.
214
+
215
+ 1. Update `package.json` version and `CHANGELOG.md`
216
+ 2. Commit the release prep
217
+ 3. Create an annotated tag like `git tag -a v0.1.2 -m "Release v0.1.2"`
218
+ 4. Push `main` and the tag to GitHub
219
+ 5. GitHub Actions publishes to npm via trusted publishing
220
+
221
+ Trusted publishing setup expected by this repo:
222
+
223
+ - GitHub repository: `co-r-e/rollberry`
224
+ - Workflow filename: `.github/workflows/publish.yml`
225
+ - Trigger: push tag `v*`
226
+ - In npm package settings, add a trusted publisher that matches the repository
227
+ and workflow above
228
+
229
+ ## License
230
+
231
+ [MIT](./LICENSE)
package/SECURITY.md ADDED
@@ -0,0 +1,23 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ Security fixes are provided for the latest published `v0.x.x` release and the
6
+ current `main` branch on a best-effort basis.
7
+
8
+ ## Reporting a Vulnerability
9
+
10
+ Do not open a public GitHub issue for suspected security vulnerabilities.
11
+
12
+ Please contact us here instead:
13
+
14
+ - https://co-r-e.com/contact
15
+
16
+ Include as much detail as possible:
17
+
18
+ - affected version
19
+ - reproduction steps
20
+ - impact assessment
21
+ - proof of concept, if available
22
+
23
+ We will review the report privately and respond through the contact channel.
@@ -0,0 +1,49 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { constants } from 'node:fs';
3
+ import { access } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import { dirname, join } from 'node:path';
6
+ import { chromium } from 'playwright';
7
+ const require = createRequire(import.meta.url);
8
+ export async function ensureChromiumInstalled(logger) {
9
+ const executablePath = chromium.executablePath();
10
+ if (await hasExecutable(executablePath)) {
11
+ return;
12
+ }
13
+ await logger.warn('browser.install.start', 'Chromium was not found. Installing Playwright Chromium.', { executablePath });
14
+ await installPlaywrightChromium(resolvePlaywrightCliPath());
15
+ if (!(await hasExecutable(executablePath))) {
16
+ throw new Error(`Chromium のインストール後も実行ファイルが見つかりません: ${executablePath}`);
17
+ }
18
+ await logger.info('browser.install.complete', 'Playwright Chromium installation finished.', { executablePath });
19
+ }
20
+ export function resolvePlaywrightCliPath() {
21
+ return join(dirname(require.resolve('playwright/package.json')), 'cli.js');
22
+ }
23
+ async function hasExecutable(path) {
24
+ try {
25
+ await access(path, constants.X_OK);
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ async function installPlaywrightChromium(cliPath) {
33
+ const child = spawn(process.execPath, [cliPath, 'install', 'chromium'], {
34
+ env: process.env,
35
+ stdio: 'inherit',
36
+ });
37
+ await new Promise((resolve, reject) => {
38
+ child.once('error', (error) => {
39
+ reject(error);
40
+ });
41
+ child.once('close', (code) => {
42
+ if (code === 0) {
43
+ resolve();
44
+ return;
45
+ }
46
+ reject(new Error(`Playwright Chromium install failed with exit code ${code ?? 'null'}.`));
47
+ });
48
+ });
49
+ }
@@ -0,0 +1,54 @@
1
+ import { chromium } from 'playwright';
2
+ import { ensureChromiumInstalled } from './browser-install.js';
3
+ import { LOCALHOST_RETRY_INTERVAL_MS } from './constants.js';
4
+ import { delay, isLocalUrl } from './utils.js';
5
+ export async function openBrowserSession(options, logger) {
6
+ await ensureChromiumInstalled(logger);
7
+ const browser = await chromium.launch({
8
+ headless: true,
9
+ });
10
+ const context = await browser.newContext({
11
+ viewport: options.viewport,
12
+ ignoreHTTPSErrors: options.urls.some(isLocalUrl),
13
+ });
14
+ const page = await context.newPage();
15
+ page.setDefaultTimeout(options.timeoutMs);
16
+ await page.addInitScript(() => {
17
+ history.scrollRestoration = 'manual';
18
+ });
19
+ return { browser, page };
20
+ }
21
+ export async function navigateWithRetry(page, url, timeoutMs) {
22
+ const deadline = Date.now() + timeoutMs;
23
+ const canRetry = isLocalUrl(url);
24
+ while (true) {
25
+ const remainingMs = Math.max(1, deadline - Date.now());
26
+ try {
27
+ await page.goto(url.toString(), {
28
+ waitUntil: 'domcontentloaded',
29
+ timeout: remainingMs,
30
+ });
31
+ return;
32
+ }
33
+ catch (error) {
34
+ if (!canRetry ||
35
+ Date.now() + LOCALHOST_RETRY_INTERVAL_MS >= deadline ||
36
+ !isRetryableNavigationError(error)) {
37
+ throw error;
38
+ }
39
+ await delay(LOCALHOST_RETRY_INTERVAL_MS);
40
+ }
41
+ }
42
+ }
43
+ function isRetryableNavigationError(error) {
44
+ if (!(error instanceof Error)) {
45
+ return false;
46
+ }
47
+ return [
48
+ 'ERR_CONNECTION_REFUSED',
49
+ 'ERR_EMPTY_RESPONSE',
50
+ 'ERR_CONNECTION_RESET',
51
+ 'ECONNREFUSED',
52
+ 'ECONNRESET',
53
+ ].some((token) => error.message.includes(token));
54
+ }
@@ -0,0 +1,167 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { navigateWithRetry, openBrowserSession } from './browser.js';
3
+ import { createVideoEncoder } from './ffmpeg.js';
4
+ import { preflightMeasurePage } from './preflight.js';
5
+ import { buildScrollFrames, resolveDurationSeconds } from './scroll-plan.js';
6
+ import { stabilizePage } from './stabilize.js';
7
+ import { ensureDirectory, ensureParentDirectory, waitForAnimationFrames, } from './utils.js';
8
+ export async function captureVideo(options, logger) {
9
+ if (options.urls.length === 0) {
10
+ throw new Error('少なくとも1つのURLを指定してください。');
11
+ }
12
+ await Promise.all([
13
+ ensureParentDirectory(options.outPath),
14
+ ensureParentDirectory(options.logFilePath),
15
+ ensureParentDirectory(options.manifestPath),
16
+ options.debugFramesDir
17
+ ? ensureDirectory(options.debugFramesDir)
18
+ : undefined,
19
+ ]);
20
+ const { browser, page } = await openBrowserSession(options, logger);
21
+ try {
22
+ const encoder = await createVideoEncoder({
23
+ fps: options.fps,
24
+ outPath: options.outPath,
25
+ });
26
+ const pages = [];
27
+ let totalFrameCount = 0;
28
+ let totalDurationSeconds = 0;
29
+ let frameOffset = 0;
30
+ let anyTruncated = false;
31
+ for (const [urlIndex, url] of options.urls.entries()) {
32
+ const { pageResult, lastFrame } = await capturePageFrames({
33
+ page,
34
+ url,
35
+ encoder,
36
+ options,
37
+ logger,
38
+ frameOffset,
39
+ urlIndex,
40
+ });
41
+ pages.push(pageResult);
42
+ totalFrameCount += pageResult.frameCount;
43
+ totalDurationSeconds += pageResult.durationSeconds;
44
+ frameOffset += pageResult.frameCount;
45
+ anyTruncated = anyTruncated || pageResult.truncated;
46
+ const isLastPage = urlIndex === options.urls.length - 1;
47
+ if (options.pageGapSeconds > 0 && !isLastPage) {
48
+ const gapFrameCount = await writeGapFrames({
49
+ encoder,
50
+ frame: lastFrame,
51
+ fps: options.fps,
52
+ gapSeconds: options.pageGapSeconds,
53
+ debugFramesDir: options.debugFramesDir,
54
+ frameOffset,
55
+ });
56
+ totalFrameCount += gapFrameCount;
57
+ totalDurationSeconds += gapFrameCount / options.fps;
58
+ frameOffset += gapFrameCount;
59
+ }
60
+ }
61
+ await encoder.finish();
62
+ await logger.info('encode.complete', 'Video encoding finished', {
63
+ outPath: options.outPath,
64
+ });
65
+ return {
66
+ outPath: options.outPath,
67
+ frameCount: totalFrameCount,
68
+ durationSeconds: totalDurationSeconds,
69
+ pages,
70
+ truncated: anyTruncated,
71
+ };
72
+ }
73
+ finally {
74
+ await browser.close();
75
+ }
76
+ }
77
+ async function capturePageFrames(input) {
78
+ const { page, url, encoder, options, logger, urlIndex } = input;
79
+ const { frameOffset } = input;
80
+ await logger.info('browser.open', `Opening page ${urlIndex + 1}`, {
81
+ url: url.toString(),
82
+ viewport: options.viewport,
83
+ });
84
+ await navigateWithRetry(page, url, options.timeoutMs);
85
+ await stabilizePage({
86
+ page,
87
+ waitFor: options.waitFor,
88
+ hideSelectors: options.hideSelectors,
89
+ });
90
+ const preflight = await preflightMeasurePage(page);
91
+ const plannedDurationSeconds = resolveDurationSeconds(options.duration, preflight.maxScroll);
92
+ const frames = buildScrollFrames({
93
+ fps: options.fps,
94
+ durationSeconds: plannedDurationSeconds,
95
+ maxScroll: preflight.maxScroll,
96
+ motion: options.motion,
97
+ });
98
+ const durationSeconds = frames.length / options.fps;
99
+ await logger.info('render.start', `Rendering frames for page ${urlIndex + 1}`, {
100
+ frameCount: frames.length,
101
+ fps: options.fps,
102
+ durationSeconds,
103
+ maxScroll: preflight.maxScroll,
104
+ url: url.toString(),
105
+ });
106
+ if (preflight.truncated) {
107
+ await logger.warn('preflight.truncated', 'Scroll height exceeded capture limit and was truncated', {
108
+ scrollHeight: preflight.scrollHeight,
109
+ url: url.toString(),
110
+ });
111
+ }
112
+ let lastFrame;
113
+ for (const [index, scrollTop] of frames.entries()) {
114
+ await page.evaluate((nextScrollTop) => {
115
+ window.scrollTo({ top: nextScrollTop, behavior: 'auto' });
116
+ }, scrollTop);
117
+ await waitForAnimationFrames(page);
118
+ const frame = await page.screenshot({
119
+ type: 'png',
120
+ scale: 'css',
121
+ animations: 'disabled',
122
+ caret: 'hide',
123
+ });
124
+ lastFrame = frame;
125
+ if (options.debugFramesDir) {
126
+ const fileName = `${String(frameOffset + index).padStart(5, '0')}.png`;
127
+ await writeFile(`${options.debugFramesDir}/${fileName}`, frame);
128
+ }
129
+ await encoder.writeFrame(frame);
130
+ if ((index + 1) % Math.max(1, Math.floor(frames.length / 10)) === 0) {
131
+ await logger.info('render.progress', 'Rendered frame batch', {
132
+ renderedFrames: index + 1,
133
+ totalFrames: frames.length,
134
+ scrollTop,
135
+ url: url.toString(),
136
+ });
137
+ }
138
+ }
139
+ if (!lastFrame) {
140
+ throw new Error(`ページのフレーム取得に失敗しました: ${url.toString()}`);
141
+ }
142
+ return {
143
+ pageResult: {
144
+ url: url.toString(),
145
+ frameCount: frames.length,
146
+ durationSeconds,
147
+ scrollHeight: preflight.scrollHeight,
148
+ truncated: preflight.truncated,
149
+ },
150
+ lastFrame,
151
+ };
152
+ }
153
+ async function writeGapFrames(input) {
154
+ const { encoder, frame, fps, gapSeconds, debugFramesDir, frameOffset } = input;
155
+ const gapFrameCount = Math.round(fps * gapSeconds);
156
+ if (gapFrameCount <= 0) {
157
+ return 0;
158
+ }
159
+ for (let i = 0; i < gapFrameCount; i++) {
160
+ if (debugFramesDir) {
161
+ const fileName = `${String(frameOffset + i).padStart(5, '0')}.png`;
162
+ await writeFile(`${debugFramesDir}/${fileName}`, frame);
163
+ }
164
+ await encoder.writeFrame(frame);
165
+ }
166
+ return gapFrameCount;
167
+ }
@@ -0,0 +1,8 @@
1
+ export const AUTO_DURATION_MIN_SECONDS = 4;
2
+ export const AUTO_DURATION_MAX_SECONDS = 40;
3
+ export const AUTO_DURATION_PIXELS_PER_SECOND = 800;
4
+ export const LOCALHOST_RETRY_INTERVAL_MS = 500;
5
+ export const STABILIZE_DELAY_MS = 500;
6
+ export const PREFLIGHT_STEP_DELAY_MS = 120;
7
+ export const PREFLIGHT_STABLE_ROUNDS = 3;
8
+ export const PREFLIGHT_MAX_SCROLL_HEIGHT = 30_000;