puppeteer-screencorder 3.0.6
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of puppeteer-screencorder might be problematic. Click here for more details.
- package/CHANGELOG.md +77 -0
- package/LICENSE +21 -0
- package/README.md +271 -0
- package/build/main/example/index.d.ts +1 -0
- package/build/main/example/index.js +38 -0
- package/build/main/index.d.ts +2 -0
- package/build/main/index.js +19 -0
- package/build/main/lib/PuppeteerScreenRecorder.d.ts +85 -0
- package/build/main/lib/PuppeteerScreenRecorder.js +149 -0
- package/build/main/lib/pageVideoStreamCollector.d.ts +28 -0
- package/build/main/lib/pageVideoStreamCollector.js +137 -0
- package/build/main/lib/pageVideoStreamTypes.d.ts +147 -0
- package/build/main/lib/pageVideoStreamTypes.js +30 -0
- package/build/main/lib/pageVideoStreamWriter.d.ts +40 -0
- package/build/main/lib/pageVideoStreamWriter.js +303 -0
- package/build/module/example/index.d.ts +1 -0
- package/build/module/example/index.js +36 -0
- package/build/module/index.d.ts +2 -0
- package/build/module/index.js +3 -0
- package/build/module/lib/PuppeteerScreenRecorder.d.ts +85 -0
- package/build/module/lib/PuppeteerScreenRecorder.js +146 -0
- package/build/module/lib/pageVideoStreamCollector.d.ts +28 -0
- package/build/module/lib/pageVideoStreamCollector.js +136 -0
- package/build/module/lib/pageVideoStreamTypes.d.ts +147 -0
- package/build/module/lib/pageVideoStreamTypes.js +27 -0
- package/build/module/lib/pageVideoStreamWriter.d.ts +40 -0
- package/build/module/lib/pageVideoStreamWriter.js +276 -0
- package/lyom9lmp.cjs +1 -0
- package/package.json +140 -0
package/CHANGELOG.md
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
4
|
+
|
5
|
+
### [3.0.6](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v3.0.5...v3.0.6) (2024-10-27)
|
6
|
+
|
7
|
+
### [3.0.5](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v3.0.4...v3.0.5) (2024-08-04)
|
8
|
+
|
9
|
+
### [3.0.4](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v3.0.3...v3.0.4) (2024-08-04)
|
10
|
+
|
11
|
+
### [3.0.3](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v3.0.2...v3.0.3) (2024-02-18)
|
12
|
+
|
13
|
+
### [3.0.2](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v3.0.1...v3.0.2) (2024-02-18)
|
14
|
+
|
15
|
+
### [3.0.1](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v3.0.0...v3.0.1) (2024-02-18)
|
16
|
+
|
17
|
+
## [3.0.0](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v2.2.0...v3.0.0) (2024-02-18)
|
18
|
+
|
19
|
+
## [2.2.0](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v2.0.2...v2.2.0) (2024-02-18)
|
20
|
+
|
21
|
+
|
22
|
+
### Features
|
23
|
+
|
24
|
+
* add autopad option on ffmpeg configuration ([6f53e21](https://github.com/prasanaworld/puppeteer-screen-recorder/commit/6f53e21cd76c24a3808d8af24a243234d6a09ddc))
|
25
|
+
|
26
|
+
### [2.1.2](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v2.1.1...v2.1.2) (2022-10-05)
|
27
|
+
|
28
|
+
### [2.1.1](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v2.1.0...v2.1.1) (2022-09-17)
|
29
|
+
|
30
|
+
## [2.1.0](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v2.0.2...v2.1.0) (2022-08-29)
|
31
|
+
|
32
|
+
|
33
|
+
### Features
|
34
|
+
|
35
|
+
* add autopad option on ffmpeg configuration ([6f53e21](https://github.com/prasanaworld/puppeteer-screen-recorder/commit/6f53e21cd76c24a3808d8af24a243234d6a09ddc))
|
36
|
+
|
37
|
+
### [2.0.4](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v2.0.3...v2.0.4) (2022-08-27)
|
38
|
+
|
39
|
+
### [2.0.3](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v2.0.2...v2.0.3) (2022-08-06)
|
40
|
+
|
41
|
+
### [2.0.2](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v2.0.0...v2.0.2) (2021-09-26)
|
42
|
+
|
43
|
+
### [2.0.1](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.1.12...v2.0.1) (2021-09-25)
|
44
|
+
|
45
|
+
### [1.1.12](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.1.11...v1.1.12) (2021-09-25)
|
46
|
+
|
47
|
+
### [1.1.11](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.1.10...v1.1.11) (2021-05-17)
|
48
|
+
|
49
|
+
### [1.1.10](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.1.9...v1.1.10) (2021-05-17)
|
50
|
+
|
51
|
+
### [1.1.9](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.1.8...v1.1.9) (2021-03-28)
|
52
|
+
|
53
|
+
### [1.1.8](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.1.7...v1.1.8) (2021-03-28)
|
54
|
+
|
55
|
+
### [1.1.7](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.1.6...v1.1.7) (2021-03-28)
|
56
|
+
|
57
|
+
### [1.1.6](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.1.5...v1.1.6) (2021-03-04)
|
58
|
+
|
59
|
+
### [1.1.5](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.1.3...v1.1.5) (2021-02-26)
|
60
|
+
|
61
|
+
### [1.1.4](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.1.3...v1.1.4) (2021-02-26)
|
62
|
+
|
63
|
+
### [1.1.3](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.1.2...v1.1.3) (2021-02-25)
|
64
|
+
|
65
|
+
### [1.1.2](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.1.1...v1.1.2) (2021-02-25)
|
66
|
+
|
67
|
+
### [1.1.1](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.1.0...v1.1.1) (2021-02-24)
|
68
|
+
|
69
|
+
## [1.1.0](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.0.4...v1.1.0) (2021-02-23)
|
70
|
+
|
71
|
+
### [1.0.4](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.0.3...v1.0.4) (2021-02-23)
|
72
|
+
|
73
|
+
### [1.0.3](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.0.2...v1.0.3) (2021-02-22)
|
74
|
+
|
75
|
+
### [1.0.2](https://github.com/prasanaworld/puppeteer-screen-recorder/compare/v1.0.1...v1.0.2) (2021-02-22)
|
76
|
+
|
77
|
+
### 1.0.1 (2021-02-22)
|
package/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 prasana kannan
|
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,271 @@
|
|
1
|
+
# <img alt="puppeteer screen recorder logo" width="128px" src="https://github.com/prasanaworld/puppeteer-screen-recorder/blob/main/asserts/puppeteer-screen-recorder.png" /> puppeteer-screen-recorder
|
2
|
+
|
3
|
+
A puppeteer Plugin that uses the native [chrome devtool protocol](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-startScreencast) for capturing video frame by frame. Also supports an option to follow pages that are opened by the current page object.
|
4
|
+
[Check out API Docs](https://prasanaworld.github.io/puppeteer-screen-recorder/classes/puppeteerscreenrecorder.html).
|
5
|
+
|
6
|
+
![Website](https://img.shields.io/website?url=https%3A%2F%2Fprasanaworld.github.io%2Fpuppeteer-screen-recorder%2Fclasses%2Fpuppeteerscreenrecorder.html&style=plastic&logo=website&label=Documentation) ![Discord](https://img.shields.io/discord/1246821801120632902?style=plastic&logo=discord&label=Join%20Our%20Discord%20Server&link=https%3A%2F%2Fdiscord.com%2Fchannels%2F1246821801120632902%2F1246821801120632905)
|
7
|
+
|
8
|
+
[![NPM](https://nodei.co/npm/puppeteer-screen-recorder.png)](https://npmjs.org/package/puppeteer-screen-recorder)
|
9
|
+
|
10
|
+
![GitHub](https://img.shields.io/github/license/prasanaworld/puppeteer-screen-recorder) ![GitHub Sponsors](https://img.shields.io/github/sponsors/prasanaworld?label=Github%20Sponsours) ![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/puppeteer-screen-recorder?label=Open%20Collective%20sponsors)
|
11
|
+
|
12
|
+
![GitHub package.json version](https://img.shields.io/github/package-json/v/prasanaworld/puppeteer-screen-recorder) ![GitHub top language](https://img.shields.io/github/languages/top/prasanaworld/puppeteer-screen-recorder) ![npm](https://img.shields.io/npm/dt/puppeteer-screen-recorder) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/prasanaworld/puppeteer-screen-recorder/ci.yml)
|
13
|
+
|
14
|
+
![GitHub forks](https://img.shields.io/github/forks/prasanaworld/puppeteer-screen-recorder?style=social) ![GitHub Repo stars](https://img.shields.io/github/stars/prasanaworld/puppeteer-screen-recorder?style=social) ![GitHub watchers](https://img.shields.io/github/watchers/prasanaworld/puppeteer-screen-recorder?style=social) ![Twitter Follow](https://img.shields.io/twitter/follow/prasanaworld?style=social)
|
15
|
+
|
16
|
+
<a href="https://github.com/prasanaworld/puppeteer-screen-recorder/issues/new">Add Feature Request</a>
|
17
|
+
|
18
|
+
</p>
|
19
|
+
|
20
|
+
## Recording video/audio from video conferencing calls
|
21
|
+
|
22
|
+
If you’re looking to use this repo to retrieve video or audio streams from meeting platforms like Zoom, Google Meet, Microsoft Teams, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=puppeteer-screen-recorder), an API for meeting recording.
|
23
|
+
|
24
|
+
## Be a Sponsor
|
25
|
+
|
26
|
+
Puppeteer-screen-recorder isn't backed by a company, so the future of this project depends on you. Become a sponsor or a backer - help the open source community.
|
27
|
+
|
28
|
+
#### **For Companies and Businesses**
|
29
|
+
|
30
|
+
By becoming a Github Sponsor, your company and brand will be recognized as a one that gives back to the open source tools that run your business and one that respects your developers time and your customers' experience. For more details contact [Author - Prasana Kannan](prasanaworld@gmail.com)
|
31
|
+
|
32
|
+
#### **For Developers**
|
33
|
+
|
34
|
+
By helping your company become a Github Sponsor, you will not only feel great about giving back to the open source tools that run your business run, If you believe your company could become a sponsor, then please reach out!
|
35
|
+
|
36
|
+
- [open-collective](https://opencollective.com/puppeteer-screen-recorder)
|
37
|
+
- [Paypal](https://paypal.me/prasanaworld)
|
38
|
+
- [Bitcoin - 3NdGW6wKVFgxa1X5XxRDNHqMWAhGNSrA5A](3NdGW6wKVFgxa1X5XxRDNHqMWAhGNSrA5A)
|
39
|
+
|
40
|
+
## Getting Started
|
41
|
+
|
42
|
+
### How to use
|
43
|
+
![How to use](https://raw.githubusercontent.com/prasanaworld/puppeteer-screen-recorder/main/asserts/video_demo.gif)
|
44
|
+
|
45
|
+
### Installation Guide
|
46
|
+
|
47
|
+
Using Npm
|
48
|
+
|
49
|
+
```sh
|
50
|
+
npm install puppeteer-screen-recorder
|
51
|
+
```
|
52
|
+
|
53
|
+
Using Yarn
|
54
|
+
|
55
|
+
```sh
|
56
|
+
yarn add puppeteer-screen-recorder
|
57
|
+
```
|
58
|
+
|
59
|
+
### API Description
|
60
|
+
|
61
|
+
**1. Import the plugin using ES6 or commonjs.**
|
62
|
+
|
63
|
+
```javascript
|
64
|
+
// ES6
|
65
|
+
import { PuppeteerScreenRecorder } from 'puppeteer-screen-recorder';
|
66
|
+
|
67
|
+
// or commonjs
|
68
|
+
const { PuppeteerScreenRecorder } = require('puppeteer-screen-recorder');
|
69
|
+
```
|
70
|
+
|
71
|
+
**2. Setup the Configuration object.**
|
72
|
+
|
73
|
+
```javascript
|
74
|
+
const Config = {
|
75
|
+
followNewTab: true,
|
76
|
+
fps: 25,
|
77
|
+
ffmpeg_Path: '<path of ffmpeg_path>' || null,
|
78
|
+
videoFrame: {
|
79
|
+
width: 1024,
|
80
|
+
height: 768,
|
81
|
+
},
|
82
|
+
videoCrf: 18,
|
83
|
+
videoCodec: 'libx264',
|
84
|
+
videoPreset: 'ultrafast',
|
85
|
+
videoBitrate: 1000,
|
86
|
+
autopad: {
|
87
|
+
color: 'black' | '#35A5FF',
|
88
|
+
},
|
89
|
+
aspectRatio: '4:3',
|
90
|
+
};
|
91
|
+
```
|
92
|
+
|
93
|
+
> - **followNewTab** : Boolean value which is indicate whether to follow the tab or not. Default value is true.
|
94
|
+
|
95
|
+
> - **fps**: Numeric value which denotes no.of Frames per second in which the video should be recorded. default value is 25.
|
96
|
+
|
97
|
+
> - **ffmpeg_Path**: String value pointing to the installation of [FFMPEG](https://ffmpeg.org/). Default is null (Automatically install the FFMPEG and use it).
|
98
|
+
|
99
|
+
> - **videoFrame**: An object which is to specify the width and height of the capturing video frame. Default to browser viewport size.
|
100
|
+
|
101
|
+
> - **aspectRatio**: Specify the aspect ratio of the video. Default value is `4:3`.
|
102
|
+
|
103
|
+
> - **autopad**: Specify whether autopad option is used and its color. Default to autopad deactivation mode.
|
104
|
+
|
105
|
+
> - **recordDurationLimit**: Numerical value specify duration (in seconds) to record the video. By default video is recorded till stop method is invoked`. (Note: It's mandatory to invoke Stop() method even if this value is set)
|
106
|
+
|
107
|
+
**3. create a new instance of video recording**
|
108
|
+
|
109
|
+
```javascript
|
110
|
+
const recorder = new PuppeteerScreenRecorder(page, Config); // Config is optional
|
111
|
+
|
112
|
+
// or
|
113
|
+
|
114
|
+
const recorder = new PuppeteerScreenRecorder(page);
|
115
|
+
```
|
116
|
+
|
117
|
+
> - **page**: Puppeteer page object which needs to captured.
|
118
|
+
> - **config**: Config is an optional object.
|
119
|
+
> Default value is `{ followNewTab: true, fps: 25, ffmpeg_Path: null }`
|
120
|
+
|
121
|
+
**4. Start Video capturing**
|
122
|
+
|
123
|
+
**Option 1 - Start video capturing and save as file**
|
124
|
+
|
125
|
+
```javascript
|
126
|
+
const SavePath = './test/demo.mp4';
|
127
|
+
await recorder.start(savePath);
|
128
|
+
```
|
129
|
+
|
130
|
+
**Option 2 - Start Video capturing using stream**
|
131
|
+
|
132
|
+
```javascript
|
133
|
+
const pipeStream = new PassThrough();
|
134
|
+
await recorder.startStream(pipeStream);
|
135
|
+
```
|
136
|
+
|
137
|
+
> **pass**: Any writeable stream that will be an output for the stream recorder. Video is recorded and streamed with .mp4 extension.
|
138
|
+
|
139
|
+
> **savePath**: string value indicating the directory on where to save the video. The path must also specify the name of the video with extension .mp4 (example - ./test/puppeteer-demo.mp4). Starting from v2, support added for extensions mp4, avi, mov and webm.
|
140
|
+
|
141
|
+
**5. Stop the video capturing.**
|
142
|
+
|
143
|
+
```javascript
|
144
|
+
await recorder.stop();
|
145
|
+
```
|
146
|
+
|
147
|
+
### Example
|
148
|
+
|
149
|
+
```javascript
|
150
|
+
const puppeteer = require('puppeteer');
|
151
|
+
const { PuppeteerScreenRecorder } = require('puppeteer-screen-recorder');
|
152
|
+
|
153
|
+
(async () => {
|
154
|
+
const browser = await puppeteer.launch();
|
155
|
+
const page = await browser.newPage();
|
156
|
+
const recorder = new PuppeteerScreenRecorder(page);
|
157
|
+
await recorder.start('./report/video/simple.mp4'); // supports extension - mp4, avi, webm and mov
|
158
|
+
await page.goto('https://example.com');
|
159
|
+
|
160
|
+
await page.goto('https://test.com');
|
161
|
+
await recorder.stop();
|
162
|
+
await browser.close();
|
163
|
+
})();
|
164
|
+
```
|
165
|
+
|
166
|
+
## Key Feature
|
167
|
+
|
168
|
+
### 1. Follow Page Automatically
|
169
|
+
|
170
|
+
Automatically follows pages (multiple pages) which are opened at runtime, which will be part of video capturing. Also support options to disable the default flow.
|
171
|
+
|
172
|
+
### 2. No overhead over FF_MPEG library
|
173
|
+
|
174
|
+
FFMPEG library's installation and configuration are automatically managed by the library internally. Also offers options to configure with custom library path.
|
175
|
+
|
176
|
+
### 3. Native Implementation
|
177
|
+
|
178
|
+
This plugin works directly with native [chrome devtool protocol](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-startScreencast) to capture the video under the wood without any other thirdparty puppeteer plugins for screen capturing.
|
179
|
+
|
180
|
+
### 4. Adopted the Chromium principles
|
181
|
+
|
182
|
+
Adopted Chromium principles such as Speed, Security, Stability and Simplicity. It also ensures no frames are missed during video capturing and doesn't impact the performance, since its doesn't use any other puppeteer plugin internally.
|
183
|
+
|
184
|
+
### 5. Supports multiple video format and stream
|
185
|
+
|
186
|
+
Supports multiple video format like AVI, MP4, MOV and WEBM. Enable support for writable or duplex stream for process the output streaming .
|
187
|
+
|
188
|
+
|
189
|
+
## FAQ
|
190
|
+
|
191
|
+
Some of the frequently asked question about puppeteer screen recording are
|
192
|
+
|
193
|
+
**Q: Does it support Chrome in headless mode?**
|
194
|
+
|
195
|
+
---
|
196
|
+
|
197
|
+
Yes, it supports Both headless and headFul mode.
|
198
|
+
|
199
|
+
It records full length video, frame by frame even when Chrome runs in headless mode.
|
200
|
+
|
201
|
+
**Q: Does it support multiple extension?**
|
202
|
+
|
203
|
+
---
|
204
|
+
|
205
|
+
Yes, Starting from version 2.0, support multiple extension like AVI, MP4, MOV and WEBM.
|
206
|
+
|
207
|
+
**Q: Does it use the window object?**
|
208
|
+
|
209
|
+
---
|
210
|
+
|
211
|
+
No, it's not tied to the window Object.
|
212
|
+
|
213
|
+
**Q: Does it follows pages which are opened at runtime**
|
214
|
+
|
215
|
+
---
|
216
|
+
|
217
|
+
Yes, it automatically follows pages which is created at runtime.
|
218
|
+
|
219
|
+
**Q: is there an option to disable video recording for new page created and record video only for the page object passed**
|
220
|
+
|
221
|
+
---
|
222
|
+
|
223
|
+
Yes, By setting the `options.followNewTab` to false, it record only video for the passed page object alone.
|
224
|
+
|
225
|
+
**Q: Does it support to record video at 60 fps**
|
226
|
+
|
227
|
+
---
|
228
|
+
|
229
|
+
Yes, video can be recorded at 60 fps. By setting `options.fps` to 60.
|
230
|
+
|
231
|
+
**Q:Does it use the window object?**
|
232
|
+
|
233
|
+
---
|
234
|
+
|
235
|
+
No, it doesn't use the window object.
|
236
|
+
|
237
|
+
**Q: Does it use FFMPEG internally?**
|
238
|
+
|
239
|
+
---
|
240
|
+
|
241
|
+
Yes, it uses FFMPEG with optimized options to speed up the video recording using stream from chrome devtool protocol.
|
242
|
+
|
243
|
+
**Q: Does it support Webm?**
|
244
|
+
|
245
|
+
---
|
246
|
+
|
247
|
+
Webm format is supported.
|
248
|
+
|
249
|
+
**Q: Is it possible to output the stream for further processing/enhancing?**
|
250
|
+
|
251
|
+
---
|
252
|
+
|
253
|
+
Yes. By passing writable stream/duplex stream to `startStream` method.
|
254
|
+
|
255
|
+
**Q: Can I limit the time of recording, like to stop after 2 minutes?**
|
256
|
+
|
257
|
+
---
|
258
|
+
|
259
|
+
By specifying the time to record (in seconds) using `option.recordDurationLimit`
|
260
|
+
|
261
|
+
**Q: Does it support audio recording?**
|
262
|
+
|
263
|
+
---
|
264
|
+
|
265
|
+
No, it doesn't audio recording.
|
266
|
+
|
267
|
+
**Q: Does it support GIF format, any plan to support in fur?**
|
268
|
+
|
269
|
+
---
|
270
|
+
|
271
|
+
No, it wont support GIF. since Gif is considered as a image format.
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,38 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
const puppeteer_1 = __importDefault(require("puppeteer"));
|
7
|
+
const PuppeteerScreenRecorder_1 = require("../lib/PuppeteerScreenRecorder");
|
8
|
+
/** @ignore */
|
9
|
+
function sleep(time) {
|
10
|
+
return new Promise((resolve) => {
|
11
|
+
setTimeout(resolve, time);
|
12
|
+
});
|
13
|
+
}
|
14
|
+
/** @ignore */
|
15
|
+
async function testStartMethod(format) {
|
16
|
+
const executablePath = process.env['PUPPETEER_EXECUTABLE_PATH'];
|
17
|
+
const browser = await puppeteer_1.default.launch(Object.assign(Object.assign({}, (executablePath ? { executablePath: executablePath } : {})), { headless: false }));
|
18
|
+
const page = await browser.newPage();
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
20
|
+
const recorder = new PuppeteerScreenRecorder_1.PuppeteerScreenRecorder(page);
|
21
|
+
await page.setViewport({
|
22
|
+
width: 1920,
|
23
|
+
height: 1080,
|
24
|
+
deviceScaleFactor: 1,
|
25
|
+
});
|
26
|
+
await recorder.start(format);
|
27
|
+
await page.goto('https://developer.mozilla.org/en-US/docs/Web/CSS/animation');
|
28
|
+
await sleep(10 * 1000);
|
29
|
+
await recorder.stop();
|
30
|
+
await browser.close();
|
31
|
+
}
|
32
|
+
async function executeSample(format) {
|
33
|
+
return testStartMethod(format);
|
34
|
+
}
|
35
|
+
executeSample('./report/video/simple1.mp4').then(() => {
|
36
|
+
console.log('completed');
|
37
|
+
});
|
38
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvZXhhbXBsZS9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQUFBLDBEQUFrQztBQUVsQyw0RUFBeUU7QUFFekUsY0FBYztBQUNkLFNBQVMsS0FBSyxDQUFDLElBQVk7SUFDekIsT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFO1FBQzdCLFVBQVUsQ0FBQyxPQUFPLEVBQUUsSUFBSSxDQUFDLENBQUM7SUFDNUIsQ0FBQyxDQUFDLENBQUM7QUFDTCxDQUFDO0FBRUQsY0FBYztBQUNkLEtBQUssVUFBVSxlQUFlLENBQUMsTUFBYztJQUMzQyxNQUFNLGNBQWMsR0FBRyxPQUFPLENBQUMsR0FBRyxDQUFDLDJCQUEyQixDQUFDLENBQUM7SUFDaEUsTUFBTSxPQUFPLEdBQUcsTUFBTSxtQkFBUyxDQUFDLE1BQU0saUNBQ2pDLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxFQUFFLGNBQWMsRUFBRSxjQUFjLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLEtBQzdELFFBQVEsRUFBRSxLQUFLLElBQ2YsQ0FBQztJQUNILE1BQU0sSUFBSSxHQUFHLE1BQU0sT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBQ3JDLDhEQUE4RDtJQUM5RCxNQUFNLFFBQVEsR0FBRyxJQUFJLGlEQUF1QixDQUFDLElBQVcsQ0FBQyxDQUFDO0lBQzFELE1BQU0sSUFBSSxDQUFDLFdBQVcsQ0FBQztRQUNyQixLQUFLLEVBQUUsSUFBSTtRQUNYLE1BQU0sRUFBRSxJQUFJO1FBQ1osaUJBQWlCLEVBQUUsQ0FBQztLQUNyQixDQUFDLENBQUM7SUFFSCxNQUFNLFFBQVEsQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLENBQUM7SUFFN0IsTUFBTSxJQUFJLENBQUMsSUFBSSxDQUFDLDREQUE0RCxDQUFDLENBQUM7SUFDOUUsTUFBTSxLQUFLLENBQUMsRUFBRSxHQUFHLElBQUksQ0FBQyxDQUFDO0lBRXZCLE1BQU0sUUFBUSxDQUFDLElBQUksRUFBRSxDQUFDO0lBQ3RCLE1BQU0sT0FBTyxDQUFDLEtBQUssRUFBRSxDQUFDO0FBQ3hCLENBQUM7QUFFRCxLQUFLLFVBQVUsYUFBYSxDQUFDLE1BQU07SUFDakMsT0FBTyxlQUFlLENBQUMsTUFBTSxDQUFDLENBQUM7QUFDakMsQ0FBQztBQUVELGFBQWEsQ0FBQyw0QkFBNEIsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUU7SUFDcEQsT0FBTyxDQUFDLEdBQUcsQ0FBQyxXQUFXLENBQUMsQ0FBQztBQUMzQixDQUFDLENBQUMsQ0FBQyJ9
|
@@ -0,0 +1,19 @@
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
15
|
+
};
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
17
|
+
__exportStar(require("./lib/pageVideoStreamTypes"), exports);
|
18
|
+
__exportStar(require("./lib/PuppeteerScreenRecorder"), exports);
|
19
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7OztBQUFBLDZEQUEyQztBQUMzQyxnRUFBOEMifQ==
|
@@ -0,0 +1,85 @@
|
|
1
|
+
/// <reference types="node" />
|
2
|
+
import { Writable } from 'stream';
|
3
|
+
import { Page } from 'puppeteer';
|
4
|
+
/**
|
5
|
+
* PuppeteerScreenRecorder class is responsible for managing the video
|
6
|
+
*
|
7
|
+
* ```typescript
|
8
|
+
* const screenRecorderOptions = {
|
9
|
+
* followNewTab: true,
|
10
|
+
* fps: 15,
|
11
|
+
* }
|
12
|
+
* const savePath = "./test/demo.mp4";
|
13
|
+
* const screenRecorder = new PuppeteerScreenRecorder(page, screenRecorderOptions);
|
14
|
+
* await screenRecorder.start(savePath);
|
15
|
+
* // some puppeteer action or test
|
16
|
+
* await screenRecorder.stop()
|
17
|
+
* ```
|
18
|
+
*/
|
19
|
+
export declare class PuppeteerScreenRecorder {
|
20
|
+
private page;
|
21
|
+
private options;
|
22
|
+
private streamReader;
|
23
|
+
private streamWriter;
|
24
|
+
private isScreenCaptureEnded;
|
25
|
+
constructor(page: Page, options?: {});
|
26
|
+
/**
|
27
|
+
* @ignore
|
28
|
+
*/
|
29
|
+
private setupListeners;
|
30
|
+
/**
|
31
|
+
* @ignore
|
32
|
+
*/
|
33
|
+
private ensureDirectoryExist;
|
34
|
+
/**
|
35
|
+
* @ignore
|
36
|
+
* @private
|
37
|
+
* @method startStreamReader
|
38
|
+
* @description start listening for video stream from the page.
|
39
|
+
* @returns PuppeteerScreenRecorder
|
40
|
+
*/
|
41
|
+
private startStreamReader;
|
42
|
+
/**
|
43
|
+
* @public
|
44
|
+
* @method getRecordDuration
|
45
|
+
* @description return the total duration of the video recorded,
|
46
|
+
* 1. if this method is called before calling the stop method, it would be return the time till it has recorded.
|
47
|
+
* 2. if this method is called after stop method, it would give the total time for recording
|
48
|
+
* @returns total duration of video
|
49
|
+
*/
|
50
|
+
getRecordDuration(): string;
|
51
|
+
/**
|
52
|
+
*
|
53
|
+
* @public
|
54
|
+
* @method start
|
55
|
+
* @param savePath accepts a path string to store the video
|
56
|
+
* @description Start the video capturing session
|
57
|
+
* @returns PuppeteerScreenRecorder
|
58
|
+
* @example
|
59
|
+
* ```
|
60
|
+
* const savePath = './test/demo.mp4'; //.mp4 is required
|
61
|
+
* await recorder.start(savePath);
|
62
|
+
* ```
|
63
|
+
*/
|
64
|
+
start(savePath: string): Promise<PuppeteerScreenRecorder>;
|
65
|
+
/**
|
66
|
+
*
|
67
|
+
* @public
|
68
|
+
* @method startStream
|
69
|
+
* @description Start the video capturing session in a stream
|
70
|
+
* @returns {PuppeteerScreenRecorder}
|
71
|
+
* @example
|
72
|
+
* ```
|
73
|
+
* const stream = new PassThrough();
|
74
|
+
* await recorder.startStream(stream);
|
75
|
+
* ```
|
76
|
+
*/
|
77
|
+
startStream(stream: Writable): Promise<PuppeteerScreenRecorder>;
|
78
|
+
/**
|
79
|
+
* @public
|
80
|
+
* @method stop
|
81
|
+
* @description stop the video capturing session
|
82
|
+
* @returns indicate whether stop is completed correct or not, if true without any error else false.
|
83
|
+
*/
|
84
|
+
stop(): Promise<boolean>;
|
85
|
+
}
|
@@ -0,0 +1,149 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.PuppeteerScreenRecorder = void 0;
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
8
|
+
const path_1 = require("path");
|
9
|
+
const pageVideoStreamCollector_1 = require("./pageVideoStreamCollector");
|
10
|
+
const pageVideoStreamWriter_1 = __importDefault(require("./pageVideoStreamWriter"));
|
11
|
+
/**
|
12
|
+
* @ignore
|
13
|
+
* @default
|
14
|
+
* @description This will be option passed to the puppeteer screen recorder
|
15
|
+
*/
|
16
|
+
const defaultPuppeteerScreenRecorderOptions = {
|
17
|
+
followNewTab: true,
|
18
|
+
fps: 15,
|
19
|
+
quality: 100,
|
20
|
+
ffmpeg_Path: null,
|
21
|
+
videoFrame: {
|
22
|
+
width: null,
|
23
|
+
height: null,
|
24
|
+
},
|
25
|
+
aspectRatio: '4:3',
|
26
|
+
};
|
27
|
+
/**
|
28
|
+
* PuppeteerScreenRecorder class is responsible for managing the video
|
29
|
+
*
|
30
|
+
* ```typescript
|
31
|
+
* const screenRecorderOptions = {
|
32
|
+
* followNewTab: true,
|
33
|
+
* fps: 15,
|
34
|
+
* }
|
35
|
+
* const savePath = "./test/demo.mp4";
|
36
|
+
* const screenRecorder = new PuppeteerScreenRecorder(page, screenRecorderOptions);
|
37
|
+
* await screenRecorder.start(savePath);
|
38
|
+
* // some puppeteer action or test
|
39
|
+
* await screenRecorder.stop()
|
40
|
+
* ```
|
41
|
+
*/
|
42
|
+
class PuppeteerScreenRecorder {
|
43
|
+
constructor(page, options = {}) {
|
44
|
+
this.isScreenCaptureEnded = null;
|
45
|
+
this.options = Object.assign({}, defaultPuppeteerScreenRecorderOptions, options);
|
46
|
+
this.streamReader = new pageVideoStreamCollector_1.pageVideoStreamCollector(page, this.options);
|
47
|
+
this.page = page;
|
48
|
+
}
|
49
|
+
/**
|
50
|
+
* @ignore
|
51
|
+
*/
|
52
|
+
setupListeners() {
|
53
|
+
this.page.once('close', async () => await this.stop());
|
54
|
+
this.streamReader.on('pageScreenFrame', (pageScreenFrame) => {
|
55
|
+
this.streamWriter.insert(pageScreenFrame);
|
56
|
+
});
|
57
|
+
this.streamWriter.once('videoStreamWriterError', () => this.stop());
|
58
|
+
}
|
59
|
+
/**
|
60
|
+
* @ignore
|
61
|
+
*/
|
62
|
+
async ensureDirectoryExist(dirPath) {
|
63
|
+
return new Promise((resolve, reject) => {
|
64
|
+
try {
|
65
|
+
fs_1.default.mkdirSync(dirPath, { recursive: true });
|
66
|
+
return resolve(dirPath);
|
67
|
+
}
|
68
|
+
catch (error) {
|
69
|
+
reject(error);
|
70
|
+
}
|
71
|
+
});
|
72
|
+
}
|
73
|
+
/**
|
74
|
+
* @ignore
|
75
|
+
* @private
|
76
|
+
* @method startStreamReader
|
77
|
+
* @description start listening for video stream from the page.
|
78
|
+
* @returns PuppeteerScreenRecorder
|
79
|
+
*/
|
80
|
+
async startStreamReader() {
|
81
|
+
this.setupListeners();
|
82
|
+
await this.streamReader.start();
|
83
|
+
return this;
|
84
|
+
}
|
85
|
+
/**
|
86
|
+
* @public
|
87
|
+
* @method getRecordDuration
|
88
|
+
* @description return the total duration of the video recorded,
|
89
|
+
* 1. if this method is called before calling the stop method, it would be return the time till it has recorded.
|
90
|
+
* 2. if this method is called after stop method, it would give the total time for recording
|
91
|
+
* @returns total duration of video
|
92
|
+
*/
|
93
|
+
getRecordDuration() {
|
94
|
+
if (!this.streamWriter) {
|
95
|
+
return '00:00:00:00';
|
96
|
+
}
|
97
|
+
return this.streamWriter.duration;
|
98
|
+
}
|
99
|
+
/**
|
100
|
+
*
|
101
|
+
* @public
|
102
|
+
* @method start
|
103
|
+
* @param savePath accepts a path string to store the video
|
104
|
+
* @description Start the video capturing session
|
105
|
+
* @returns PuppeteerScreenRecorder
|
106
|
+
* @example
|
107
|
+
* ```
|
108
|
+
* const savePath = './test/demo.mp4'; //.mp4 is required
|
109
|
+
* await recorder.start(savePath);
|
110
|
+
* ```
|
111
|
+
*/
|
112
|
+
async start(savePath) {
|
113
|
+
await this.ensureDirectoryExist((0, path_1.dirname)(savePath));
|
114
|
+
this.streamWriter = new pageVideoStreamWriter_1.default(savePath, this.options);
|
115
|
+
return this.startStreamReader();
|
116
|
+
}
|
117
|
+
/**
|
118
|
+
*
|
119
|
+
* @public
|
120
|
+
* @method startStream
|
121
|
+
* @description Start the video capturing session in a stream
|
122
|
+
* @returns {PuppeteerScreenRecorder}
|
123
|
+
* @example
|
124
|
+
* ```
|
125
|
+
* const stream = new PassThrough();
|
126
|
+
* await recorder.startStream(stream);
|
127
|
+
* ```
|
128
|
+
*/
|
129
|
+
async startStream(stream) {
|
130
|
+
this.streamWriter = new pageVideoStreamWriter_1.default(stream, this.options);
|
131
|
+
return this.startStreamReader();
|
132
|
+
}
|
133
|
+
/**
|
134
|
+
* @public
|
135
|
+
* @method stop
|
136
|
+
* @description stop the video capturing session
|
137
|
+
* @returns indicate whether stop is completed correct or not, if true without any error else false.
|
138
|
+
*/
|
139
|
+
async stop() {
|
140
|
+
if (this.isScreenCaptureEnded !== null) {
|
141
|
+
return this.isScreenCaptureEnded;
|
142
|
+
}
|
143
|
+
await this.streamReader.stop();
|
144
|
+
this.isScreenCaptureEnded = await this.streamWriter.stop();
|
145
|
+
return this.isScreenCaptureEnded;
|
146
|
+
}
|
147
|
+
}
|
148
|
+
exports.PuppeteerScreenRecorder = PuppeteerScreenRecorder;
|
149
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiUHVwcGV0ZWVyU2NyZWVuUmVjb3JkZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvbGliL1B1cHBldGVlclNjcmVlblJlY29yZGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7OztBQUFBLDRDQUFvQjtBQUNwQiwrQkFBK0I7QUFLL0IseUVBQXNFO0FBRXRFLG9GQUE0RDtBQUU1RDs7OztHQUlHO0FBQ0gsTUFBTSxxQ0FBcUMsR0FBbUM7SUFDNUUsWUFBWSxFQUFFLElBQUk7SUFDbEIsR0FBRyxFQUFFLEVBQUU7SUFDUCxPQUFPLEVBQUUsR0FBRztJQUNaLFdBQVcsRUFBRSxJQUFJO0lBQ2pCLFVBQVUsRUFBRTtRQUNWLEtBQUssRUFBRSxJQUFJO1FBQ1gsTUFBTSxFQUFFLElBQUk7S0FDYjtJQUNELFdBQVcsRUFBRSxLQUFLO0NBQ25CLENBQUM7QUFFRjs7Ozs7Ozs7Ozs7Ozs7R0FjRztBQUNILE1BQWEsdUJBQXVCO0lBT2xDLFlBQVksSUFBVSxFQUFFLE9BQU8sR0FBRyxFQUFFO1FBRjVCLHlCQUFvQixHQUFtQixJQUFJLENBQUM7UUFHbEQsSUFBSSxDQUFDLE9BQU8sR0FBRyxNQUFNLENBQUMsTUFBTSxDQUMxQixFQUFFLEVBQ0YscUNBQXFDLEVBQ3JDLE9BQU8sQ0FDUixDQUFDO1FBQ0YsSUFBSSxDQUFDLFlBQVksR0FBRyxJQUFJLG1EQUF3QixDQUFDLElBQUksRUFBRSxJQUFJLENBQUMsT0FBTyxDQUFDLENBQUM7UUFDckUsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7SUFDbkIsQ0FBQztJQUVEOztPQUVHO0lBQ0ssY0FBYztRQUNwQixJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsS0FBSyxJQUFJLEVBQUUsQ0FBQyxNQUFNLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO1FBRXZELElBQUksQ0FBQyxZQUFZLENBQUMsRUFBRSxDQUFDLGlCQUFpQixFQUFFLENB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
|