pupeteer-capture 0.0.1-security → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of pupeteer-capture might be problematic. Click here for more details.

package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Oleksii PELYKH
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 CHANGED
@@ -1,5 +1,125 @@
1
- # Security holding package
1
+ # puppeteer-capture
2
2
 
3
- This package contained malicious code and was removed from the registry by the npm security team. A placeholder was published to ensure users are not affected in the future.
3
+ ![node-current](https://img.shields.io/node/v/puppeteer-capture)
4
+ ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/alexey-pelykh/puppeteer-capture/ci.yml?branch=main)
5
+ ![Codecov](https://img.shields.io/codecov/c/gh/alexey-pelykh/puppeteer-capture)
6
+ ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/alexey-pelykh/puppeteer-capture)
7
+ [![GitHub license](https://img.shields.io/github/license/alexey-pelykh/puppeteer-capture)](https://github.com/alexey-pelykh/puppeteer-capture/blob/main/LICENSE)
4
8
 
5
- Please refer to www.npmjs.com/advisories?search=pupeteer-capture for more information.
9
+ A Puppeteer plugin for capturing page as a video.
10
+
11
+ ## Under The Hood
12
+
13
+ [`HeadlessExperimental`](https://chromedevtools.github.io/devtools-protocol/tot/HeadlessExperimental/) is used to capture frames in a deterministic way. This approach allows to achieve better quality than using screencast.
14
+
15
+ ## Getting Started
16
+
17
+ ```js
18
+ const { capture, launch } = require('puppeteer-capture')
19
+
20
+ (async () => {
21
+ const browser = await launch()
22
+ const page = await browser.newPage()
23
+ const recorder = await capture(page)
24
+ await page.goto('https://google.com', {
25
+ waitUntil: 'networkidle0',
26
+ })
27
+ await recorder.start('capture.mp4')
28
+ await page.waitForTimeout(1000)
29
+ await recorder.stop()
30
+ await recorder.detach()
31
+ await browser.close()
32
+ })()
33
+ ```
34
+
35
+ ## Known Issues
36
+
37
+ ### MacOS is not supported
38
+
39
+ Unfortunately, [it is so](https://source.chromium.org/chromium/chromium/src/+/main:headless/lib/browser/protocol/target_handler.cc;drc=5811aa08e60ba5ac7622f029163213cfbdb682f7;l=32).
40
+
41
+ ## No capturing == Nothing happens
42
+
43
+ This relates to timers, animations, clicks, etc. To process interaction with the page, frame requests have to be submitted and thus capturing have to be active.
44
+
45
+ ## Setting `defaultViewport` causes rendering to freeze
46
+
47
+ The exact origin of the issue is not yet known, yet it's likely to be related to the deterministic mode.
48
+
49
+ Calling `page.setViewport()` before starting the capture behaves the same, yet calling it _after_ starting the capture works yet not always. Thus it's safe to assume that there's some sort of race condition, since adding `page.waitForTimeout(100)` just before setting the viewport workarounds the issue.
50
+
51
+ Also it should be taken into account that since frame size is going to change over the time of the recording, frame size autodetection will fail. To workaround this issue, frame size have to be specified:
52
+ ```js
53
+ const recorder = await capture(page, {
54
+ size: `${viewportWidth}x${viewportHeight}`,
55
+ })
56
+ await recorder.start('capture.mp4', { waitForTimeout: false })
57
+ await page.waitForTimeout(100)
58
+ await page.setViewport({
59
+ width: viewportWidth,
60
+ height: viewportHeight,
61
+ deviceScaleFactor: 1.0,
62
+ })
63
+ ```
64
+
65
+ A friendlier workaround is enabled by default: `recorder.start()` automatically waits for the first frame to be captured.
66
+ This approach seems to allow bypassing the alleged race condition:
67
+
68
+ ```js
69
+ const recorder = await capture(page, {
70
+ size: `${viewportWidth}x${viewportHeight}`,
71
+ })
72
+ await recorder.start('capture.mp4')
73
+ await page.setViewport({
74
+ width: viewportWidth,
75
+ height: viewportHeight,
76
+ deviceScaleFactor: 1.0,
77
+ })
78
+ ```
79
+
80
+ ## `waitForTimeout()` won't work
81
+
82
+ The `Page.waitForTimeout()` method implementation essentially forwards the call to the `Frame.waitForTimeout()` on the `page.mainFrame()`. The latter is implemented via `setTimeout()`, thus can not work in deterministic mode at all.
83
+
84
+ To workaround this issue, there's a `PuppeteerCapture.waitForTimeout()` that waits for the timeout in the timeline of the captured page, which is not real time at all. For convenience, while capturing is active, the page's `waitForTimeout()` becomes a wrapper for `PuppeteerCapture.waitForTimeout()`.
85
+
86
+ ## Multiple `start()`/`stop()` fail
87
+
88
+ It's unclear why, yet after disabling and re-enabling the capture, callbacks from browser stop arriving.
89
+
90
+ ## Time-related functions are affected
91
+
92
+ The following functions have to be overriden with injected versions:
93
+
94
+ - `setTimeout` & `clearTimeout`
95
+ - `setInterval` & `clearInterval`
96
+ - `requestAnimationFrame` & `cancelAnimationFrame`
97
+ - `Date()` & `Date.now()`
98
+ - `performance.now()`
99
+
100
+ The injection should happen before page content loads:
101
+
102
+ ```js
103
+ const recorder = await capture(page) // Injection happens here during attach()
104
+ await page.goto('https://google.com') // Possible capture would happen here, thus injected versions would be captured
105
+ ```
106
+
107
+ ## Events
108
+
109
+ `PuppeteerCapture` supports following events:
110
+
111
+ - `captureStarted`: capture was successfully started
112
+ - `frameCaptured`: frame was captured
113
+ - `frameCaptureFailed`: frame capture failed
114
+ - `frameRecorded`: frame has been submitted to `ffmpeg`
115
+ - `captureStopped`: capture was stopped
116
+
117
+ ## Dependencies
118
+
119
+ ### `ffmpeg`
120
+
121
+ It is resolved in the following order:
122
+
123
+ 1. `FFMPEG` environment variable, should point to the executable
124
+ 2. The executable that's available via the `PATH` environment variable
125
+ 3. Via `@ffmpeg-installer/ffmpeg`, if it's installed as dependency
@@ -0,0 +1,3 @@
1
+ export declare class MissingHeadlessExperimentalRequiredArgs extends Error {
2
+ constructor();
3
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MissingHeadlessExperimentalRequiredArgs = void 0;
4
+ const PuppeteerCaptureViaHeadlessExperimental_1 = require("./PuppeteerCaptureViaHeadlessExperimental");
5
+ class MissingHeadlessExperimentalRequiredArgs extends Error {
6
+ constructor() {
7
+ super('Missing one or more of required arguments: ' + PuppeteerCaptureViaHeadlessExperimental_1.PuppeteerCaptureViaHeadlessExperimental.REQUIRED_ARGS.join(', '));
8
+ this.name = this.constructor.name;
9
+ Error.captureStackTrace(this, this.constructor);
10
+ }
11
+ }
12
+ exports.MissingHeadlessExperimentalRequiredArgs = MissingHeadlessExperimentalRequiredArgs;
@@ -0,0 +1,19 @@
1
+ /// <reference types="node" />
2
+ import type { Page as PuppeteerPage } from 'puppeteer';
3
+ import { Writable } from 'stream';
4
+ import { PuppeteerCaptureEvents } from './PuppeteerCaptureEvents';
5
+ import { PuppeteerCaptureStartOptions } from './PuppeteerCaptureStartOptions';
6
+ export interface PuppeteerCapture {
7
+ page: PuppeteerPage | null;
8
+ isCapturing: boolean;
9
+ captureTimestamp: number;
10
+ capturedFrames: number;
11
+ dropCapturedFrames: boolean;
12
+ recordedFrames: number;
13
+ attach: (page: PuppeteerPage) => Promise<void>;
14
+ detach: () => Promise<void>;
15
+ start: (target: string | Writable, options?: PuppeteerCaptureStartOptions) => Promise<void>;
16
+ stop: () => Promise<void>;
17
+ waitForTimeout: (milliseconds: number) => Promise<void>;
18
+ on: <Event extends keyof PuppeteerCaptureEvents>(event: Event, listener: PuppeteerCaptureEvents[Event]) => this;
19
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,59 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ /// <reference types="node" />
4
+ import { Mutex } from 'async-mutex';
5
+ import { FfmpegCommand } from 'fluent-ffmpeg';
6
+ import { EventEmitter } from 'node:events';
7
+ import type { Page as PuppeteerPage } from 'puppeteer';
8
+ import { PassThrough, Writable } from 'stream';
9
+ import { PuppeteerCapture } from './PuppeteerCapture';
10
+ import { PuppeteerCaptureEvents } from './PuppeteerCaptureEvents';
11
+ import { PuppeteerCaptureOptions } from './PuppeteerCaptureOptions';
12
+ import { PuppeteerCaptureStartOptions } from './PuppeteerCaptureStartOptions';
13
+ export declare abstract class PuppeteerCaptureBase extends EventEmitter implements PuppeteerCapture {
14
+ static readonly DEFAULT_OPTIONS: PuppeteerCaptureOptions;
15
+ static readonly DEFAULT_START_OPTIONS: PuppeteerCaptureStartOptions;
16
+ protected readonly _options: PuppeteerCaptureOptions;
17
+ protected readonly _frameInterval: number;
18
+ protected readonly _onPageClose: () => void;
19
+ protected readonly _startStopMutex: Mutex;
20
+ protected _page: PuppeteerPage | null;
21
+ protected _target: string | Writable | null;
22
+ protected _frameBeingCaptured: Promise<void> | null;
23
+ protected _captureTimestamp: number;
24
+ protected _capturedFrames: number;
25
+ protected _dropCapturedFrames: boolean;
26
+ protected _recordedFrames: number;
27
+ protected _error: any | null;
28
+ protected _framesStream: PassThrough | null;
29
+ protected _ffmpegStream: FfmpegCommand | null;
30
+ protected _ffmpegStarted: Promise<void> | null;
31
+ protected _ffmpegExited: Promise<void> | null;
32
+ protected _ffmpegExitedResolve: (() => void) | null;
33
+ protected _pageWaitForTimeout: ((milliseconds: number) => Promise<void>) | null;
34
+ protected _isCapturing: boolean;
35
+ constructor(options?: PuppeteerCaptureOptions);
36
+ get page(): PuppeteerPage | null;
37
+ get isCapturing(): boolean;
38
+ get captureTimestamp(): number;
39
+ get capturedFrames(): number;
40
+ get dropCapturedFrames(): boolean;
41
+ set dropCapturedFrames(dropCaptiuredFrames: boolean);
42
+ get recordedFrames(): number;
43
+ attach(page: PuppeteerPage): Promise<void>;
44
+ protected _attach(page: PuppeteerPage): Promise<void>;
45
+ detach(): Promise<void>;
46
+ protected _detach(page: PuppeteerPage): Promise<void>;
47
+ start(target: string | Writable, options?: PuppeteerCaptureStartOptions): Promise<void>;
48
+ protected _start(target: string | Writable, options?: PuppeteerCaptureStartOptions): Promise<void>;
49
+ stop(): Promise<void>;
50
+ protected _stop(): Promise<void>;
51
+ waitForTimeout(milliseconds: number): Promise<void>;
52
+ emit<Event extends keyof PuppeteerCaptureEvents>(eventName: Event, ...args: Parameters<PuppeteerCaptureEvents[Event]>): boolean;
53
+ protected onPostCaptureStarted(): Promise<void>;
54
+ protected onPostCaptureStopped(): Promise<void>;
55
+ protected onFrameCaptured(timestamp: number, data: Buffer): Promise<void>;
56
+ protected onFrameCaptureFailed(reason?: any): Promise<void>;
57
+ protected onPageClose(): void;
58
+ private static findFfmpeg;
59
+ }
@@ -0,0 +1,394 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.PuppeteerCaptureBase = void 0;
39
+ const async_mutex_1 = require("async-mutex");
40
+ const fluent_ffmpeg_1 = __importStar(require("fluent-ffmpeg"));
41
+ const promises_1 = require("fs/promises");
42
+ const node_events_1 = require("node:events");
43
+ const path_1 = require("path");
44
+ const stream_1 = require("stream");
45
+ const which_1 = __importDefault(require("which"));
46
+ const PuppeteerCaptureFormat_1 = require("./PuppeteerCaptureFormat");
47
+ class PuppeteerCaptureBase extends node_events_1.EventEmitter {
48
+ constructor(options) {
49
+ super();
50
+ this._options = Object.assign(Object.assign({}, PuppeteerCaptureBase.DEFAULT_OPTIONS), (options !== null ? options : {}));
51
+ if (this._options.fps == null) {
52
+ throw new Error('options.fps needs to be set');
53
+ }
54
+ if (this._options.fps < 0) {
55
+ throw new Error(`options.fps can not be set to ${this._options.fps}`);
56
+ }
57
+ this._frameInterval = 1000.0 / this._options.fps;
58
+ this._onPageClose = this.onPageClose.bind(this);
59
+ this._startStopMutex = new async_mutex_1.Mutex();
60
+ this._page = null;
61
+ this._target = null;
62
+ this._frameBeingCaptured = null;
63
+ this._captureTimestamp = 0;
64
+ this._capturedFrames = 0;
65
+ this._dropCapturedFrames = false;
66
+ this._recordedFrames = 0;
67
+ this._error = null;
68
+ this._framesStream = null;
69
+ this._ffmpegStream = null;
70
+ this._ffmpegStarted = null;
71
+ this._ffmpegExited = null;
72
+ this._ffmpegExitedResolve = null;
73
+ this._pageWaitForTimeout = null;
74
+ this._isCapturing = false;
75
+ }
76
+ get page() {
77
+ return this._page;
78
+ }
79
+ get isCapturing() {
80
+ return this._isCapturing;
81
+ }
82
+ get captureTimestamp() {
83
+ return this._captureTimestamp;
84
+ }
85
+ get capturedFrames() {
86
+ return this._capturedFrames;
87
+ }
88
+ get dropCapturedFrames() {
89
+ return this._dropCapturedFrames;
90
+ }
91
+ set dropCapturedFrames(dropCaptiuredFrames) {
92
+ this._dropCapturedFrames = dropCaptiuredFrames;
93
+ }
94
+ get recordedFrames() {
95
+ return this._recordedFrames;
96
+ }
97
+ attach(page) {
98
+ return __awaiter(this, void 0, void 0, function* () {
99
+ if (this._page != null) {
100
+ throw new Error('Already attached to a page');
101
+ }
102
+ yield this._attach(page);
103
+ this._page = page;
104
+ });
105
+ }
106
+ _attach(page) {
107
+ return __awaiter(this, void 0, void 0, function* () {
108
+ });
109
+ }
110
+ detach() {
111
+ return __awaiter(this, void 0, void 0, function* () {
112
+ if (this._page == null) {
113
+ throw new Error('Already detached from a page');
114
+ }
115
+ yield this._detach(this._page);
116
+ this._page = null;
117
+ });
118
+ }
119
+ _detach(page) {
120
+ return __awaiter(this, void 0, void 0, function* () {
121
+ });
122
+ }
123
+ start(target, options) {
124
+ return __awaiter(this, void 0, void 0, function* () {
125
+ yield this._startStopMutex.runExclusive(() => __awaiter(this, void 0, void 0, function* () { return yield this._start(target, options); }));
126
+ });
127
+ }
128
+ _start(target, options) {
129
+ return __awaiter(this, void 0, void 0, function* () {
130
+ options = Object.assign(Object.assign({}, PuppeteerCaptureBase.DEFAULT_START_OPTIONS), (options !== null ? options : {}));
131
+ if (options.waitForFirstFrame == null) {
132
+ throw new Error('options.waitForFirstFrame can not be null or undefined');
133
+ }
134
+ if (options.waitForFirstFrame == null) {
135
+ throw new Error('options.waitForFirstFrame can not be null or undefined');
136
+ }
137
+ if (this._page == null) {
138
+ throw new Error('Not attached to a page');
139
+ }
140
+ if (this._isCapturing) {
141
+ throw new Error('Capture is in progress');
142
+ }
143
+ if (this._page.isClosed()) {
144
+ throw new Error('Can not start capturing a closed page');
145
+ }
146
+ if (typeof target === 'string' || target instanceof String) {
147
+ yield (0, promises_1.mkdir)((0, path_1.dirname)(target.toString()), { recursive: true });
148
+ }
149
+ const framesStream = new stream_1.PassThrough();
150
+ (0, fluent_ffmpeg_1.setFfmpegPath)(this._options.ffmpeg != null
151
+ ? this._options.ffmpeg
152
+ : yield PuppeteerCaptureBase.findFfmpeg());
153
+ const ffmpegStream = (0, fluent_ffmpeg_1.default)();
154
+ ffmpegStream
155
+ .input(framesStream)
156
+ .inputFormat('image2pipe')
157
+ .inputFPS(this._options.fps); // eslint-disable-line @typescript-eslint/no-non-null-assertion
158
+ ffmpegStream
159
+ .output(target)
160
+ .outputFPS(this._options.fps); // eslint-disable-line @typescript-eslint/no-non-null-assertion
161
+ if (this._options.size != null) {
162
+ ffmpegStream
163
+ .size(this._options.size);
164
+ }
165
+ yield this._options.format(ffmpegStream); // eslint-disable-line @typescript-eslint/no-non-null-assertion
166
+ if (this._options.customFfmpegConfig != null) {
167
+ yield this._options.customFfmpegConfig(ffmpegStream);
168
+ }
169
+ this._page.once('close', this._onPageClose);
170
+ this._target = target;
171
+ this._captureTimestamp = 0;
172
+ this._capturedFrames = 0;
173
+ this._dropCapturedFrames = options.dropCapturedFrames; // eslint-disable-line @typescript-eslint/no-non-null-assertion
174
+ this._recordedFrames = 0;
175
+ this._error = null;
176
+ this._framesStream = framesStream;
177
+ this._ffmpegStream = ffmpegStream;
178
+ this._ffmpegStarted = new Promise((resolve, reject) => {
179
+ const onStart = () => {
180
+ ffmpegStream.off('error', onError);
181
+ resolve();
182
+ };
183
+ const onError = (reason) => {
184
+ ffmpegStream.off('start', onStart);
185
+ reject(reason);
186
+ };
187
+ ffmpegStream
188
+ .once('start', onStart)
189
+ .once('error', onError);
190
+ });
191
+ this._ffmpegExited = new Promise((resolve) => {
192
+ this._ffmpegExitedResolve = resolve;
193
+ const onEnd = () => {
194
+ ffmpegStream.off('error', onError);
195
+ resolve();
196
+ };
197
+ const onError = (reason) => {
198
+ ffmpegStream.off('end', onEnd);
199
+ this._error = reason;
200
+ resolve();
201
+ this._startStopMutex.runExclusive(() => __awaiter(this, void 0, void 0, function* () { return yield this._stop(); }))
202
+ .then(() => { })
203
+ .catch(() => { });
204
+ };
205
+ ffmpegStream
206
+ .once('error', onError)
207
+ .once('end', onEnd);
208
+ });
209
+ this._ffmpegStream.run();
210
+ yield this._ffmpegStarted;
211
+ this._pageWaitForTimeout = this._page.waitForTimeout;
212
+ this._page.waitForTimeout = (milliseconds) => __awaiter(this, void 0, void 0, function* () {
213
+ yield this.waitForTimeout(milliseconds);
214
+ });
215
+ this._isCapturing = true;
216
+ this.emit('captureStarted');
217
+ yield this.onPostCaptureStarted();
218
+ if (options.waitForFirstFrame) {
219
+ yield new Promise((resolve, reject) => {
220
+ const onFrameCaptured = () => {
221
+ this.off('frameCaptureFailed', onFrameCaptureFailed);
222
+ resolve();
223
+ };
224
+ const onFrameCaptureFailed = (reason) => {
225
+ this.off('frameCaptured', onFrameCaptured);
226
+ reject(reason);
227
+ };
228
+ this
229
+ .once('frameCaptured', onFrameCaptured)
230
+ .once('frameCaptureFailed', onFrameCaptureFailed);
231
+ });
232
+ }
233
+ });
234
+ }
235
+ stop() {
236
+ return __awaiter(this, void 0, void 0, function* () {
237
+ if (this._error != null) {
238
+ const error = this._error;
239
+ this._error = null;
240
+ throw error;
241
+ }
242
+ yield this._startStopMutex.runExclusive(() => __awaiter(this, void 0, void 0, function* () { return yield this._stop(); }));
243
+ });
244
+ }
245
+ _stop() {
246
+ return __awaiter(this, void 0, void 0, function* () {
247
+ if (this._page == null) {
248
+ throw new Error('Not attached to a page');
249
+ }
250
+ if (!this._isCapturing) {
251
+ throw new Error('Capture is not in progress');
252
+ }
253
+ this._isCapturing = false;
254
+ while (this._frameBeingCaptured != null) {
255
+ yield this._frameBeingCaptured;
256
+ }
257
+ if (this._ffmpegStarted != null) {
258
+ yield this._ffmpegStarted;
259
+ this._ffmpegStarted = null;
260
+ }
261
+ if (this._framesStream != null) {
262
+ if (this._ffmpegStream != null) {
263
+ this._ffmpegStream.removeAllListeners('error');
264
+ this._ffmpegStream.once('error', () => {
265
+ if (this._ffmpegExitedResolve != null) {
266
+ this._ffmpegExitedResolve();
267
+ }
268
+ });
269
+ }
270
+ this._framesStream.end();
271
+ this._framesStream = null;
272
+ }
273
+ if (this._ffmpegExited != null) {
274
+ yield this._ffmpegExited;
275
+ this._ffmpegExited = null;
276
+ this._ffmpegExitedResolve = null;
277
+ }
278
+ if (this._ffmpegStream != null) {
279
+ this._ffmpegStream = null;
280
+ }
281
+ if (this._target != null) {
282
+ this._target = null;
283
+ }
284
+ if (this._pageWaitForTimeout != null) {
285
+ this._page.waitForTimeout = this._pageWaitForTimeout;
286
+ }
287
+ this._page.off('close', this._onPageClose);
288
+ this.emit('captureStopped');
289
+ yield this.onPostCaptureStopped();
290
+ });
291
+ }
292
+ waitForTimeout(milliseconds) {
293
+ return __awaiter(this, void 0, void 0, function* () {
294
+ if (!this._isCapturing) {
295
+ throw new Error('Can not wait for timeout while not capturing');
296
+ }
297
+ const desiredCaptureTimestamp = this._captureTimestamp + milliseconds;
298
+ let waitPromiseResolve;
299
+ let waitPromiseReject;
300
+ const waitPromise = new Promise((resolve, reject) => {
301
+ waitPromiseResolve = resolve;
302
+ waitPromiseReject = reject;
303
+ });
304
+ const onFrameCaptured = () => {
305
+ if (this._captureTimestamp < desiredCaptureTimestamp) {
306
+ return;
307
+ }
308
+ this
309
+ .off('frameCaptured', onFrameCaptured)
310
+ .off('frameCaptureFailed', onFrameCaptureFailed);
311
+ waitPromiseResolve();
312
+ };
313
+ const onFrameCaptureFailed = (reason) => {
314
+ this
315
+ .off('frameCaptured', onFrameCaptured)
316
+ .off('frameCaptureFailed', onFrameCaptureFailed);
317
+ waitPromiseReject(reason);
318
+ };
319
+ this
320
+ .on('frameCaptured', onFrameCaptured)
321
+ .on('frameCaptureFailed', onFrameCaptureFailed);
322
+ yield waitPromise;
323
+ });
324
+ }
325
+ emit(eventName, ...args) {
326
+ return super.emit(eventName, ...args);
327
+ }
328
+ onPostCaptureStarted() {
329
+ return __awaiter(this, void 0, void 0, function* () {
330
+ });
331
+ }
332
+ onPostCaptureStopped() {
333
+ return __awaiter(this, void 0, void 0, function* () {
334
+ });
335
+ }
336
+ onFrameCaptured(timestamp, data) {
337
+ var _a;
338
+ return __awaiter(this, void 0, void 0, function* () {
339
+ this.emit('frameCaptured', this._capturedFrames, timestamp, data);
340
+ this._capturedFrames += 1;
341
+ if (this._dropCapturedFrames) {
342
+ return;
343
+ }
344
+ (_a = this._framesStream) === null || _a === void 0 ? void 0 : _a.write(data);
345
+ this.emit('frameRecorded', this._recordedFrames, timestamp, data);
346
+ this._recordedFrames += 1;
347
+ });
348
+ }
349
+ onFrameCaptureFailed(reason) {
350
+ return __awaiter(this, void 0, void 0, function* () {
351
+ yield this.stop();
352
+ this._error = reason;
353
+ this.emit('frameCaptureFailed', reason);
354
+ });
355
+ }
356
+ onPageClose() {
357
+ this._error = new Error('Page was closed');
358
+ this._startStopMutex.runExclusive(() => __awaiter(this, void 0, void 0, function* () {
359
+ if (this._isCapturing) {
360
+ yield this._stop();
361
+ }
362
+ yield this.detach();
363
+ }))
364
+ .then(() => { })
365
+ .catch(() => { });
366
+ }
367
+ static findFfmpeg() {
368
+ return __awaiter(this, void 0, void 0, function* () {
369
+ if (process.env.FFMPEG != null) {
370
+ return process.env.FFMPEG;
371
+ }
372
+ try {
373
+ const systemFfmpeg = yield (0, which_1.default)('ffmpeg');
374
+ return systemFfmpeg;
375
+ }
376
+ catch (e) { }
377
+ try {
378
+ const ffmpeg = require('@ffmpeg-installer/ffmpeg'); // eslint-disable-line @typescript-eslint/no-var-requires
379
+ return ffmpeg.path;
380
+ }
381
+ catch (e) { }
382
+ throw new Error('ffmpeg not available: specify FFMPEG environment variable, or make it available via PATH, or add @ffmpeg-installer/ffmpeg to the project');
383
+ });
384
+ }
385
+ }
386
+ exports.PuppeteerCaptureBase = PuppeteerCaptureBase;
387
+ PuppeteerCaptureBase.DEFAULT_OPTIONS = {
388
+ fps: 60,
389
+ format: (0, PuppeteerCaptureFormat_1.MP4)()
390
+ };
391
+ PuppeteerCaptureBase.DEFAULT_START_OPTIONS = {
392
+ waitForFirstFrame: true,
393
+ dropCapturedFrames: false
394
+ };
@@ -0,0 +1,8 @@
1
+ /// <reference types="node" />
2
+ export interface PuppeteerCaptureEvents {
3
+ captureStarted: () => void;
4
+ frameCaptured: (index: number, timestamp: number, data: Buffer) => void;
5
+ frameCaptureFailed: (reason?: any) => void;
6
+ frameRecorded: (index: number, timestamp: number, data: Buffer) => void;
7
+ captureStopped: () => void;
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ import { FfmpegCommand } from 'fluent-ffmpeg';
2
+ export declare function MP4(preset?: 'ultrafast' | 'superfast' | 'veryfast' | 'faster' | 'fast' | 'medium' | 'slow' | 'slower' | 'veryslow', videoCodec?: string): (ffmpeg: FfmpegCommand) => Promise<void>;