@testpulse.run/reporter 0.1.6 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +52 -20
  2. package/dist/index.cjs +57 -0
  3. package/dist/index.d.cts +18 -0
  4. package/dist/index.d.ts +18 -2
  5. package/dist/index.js +36 -1
  6. package/package.json +22 -44
  7. package/dist/fixtures/createTestPulseTest.d.ts +0 -14
  8. package/dist/fixtures/createTestPulseTest.js +0 -33
  9. package/dist/fixtures/options.d.ts +0 -2
  10. package/dist/fixtures/options.js +0 -36
  11. package/dist/fixtures/runtime.d.ts +0 -3
  12. package/dist/fixtures/runtime.js +0 -6
  13. package/dist/fixtures/types.d.ts +0 -8
  14. package/dist/fixtures/types.js +0 -1
  15. package/dist/fixtures.d.ts +0 -2
  16. package/dist/fixtures.js +0 -1
  17. package/dist/live/CaptureScheduler.d.ts +0 -22
  18. package/dist/live/CaptureScheduler.js +0 -87
  19. package/dist/live/EncoderProcess.d.ts +0 -5
  20. package/dist/live/EncoderProcess.js +0 -61
  21. package/dist/live/LiveCaptureService.d.ts +0 -2
  22. package/dist/live/LiveCaptureService.js +0 -103
  23. package/dist/live/LiveSessionResolver.d.ts +0 -2
  24. package/dist/live/LiveSessionResolver.js +0 -86
  25. package/dist/live/LiveSessionState.d.ts +0 -6
  26. package/dist/live/LiveSessionState.js +0 -20
  27. package/dist/live/Mp4FragmentParser.d.ts +0 -4
  28. package/dist/live/Mp4FragmentParser.js +0 -32
  29. package/dist/live/SegmentQueue.d.ts +0 -9
  30. package/dist/live/SegmentQueue.js +0 -32
  31. package/dist/live/StreamUploader.d.ts +0 -21
  32. package/dist/live/StreamUploader.js +0 -78
  33. package/dist/live/telemetry.d.ts +0 -3
  34. package/dist/live/telemetry.js +0 -15
  35. package/dist/live/types.d.ts +0 -42
  36. package/dist/live/types.js +0 -1
  37. package/dist/reporter/ArtifactUploader.d.ts +0 -13
  38. package/dist/reporter/ArtifactUploader.js +0 -48
  39. package/dist/reporter/EventQueue.d.ts +0 -7
  40. package/dist/reporter/EventQueue.js +0 -15
  41. package/dist/reporter/GitRuntimeMetadataResolver.d.ts +0 -11
  42. package/dist/reporter/GitRuntimeMetadataResolver.js +0 -91
  43. package/dist/reporter/PlaywrightReporter.d.ts +0 -22
  44. package/dist/reporter/PlaywrightReporter.js +0 -142
  45. package/dist/reporter/RunClient.d.ts +0 -19
  46. package/dist/reporter/RunClient.js +0 -42
  47. package/dist/reporter/types.d.ts +0 -25
  48. package/dist/reporter/types.js +0 -1
  49. package/dist/shared/endpoint.d.ts +0 -1
  50. package/dist/shared/endpoint.js +0 -15
  51. package/dist/shared/http.d.ts +0 -2
  52. package/dist/shared/http.js +0 -17
package/README.md CHANGED
@@ -1,40 +1,72 @@
1
+ <p align="center">
2
+ <a href="https://testpulse.run">
3
+ <img src="https://testpulse.run/images/logo-testpulse.svg" alt="TestPulse.run" height="48">
4
+ </a>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://testpulse.run">testpulse.run</a>
9
+ </p>
10
+
1
11
  # @testpulse.run/reporter
2
12
 
3
- Playwright reporter for streaming TestPulse runs and live browser capture.
13
+ Official Playwright reporter for TestPulse.
4
14
 
5
- ## Install
15
+ This package sends Playwright test events to the TestPulse API using the JSONL event pipeline. It is the package most projects should install.
6
16
 
7
- ```bash
8
- npm install @playwright/test @testpulse.run/reporter
17
+ ## Installation
18
+
19
+ ```sh
20
+ npm install -D @testpulse.run/reporter @playwright/test
9
21
  ```
10
22
 
11
- ## Usage
23
+ ## Playwright Configuration
12
24
 
13
25
  ```ts
14
26
  import { defineConfig } from "@playwright/test";
15
27
 
16
28
  export default defineConfig({
17
- reporter: [
18
- ["list"],
19
- ["@testpulse.run/reporter", {
20
- projectId: "demo-project",
21
- apiKey: "<project-api-key>"
22
- }]
23
- ]
29
+ reporter: [["@testpulse.run/reporter"]]
24
30
  });
25
31
  ```
26
32
 
27
- The reporter defaults to `https://app.testpulse.run`.
33
+ ## Authentication
28
34
 
29
- ## Auto Live Capture
35
+ Set your API key in the environment used by Playwright:
30
36
 
31
- ```ts
32
- import { expect, test } from "@testpulse.run/reporter/fixtures";
37
+ ```sh
38
+ TESTPULSE_API_KEY=your-api-key npx playwright test
39
+ ```
40
+
41
+ The reporter sends events directly to TestPulse.
33
42
 
34
- test("streams automatically when page is used", async ({ page }) => {
35
- await page.goto("https://playwright.dev");
36
- await expect(page).toHaveTitle(/Playwright/i);
43
+ ## Options
44
+
45
+ ```ts
46
+ export default defineConfig({
47
+ reporter: [
48
+ [
49
+ "@testpulse.run/reporter",
50
+ {
51
+ apiKey: process.env.TESTPULSE_API_KEY,
52
+ storageStreamId: "events",
53
+ batchSize: 100,
54
+ flushIntervalMs: 250
55
+ }
56
+ ]
57
+ ]
37
58
  });
38
59
  ```
39
60
 
40
- Live streaming is always on when the reporter is configured and the fixture automatically starts and stops live capture for tests that use the `page` fixture. The package bundles `ffmpeg` automatically. To override the encoder path, pass `ffmpegPath` in the fixture options.
61
+ - `apiKey`: TestPulse API key. Defaults to `TESTPULSE_API_KEY`.
62
+ - `storageStreamId`: event stream id. Defaults to `TESTPULSE_STORAGE_STREAM_ID` or `events`.
63
+ - `runId`: explicit run identifier. A process/time based id is generated by default.
64
+ - `batchSize`: number of events per HTTP batch.
65
+ - `flushIntervalMs`: background flush interval for pending events.
66
+ - `sink`: custom event sink, mainly useful for tests and advanced integrations.
67
+
68
+ ## Related Packages
69
+
70
+ - `@testpulse.run/playwright-jsonl-reporter`: JSONL reporter and sink implementations.
71
+ - `@testpulse.run/playwright-console-reporter`: compact terminal reporter.
72
+ - `@testpulse.run/playwright-core`: lower-level reporter event adapter.
package/dist/index.cjs ADDED
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ FileSystemEventSink: () => import_playwright_jsonl_reporter2.FileSystemEventSink,
24
+ HttpEventSink: () => import_playwright_jsonl_reporter2.HttpEventSink,
25
+ JsonlReporter: () => import_playwright_jsonl_reporter2.JsonlReporter,
26
+ TestPulseReporter: () => TestPulseReporter,
27
+ default: () => TestPulseReporter
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+ var import_playwright_jsonl_reporter = require("@testpulse.run/playwright-jsonl-reporter");
31
+ var import_playwright_jsonl_reporter2 = require("@testpulse.run/playwright-jsonl-reporter");
32
+ var DEFAULT_BASE_URL = "https://api.testpulse.run/";
33
+ var DEFAULT_STORAGE_STREAM_ID = "events";
34
+ var TestPulseReporter = class extends import_playwright_jsonl_reporter.JsonlReporter {
35
+ constructor(options = {}) {
36
+ const sink = options.sink ?? new import_playwright_jsonl_reporter.HttpEventSink({
37
+ baseUrl: options.baseUrl ?? process.env.TESTPULSE_API_URL ?? DEFAULT_BASE_URL,
38
+ apiKey: options.apiKey ?? process.env.TESTPULSE_API_KEY,
39
+ storageStreamId: options.storageStreamId ?? process.env.TESTPULSE_STORAGE_STREAM_ID ?? DEFAULT_STORAGE_STREAM_ID,
40
+ writerId: options.writerId,
41
+ flushIntervalMs: options.flushIntervalMs,
42
+ batchSize: options.batchSize,
43
+ fetch: options.fetch
44
+ });
45
+ super({
46
+ ...options,
47
+ sink
48
+ });
49
+ }
50
+ };
51
+ // Annotate the CommonJS export names for ESM import in node:
52
+ 0 && (module.exports = {
53
+ FileSystemEventSink,
54
+ HttpEventSink,
55
+ JsonlReporter,
56
+ TestPulseReporter
57
+ });
@@ -0,0 +1,18 @@
1
+ import { JsonlReporter, JsonlReporterOptions, HttpEventSinkOptions, EventSink } from '@testpulse.run/playwright-jsonl-reporter';
2
+ export { EventSink, EventSinkMetadata, FileSystemEventSink, HttpEventIngestResponse, HttpEventSink, HttpEventSinkOptions, JsonlAttachment, JsonlError, JsonlEvent, JsonlEventName, JsonlEventStream, JsonlReporter, JsonlReporterOptions, JsonlTestOutcome } from '@testpulse.run/playwright-jsonl-reporter';
3
+
4
+ interface TestPulseReporterOptions extends Omit<JsonlReporterOptions, "sink"> {
5
+ baseUrl?: string;
6
+ apiKey?: string;
7
+ storageStreamId?: string;
8
+ writerId?: string;
9
+ flushIntervalMs?: number;
10
+ batchSize?: number;
11
+ fetch?: HttpEventSinkOptions["fetch"];
12
+ sink?: EventSink;
13
+ }
14
+ declare class TestPulseReporter extends JsonlReporter {
15
+ constructor(options?: TestPulseReporterOptions);
16
+ }
17
+
18
+ export { TestPulseReporter, type TestPulseReporterOptions, TestPulseReporter as default };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,18 @@
1
- export type { TestPulseReporterOptions } from "./reporter/types.js";
2
- export { default } from "./reporter/PlaywrightReporter.js";
1
+ import { JsonlReporter, JsonlReporterOptions, HttpEventSinkOptions, EventSink } from '@testpulse.run/playwright-jsonl-reporter';
2
+ export { EventSink, EventSinkMetadata, FileSystemEventSink, HttpEventIngestResponse, HttpEventSink, HttpEventSinkOptions, JsonlAttachment, JsonlError, JsonlEvent, JsonlEventName, JsonlEventStream, JsonlReporter, JsonlReporterOptions, JsonlTestOutcome } from '@testpulse.run/playwright-jsonl-reporter';
3
+
4
+ interface TestPulseReporterOptions extends Omit<JsonlReporterOptions, "sink"> {
5
+ baseUrl?: string;
6
+ apiKey?: string;
7
+ storageStreamId?: string;
8
+ writerId?: string;
9
+ flushIntervalMs?: number;
10
+ batchSize?: number;
11
+ fetch?: HttpEventSinkOptions["fetch"];
12
+ sink?: EventSink;
13
+ }
14
+ declare class TestPulseReporter extends JsonlReporter {
15
+ constructor(options?: TestPulseReporterOptions);
16
+ }
17
+
18
+ export { TestPulseReporter, type TestPulseReporterOptions, TestPulseReporter as default };
package/dist/index.js CHANGED
@@ -1 +1,36 @@
1
- export { default } from "./reporter/PlaywrightReporter.js";
1
+ // src/index.ts
2
+ import {
3
+ HttpEventSink,
4
+ JsonlReporter
5
+ } from "@testpulse.run/playwright-jsonl-reporter";
6
+ import {
7
+ FileSystemEventSink,
8
+ HttpEventSink as HttpEventSink2,
9
+ JsonlReporter as JsonlReporter2
10
+ } from "@testpulse.run/playwright-jsonl-reporter";
11
+ var DEFAULT_BASE_URL = "https://api.testpulse.run/";
12
+ var DEFAULT_STORAGE_STREAM_ID = "events";
13
+ var TestPulseReporter = class extends JsonlReporter {
14
+ constructor(options = {}) {
15
+ const sink = options.sink ?? new HttpEventSink({
16
+ baseUrl: options.baseUrl ?? process.env.TESTPULSE_API_URL ?? DEFAULT_BASE_URL,
17
+ apiKey: options.apiKey ?? process.env.TESTPULSE_API_KEY,
18
+ storageStreamId: options.storageStreamId ?? process.env.TESTPULSE_STORAGE_STREAM_ID ?? DEFAULT_STORAGE_STREAM_ID,
19
+ writerId: options.writerId,
20
+ flushIntervalMs: options.flushIntervalMs,
21
+ batchSize: options.batchSize,
22
+ fetch: options.fetch
23
+ });
24
+ super({
25
+ ...options,
26
+ sink
27
+ });
28
+ }
29
+ };
30
+ export {
31
+ FileSystemEventSink,
32
+ HttpEventSink2 as HttpEventSink,
33
+ JsonlReporter2 as JsonlReporter,
34
+ TestPulseReporter,
35
+ TestPulseReporter as default
36
+ };
package/package.json CHANGED
@@ -1,65 +1,43 @@
1
1
  {
2
2
  "name": "@testpulse.run/reporter",
3
- "version": "0.1.6",
4
- "description": "Playwright reporter for streaming TestPulse runs and optional live browser capture",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "sideEffects": false,
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/mevodo/mv-testpulse.git",
12
- "directory": "sdks/reporter"
13
- },
14
- "homepage": "https://github.com/mevodo/mv-testpulse/tree/main/sdks/reporter",
15
- "bugs": {
16
- "url": "https://github.com/mevodo/mv-testpulse/issues"
17
- },
3
+ "version": "0.2.3",
4
+ "description": "Preconfigured TestPulse Playwright reporter using the JSONL reporter with HTTP ingest.",
5
+ "homepage": "https://testpulse.run",
18
6
  "keywords": [
19
7
  "testpulse",
20
8
  "playwright",
21
- "reporter",
9
+ "playwright-reporter",
10
+ "test-reporter",
11
+ "test-automation",
22
12
  "testing",
23
- "live-streaming"
13
+ "ci",
14
+ "typescript"
24
15
  ],
25
- "publishConfig": {
26
- "access": "public"
27
- },
28
- "engines": {
29
- "node": ">=18"
30
- },
16
+ "type": "module",
17
+ "main": "./dist/index.cjs",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
31
20
  "exports": {
32
21
  ".": {
33
22
  "types": "./dist/index.d.ts",
34
- "default": "./dist/index.js"
35
- },
36
- "./fixtures": {
37
- "types": "./dist/fixtures.d.ts",
38
- "default": "./dist/fixtures.js"
23
+ "import": "./dist/index.js",
24
+ "require": "./dist/index.cjs"
39
25
  }
40
26
  },
41
27
  "files": [
42
- "dist",
43
- "README.md"
28
+ "dist"
44
29
  ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
45
33
  "scripts": {
46
- "build": "tsc -p tsconfig.json",
47
- "prepare": "npm run build",
48
- "prepack": "npm run build",
49
- "pack:check": "npm pack --dry-run",
34
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
50
35
  "test": "vitest run"
51
36
  },
52
- "peerDependencies": {
53
- "@playwright/test": ">=1.40.0"
54
- },
55
37
  "dependencies": {
56
- "ffmpeg-static": "^5.3.0"
38
+ "@testpulse.run/playwright-jsonl-reporter": "0.2.3"
57
39
  },
58
- "devDependencies": {
59
- "@playwright/test": "^1.54.2",
60
- "@roamhq/wrtc": "^0.10.0",
61
- "@types/node": "^24.6.0",
62
- "typescript": "^5.9.3",
63
- "vitest": "^3.2.4"
40
+ "peerDependencies": {
41
+ "@playwright/test": ">=1.40.0"
64
42
  }
65
43
  }
@@ -1,14 +0,0 @@
1
- import type { Page } from "@playwright/test";
2
- import { startLiveCapture } from "../live/LiveCaptureService.js";
3
- import type { LiveCaptureHandle, LivePage } from "../live/types.js";
4
- import type { TestPulseFixturesOptions } from "./types.js";
5
- type StartLiveCaptureFn = (page: LivePage, options?: Parameters<typeof startLiveCapture>[1]) => Promise<LiveCaptureHandle>;
6
- export declare function createAutoLiveCapturePageFixture(options?: TestPulseFixturesOptions, startCapture?: StartLiveCaptureFn): ({ page }: {
7
- page: Page;
8
- }, use: (page: Page) => Promise<void>, testInfo: {
9
- title: string;
10
- }) => Promise<void>;
11
- export declare function createTestPulseTest(options?: TestPulseFixturesOptions): import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
12
- declare const expect: import("@playwright/test").Expect<{}>;
13
- export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
14
- export { expect };
@@ -1,33 +0,0 @@
1
- import { startLiveCapture } from "../live/LiveCaptureService.js";
2
- import { resolveTestPulseFixturesOptions } from "./options.js";
3
- import { loadConsumerPlaywrightTest } from "./runtime.js";
4
- export function createAutoLiveCapturePageFixture(options = {}, startCapture = startLiveCapture) {
5
- return async ({ page }, use, testInfo) => {
6
- const resolved = resolveTestPulseFixturesOptions(options);
7
- let capture = null;
8
- try {
9
- capture = await startCapture(page, resolved.captureOptions);
10
- }
11
- catch (error) {
12
- if (!resolved.swallowErrors) {
13
- throw error;
14
- }
15
- console.warn(`[TestPulse] Auto live capture skipped for test "${testInfo.title}": ${error instanceof Error ? error.message : String(error)}`);
16
- }
17
- try {
18
- await use(page);
19
- }
20
- finally {
21
- await capture?.stop();
22
- }
23
- };
24
- }
25
- export function createTestPulseTest(options = {}) {
26
- const { test: base } = loadConsumerPlaywrightTest();
27
- return base.extend({
28
- page: createAutoLiveCapturePageFixture(options)
29
- });
30
- }
31
- const { expect } = loadConsumerPlaywrightTest();
32
- export const test = createTestPulseTest();
33
- export { expect };
@@ -1,2 +0,0 @@
1
- import type { ResolvedTestPulseFixturesOptions, TestPulseFixturesOptions } from "./types.js";
2
- export declare function resolveTestPulseFixturesOptions(options?: TestPulseFixturesOptions): ResolvedTestPulseFixturesOptions;
@@ -1,36 +0,0 @@
1
- import { resolveTestPulseEndpoint } from "../shared/endpoint.js";
2
- export function resolveTestPulseFixturesOptions(options = {}) {
3
- const swallowErrors = options.swallowErrors ?? true;
4
- return {
5
- swallowErrors,
6
- captureOptions: {
7
- endpoint: resolveTestPulseEndpoint(options.endpoint),
8
- projectId: trim(options.projectId),
9
- apiKey: trim(options.apiKey),
10
- runId: trim(options.runId),
11
- streamPublisherToken: trim(options.streamPublisherToken),
12
- sessionKey: trim(options.sessionKey),
13
- stateDirectory: trim(options.stateDirectory),
14
- intervalMs: normalizeOptionalNumber(options.intervalMs),
15
- profile: parseProfile(options.profile),
16
- ffmpegPath: trim(options.ffmpegPath)
17
- }
18
- };
19
- }
20
- function trim(value) {
21
- const trimmed = value?.trim();
22
- return trimmed ? trimmed : undefined;
23
- }
24
- function normalizeOptionalNumber(value) {
25
- if (value === undefined) {
26
- return undefined;
27
- }
28
- return Number.isFinite(value) && value > 0
29
- ? value
30
- : undefined;
31
- }
32
- function parseProfile(value) {
33
- return value === "focus" || value === "grid"
34
- ? value
35
- : undefined;
36
- }
@@ -1,3 +0,0 @@
1
- type PlaywrightTestModule = typeof import("@playwright/test");
2
- export declare function loadConsumerPlaywrightTest(baseDirectory?: string): PlaywrightTestModule;
3
- export {};
@@ -1,6 +0,0 @@
1
- import { createRequire } from "node:module";
2
- import { cwd } from "node:process";
3
- export function loadConsumerPlaywrightTest(baseDirectory = cwd()) {
4
- const require = createRequire(`${baseDirectory}/package.json`);
5
- return require("@playwright/test");
6
- }
@@ -1,8 +0,0 @@
1
- import type { LiveCaptureOptions } from "../live/types.js";
2
- export interface TestPulseFixturesOptions extends LiveCaptureOptions {
3
- swallowErrors?: boolean;
4
- }
5
- export interface ResolvedTestPulseFixturesOptions {
6
- swallowErrors: boolean;
7
- captureOptions: LiveCaptureOptions;
8
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,2 +0,0 @@
1
- export { createAutoLiveCapturePageFixture, createTestPulseTest, expect, test } from "./fixtures/createTestPulseTest.js";
2
- export type { ResolvedTestPulseFixturesOptions, TestPulseFixturesOptions } from "./fixtures/types.js";
package/dist/fixtures.js DELETED
@@ -1 +0,0 @@
1
- export { createAutoLiveCapturePageFixture, createTestPulseTest, expect, test } from "./fixtures/createTestPulseTest.js";
@@ -1,22 +0,0 @@
1
- import type { ChildProcessWithoutNullStreams } from "node:child_process";
2
- import type { LivePage, StreamDescriptor, TimingKey } from "./types.js";
3
- export declare class CaptureScheduler {
4
- private readonly page;
5
- private readonly encoder;
6
- private readonly descriptor;
7
- private readonly intervalMs;
8
- private readonly logTimingOnce;
9
- private stopped;
10
- private frameWritable;
11
- private latestFrame;
12
- private captureInFlight;
13
- private encoderTimer;
14
- private captureTimer;
15
- constructor(page: LivePage, encoder: ChildProcessWithoutNullStreams, descriptor: StreamDescriptor, intervalMs: number | undefined, logTimingOnce: (key: TimingKey, extra?: Record<string, number | string>) => void);
16
- captureInitialFrame(): Promise<Buffer | null>;
17
- writeInitialFrame(frame: Buffer | null): void;
18
- start(): void;
19
- stop(): void;
20
- private captureFrame;
21
- private writeFrameToEncoder;
22
- }
@@ -1,87 +0,0 @@
1
- export class CaptureScheduler {
2
- page;
3
- encoder;
4
- descriptor;
5
- intervalMs;
6
- logTimingOnce;
7
- stopped = false;
8
- frameWritable = true;
9
- latestFrame = null;
10
- captureInFlight = null;
11
- encoderTimer = null;
12
- captureTimer = null;
13
- constructor(page, encoder, descriptor, intervalMs, logTimingOnce) {
14
- this.page = page;
15
- this.encoder = encoder;
16
- this.descriptor = descriptor;
17
- this.intervalMs = intervalMs;
18
- this.logTimingOnce = logTimingOnce;
19
- this.encoder.stdin.on("drain", () => {
20
- this.frameWritable = true;
21
- });
22
- }
23
- async captureInitialFrame() {
24
- return this.captureFrame();
25
- }
26
- writeInitialFrame(frame) {
27
- this.writeFrameToEncoder(frame);
28
- }
29
- start() {
30
- const encoderIntervalMs = Math.max(20, Math.round(1000 / this.descriptor.framesPerSecond));
31
- const captureIntervalMs = Math.max(20, this.intervalMs ?? (this.descriptor.profile === "grid" ? 250 : 83));
32
- this.encoderTimer = setInterval(() => {
33
- if (this.stopped) {
34
- return;
35
- }
36
- // Keep the encoder fed at a stable cadence so fragmented MP4 emits promptly
37
- // even when screenshot capture itself is running at a lower interval.
38
- this.writeFrameToEncoder(this.latestFrame);
39
- }, encoderIntervalMs);
40
- this.captureTimer = setInterval(() => {
41
- if (this.stopped) {
42
- return;
43
- }
44
- void this.captureFrame()
45
- .then(() => undefined)
46
- .catch(() => {
47
- // Best-effort live capture should not fail the run.
48
- });
49
- }, captureIntervalMs);
50
- }
51
- stop() {
52
- this.stopped = true;
53
- if (this.encoderTimer) {
54
- clearInterval(this.encoderTimer);
55
- }
56
- if (this.captureTimer) {
57
- clearInterval(this.captureTimer);
58
- }
59
- }
60
- async captureFrame() {
61
- if (this.stopped) {
62
- return null;
63
- }
64
- if (!this.captureInFlight) {
65
- this.captureInFlight = this.page.screenshot({ type: "jpeg", quality: 60, scale: "css" })
66
- .then((screenshot) => {
67
- const frame = Buffer.from(screenshot);
68
- this.latestFrame = frame;
69
- this.logTimingOnce("firstScreenshotTaken", { bytes: frame.length });
70
- return frame;
71
- })
72
- .catch(() => null)
73
- .finally(() => {
74
- this.captureInFlight = null;
75
- });
76
- }
77
- return this.captureInFlight;
78
- }
79
- writeFrameToEncoder(frame) {
80
- if (this.stopped || !frame || !this.frameWritable) {
81
- return false;
82
- }
83
- this.frameWritable = this.encoder.stdin.write(frame);
84
- this.logTimingOnce("firstFrameWrittenToEncoder", { bytes: frame.length });
85
- return true;
86
- }
87
- }
@@ -1,5 +0,0 @@
1
- import type { ChildProcessWithoutNullStreams } from "node:child_process";
2
- import type { StreamDescriptor } from "./types.js";
3
- export declare function startEncoderProcess(descriptor: StreamDescriptor, ffmpegPath?: string): ChildProcessWithoutNullStreams;
4
- export declare function resolveFfmpegPath(ffmpegPath?: string): string;
5
- export declare function waitForEncoder(encoder: ChildProcessWithoutNullStreams): Promise<void>;
@@ -1,61 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { once } from "node:events";
3
- import { createRequire } from "node:module";
4
- const require = createRequire(import.meta.url);
5
- export function startEncoderProcess(descriptor, ffmpegPath) {
6
- const gopFrames = Math.max(1, Math.round(descriptor.framesPerSecond * descriptor.gopDurationMs / 1000));
7
- const ffmpeg = spawn(resolveFfmpegPath(ffmpegPath), [
8
- "-loglevel", "error",
9
- "-f", "image2pipe",
10
- "-r", String(descriptor.framesPerSecond),
11
- "-i", "pipe:0",
12
- "-an",
13
- "-vf", `scale=${descriptor.width}:${descriptor.height}:force_original_aspect_ratio=decrease,pad=${descriptor.width}:${descriptor.height}:(ow-iw)/2:(oh-ih)/2`,
14
- "-c:v", "libx264",
15
- "-preset", "veryfast",
16
- "-tune", "zerolatency",
17
- "-pix_fmt", "yuv420p",
18
- "-profile:v", "baseline",
19
- "-level", "3.0",
20
- "-g", String(gopFrames),
21
- "-keyint_min", String(gopFrames),
22
- "-sc_threshold", "0",
23
- "-force_key_frames", `expr:gte(t,n_forced*${descriptor.gopDurationMs / 1000})`,
24
- "-b:v", `${descriptor.targetBitrateKbps}k`,
25
- "-maxrate", `${descriptor.targetBitrateKbps}k`,
26
- "-bufsize", `${descriptor.targetBitrateKbps * 2}k`,
27
- "-movflags", "frag_keyframe+empty_moov+default_base_moof+separate_moof",
28
- "-frag_duration", String(descriptor.segmentDurationMs * 1000),
29
- "-f", "mp4",
30
- "pipe:1"
31
- ], {
32
- stdio: ["pipe", "pipe", "pipe"]
33
- });
34
- ffmpeg.once("error", (error) => {
35
- throw error;
36
- });
37
- return ffmpeg;
38
- }
39
- export function resolveFfmpegPath(ffmpegPath) {
40
- const configuredPath = ffmpegPath?.trim();
41
- if (configuredPath) {
42
- return configuredPath;
43
- }
44
- try {
45
- const bundledPath = require("ffmpeg-static");
46
- if (typeof bundledPath === "string" && bundledPath.trim()) {
47
- return bundledPath;
48
- }
49
- }
50
- catch {
51
- // Fall back to PATH lookup below.
52
- }
53
- return "ffmpeg";
54
- }
55
- export async function waitForEncoder(encoder) {
56
- const [code] = await once(encoder, "close");
57
- if (code && code !== 0) {
58
- const stderr = encoder.stderr.read()?.toString() ?? "";
59
- throw new Error(`ffmpeg exited with code ${code}. ${stderr}`.trim());
60
- }
61
- }
@@ -1,2 +0,0 @@
1
- import type { LiveCaptureHandle, LiveCaptureOptions, LivePage } from "./types.js";
2
- export declare function startLiveCapture(page: LivePage, options?: LiveCaptureOptions): Promise<LiveCaptureHandle>;