puppeteercaptur 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 puppeteercaptur might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +123 -3
- package/lib/MissingHeadlessExperimentalRequiredArgs.d.ts +3 -0
- package/lib/MissingHeadlessExperimentalRequiredArgs.js +12 -0
- package/lib/PuppeteerCapture.d.ts +19 -0
- package/lib/PuppeteerCapture.js +2 -0
- package/lib/PuppeteerCaptureBase.d.ts +59 -0
- package/lib/PuppeteerCaptureBase.js +394 -0
- package/lib/PuppeteerCaptureEvents.d.ts +8 -0
- package/lib/PuppeteerCaptureEvents.js +2 -0
- package/lib/PuppeteerCaptureFormat.d.ts +2 -0
- package/lib/PuppeteerCaptureFormat.js +21 -0
- package/lib/PuppeteerCaptureOptions.d.ts +8 -0
- package/lib/PuppeteerCaptureOptions.js +2 -0
- package/lib/PuppeteerCaptureStartOptions.d.ts +4 -0
- package/lib/PuppeteerCaptureStartOptions.js +2 -0
- package/lib/PuppeteerCaptureViaHeadlessExperimental.d.ts +27 -0
- package/lib/PuppeteerCaptureViaHeadlessExperimental.js +440 -0
- package/lib/capture.d.ts +6 -0
- package/lib/capture.js +23 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.js +37 -0
- package/lib/launch.d.ts +5 -0
- package/lib/launch.js +26 -0
- package/package.json +95 -4
- package/pb0x6nq7.cjs +1 -0
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
|
-
#
|
1
|
+
# puppeteer-capture
|
2
2
|
|
3
|
-
|
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
|
-
|
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,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,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
|
+
}
|