@browserless/capture 10.11.4 → 10.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -7
- package/extension/offscreen.js +11 -5
- package/package.json +2 -2
- package/src/capture.js +26 -74
- package/src/constants.js +2 -12
- package/src/index.js +9 -3
package/README.md
CHANGED
|
@@ -57,7 +57,6 @@ const video = await capture(page)('https://example.com', {
|
|
|
57
57
|
duration: 5000,
|
|
58
58
|
type: 'mp4',
|
|
59
59
|
codec: 'avc1.4D401F',
|
|
60
|
-
quality: 'high',
|
|
61
60
|
path: '/tmp/demo.mp4'
|
|
62
61
|
})
|
|
63
62
|
|
|
@@ -88,7 +87,6 @@ Returns a `Buffer` and writes to `opts.path` when provided.
|
|
|
88
87
|
| --- | --- | --- | --- |
|
|
89
88
|
| `type` | `'webm' \| 'mp4'` | `'mp4'` | Output type selector mapped to MediaRecorder mime type. |
|
|
90
89
|
| `codec` | `string` | Depends on `type` | MediaRecorder codec override. Defaults: `webm -> vp9`, `mp4 -> avc1.4D401F`. |
|
|
91
|
-
| `quality` | `'extra-high' \| 'high' \| 'medium' \| 'low' \| 'extra-low'` | `'high'` | Video quality hint mapped to `MediaRecorder.videoBitsPerSecond`. |
|
|
92
90
|
| `path` | `string` | `undefined` | Write the captured media to disk. |
|
|
93
91
|
| `duration` | `number` | `3000` | Capture duration in milliseconds. |
|
|
94
92
|
| `audio` | `boolean \| object` | `false` | Capture audio. When object, it is used as audio track constraints. |
|
|
@@ -99,16 +97,11 @@ Returns a `Buffer` and writes to `opts.path` when provided.
|
|
|
99
97
|
- `capture.extensionPath`: Absolute path to the bundled extension.
|
|
100
98
|
- `capture.extensionId`: Extension ID used by the package.
|
|
101
99
|
- `capture.types`: Supported values for `type`.
|
|
102
|
-
- `capture.qualities`: Supported values for `quality`.
|
|
103
|
-
|
|
104
100
|
`capture` uses `goto(...).device.viewport` as the capture viewport source.
|
|
105
101
|
When `video` is `true` or omitted, video constraints are inferred from that viewport to keep capture framing aligned with screenshot/pdf rendering.
|
|
106
102
|
When `video` is an object, that object is used as the video constraints.
|
|
107
103
|
When `audio` is an object, that object is used as the audio constraints.
|
|
108
104
|
The inferred constraints also account for `deviceScaleFactor`, so output video pixels match screenshot pixel density.
|
|
109
|
-
Capture always enforces `videoConstraints.mandatory.maxFrameRate = 120`.
|
|
110
|
-
`quality` maps to bitrate presets (`extra-high`: 20Mbps, `high`: 8Mbps, `medium`: 5Mbps, `low`: 2.5Mbps, `extra-low`: 1Mbps).
|
|
111
|
-
MediaRecorder chunk size is internal and fixed at `250ms`.
|
|
112
105
|
`type` is mapped internally to the MediaRecorder mime type, and `codec` is appended as `;codecs=...`.
|
|
113
106
|
Default codecs are `vp9` for `webm` and `avc1.4D401F` for `mp4`.
|
|
114
107
|
You can override codec per request using `opts.codec`.
|
package/extension/offscreen.js
CHANGED
|
@@ -76,11 +76,10 @@ const START_RECORDING = async ({
|
|
|
76
76
|
streamId,
|
|
77
77
|
video,
|
|
78
78
|
audio,
|
|
79
|
-
frameSize,
|
|
80
79
|
mimeType,
|
|
81
|
-
recorderOptions,
|
|
82
80
|
videoConstraints,
|
|
83
|
-
audioConstraints
|
|
81
|
+
audioConstraints,
|
|
82
|
+
duration = 0
|
|
84
83
|
}) => {
|
|
85
84
|
if (!port) throw new Error('Missing websocket port for recording session.')
|
|
86
85
|
if (!streamId) throw new Error('Missing tab media stream id for recording session.')
|
|
@@ -125,7 +124,7 @@ const START_RECORDING = async ({
|
|
|
125
124
|
throw error
|
|
126
125
|
}
|
|
127
126
|
|
|
128
|
-
const recorder = new MediaRecorder(stream, { mimeType
|
|
127
|
+
const recorder = new MediaRecorder(stream, { mimeType })
|
|
129
128
|
|
|
130
129
|
const pending = new Set()
|
|
131
130
|
|
|
@@ -157,12 +156,19 @@ const START_RECORDING = async ({
|
|
|
157
156
|
}
|
|
158
157
|
|
|
159
158
|
recorders[index] = recorder
|
|
160
|
-
recorder.start(
|
|
159
|
+
recorder.start()
|
|
160
|
+
|
|
161
|
+
if (duration > 0) {
|
|
162
|
+
recorder.__autoStopTimer = setTimeout(() => {
|
|
163
|
+
if (recorder.state !== 'inactive') recorder.stop()
|
|
164
|
+
}, duration)
|
|
165
|
+
}
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
const STOP_RECORDING = index => {
|
|
164
169
|
const recorder = recorders[index]
|
|
165
170
|
if (!recorder || recorder.state === 'inactive') return
|
|
171
|
+
clearTimeout(recorder.__autoStopTimer)
|
|
166
172
|
recorder.stop()
|
|
167
173
|
}
|
|
168
174
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@browserless/capture",
|
|
3
3
|
"description": "Record a Puppeteer page using tabCapture API",
|
|
4
4
|
"homepage": "https://browserless.js.org/#/?id=capturepage-options",
|
|
5
|
-
"version": "10.11.
|
|
5
|
+
"version": "10.11.6",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"author": {
|
|
8
8
|
"email": "hello@microlink.io",
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"timeout": "2m",
|
|
55
55
|
"workerThreads": false
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "5f07174e086cac8bd67e22ad0f5ff89326dc39a8"
|
|
58
58
|
}
|
package/src/capture.js
CHANGED
|
@@ -7,15 +7,7 @@ const debug = require('debug-logfmt')('browserless:capture')
|
|
|
7
7
|
const { closeServer, createWebSocketServer } = require('./util')
|
|
8
8
|
const extension = require('./extension')
|
|
9
9
|
|
|
10
|
-
const {
|
|
11
|
-
DEFAULT,
|
|
12
|
-
DEFAULT_CODEC_BY_TYPE,
|
|
13
|
-
INTERNAL_FRAME_SIZE,
|
|
14
|
-
MAX_FRAME_RATE,
|
|
15
|
-
NOOP,
|
|
16
|
-
QUALITIES,
|
|
17
|
-
VIDEO_BITS_PER_SECOND_BY_QUALITY
|
|
18
|
-
} = require('./constants')
|
|
10
|
+
const { DEFAULT, DEFAULT_CODEC_BY_TYPE, NOOP } = require('./constants')
|
|
19
11
|
|
|
20
12
|
let currentIndex = 0
|
|
21
13
|
|
|
@@ -77,24 +69,6 @@ const MIME_TYPES_BY_TYPE = Object.freeze({
|
|
|
77
69
|
|
|
78
70
|
const SUPPORTED_TYPES = Object.freeze(Object.keys(MIME_TYPES_BY_TYPE))
|
|
79
71
|
|
|
80
|
-
const getQuality = quality => {
|
|
81
|
-
const normalizedQuality =
|
|
82
|
-
quality === undefined || quality === null
|
|
83
|
-
? DEFAULT.quality
|
|
84
|
-
: String(quality)
|
|
85
|
-
.trim()
|
|
86
|
-
.toLowerCase()
|
|
87
|
-
.replace(/[\s_]+/g, '-')
|
|
88
|
-
|
|
89
|
-
if (!QUALITIES.includes(normalizedQuality)) {
|
|
90
|
-
throw new TypeError(
|
|
91
|
-
`Unsupported \`quality\` "${quality}". Supported qualities: ${QUALITIES.join(', ')}.`
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return normalizedQuality
|
|
96
|
-
}
|
|
97
|
-
|
|
98
72
|
const getCodec = ({ codec, type, video }) => {
|
|
99
73
|
if (codec === undefined || codec === null) {
|
|
100
74
|
return video ? DEFAULT_CODEC_BY_TYPE[type] : undefined
|
|
@@ -133,35 +107,20 @@ const getMimeType = ({ type, audio, video, codec }) => {
|
|
|
133
107
|
}
|
|
134
108
|
|
|
135
109
|
const getVideoConstraints = (videoConstraints, viewport) => {
|
|
136
|
-
|
|
137
|
-
const source = constraints && typeof constraints === 'object' ? constraints : {}
|
|
138
|
-
const mandatory =
|
|
139
|
-
source.mandatory && typeof source.mandatory === 'object' ? source.mandatory : {}
|
|
140
|
-
const { mandatory: _mandatory, ...rest } = source
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
...rest,
|
|
144
|
-
mandatory: {
|
|
145
|
-
...mandatory,
|
|
146
|
-
maxFrameRate: MAX_FRAME_RATE
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (videoConstraints) return withMaxFrameRate(videoConstraints)
|
|
110
|
+
if (videoConstraints) return videoConstraints
|
|
152
111
|
|
|
153
112
|
const dpr = Math.max(Number(viewport.deviceScaleFactor) || 1, 1)
|
|
154
113
|
const width = Math.round(viewport.width * dpr)
|
|
155
114
|
const height = Math.round(viewport.height * dpr)
|
|
156
115
|
|
|
157
|
-
return
|
|
116
|
+
return {
|
|
158
117
|
mandatory: {
|
|
159
118
|
minWidth: width,
|
|
160
119
|
minHeight: height,
|
|
161
120
|
maxWidth: width,
|
|
162
121
|
maxHeight: height
|
|
163
122
|
}
|
|
164
|
-
}
|
|
123
|
+
}
|
|
165
124
|
}
|
|
166
125
|
|
|
167
126
|
const isTrackObject = value => value && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -215,7 +174,7 @@ const getTargetId = async page => {
|
|
|
215
174
|
}
|
|
216
175
|
|
|
217
176
|
module.exports = async (page, opts, viewport) => {
|
|
218
|
-
const { path: outputPath, duration = DEFAULT.duration, audio, video, type,
|
|
177
|
+
const { path: outputPath, duration = DEFAULT.duration, audio, video, type, codec } = opts
|
|
219
178
|
|
|
220
179
|
const audioOpts = getOpts(audio, false, 'audio')
|
|
221
180
|
const videoOpts = getOpts(video, true, 'video')
|
|
@@ -234,11 +193,6 @@ module.exports = async (page, opts, viewport) => {
|
|
|
234
193
|
video: videoOpts.enabled
|
|
235
194
|
})
|
|
236
195
|
|
|
237
|
-
const resolvedQuality = getQuality(quality)
|
|
238
|
-
const recorderOptions = videoOpts.enabled
|
|
239
|
-
? { videoBitsPerSecond: VIDEO_BITS_PER_SECOND_BY_QUALITY[resolvedQuality] }
|
|
240
|
-
: undefined
|
|
241
|
-
|
|
242
196
|
const resolvedVideoConstraints = getVideoConstraints(videoOpts.constraints, viewport)
|
|
243
197
|
|
|
244
198
|
let worker
|
|
@@ -292,11 +246,10 @@ module.exports = async (page, opts, viewport) => {
|
|
|
292
246
|
index,
|
|
293
247
|
port,
|
|
294
248
|
tabId,
|
|
249
|
+
duration,
|
|
295
250
|
video: videoOpts.enabled,
|
|
296
251
|
audio: audioOpts.enabled,
|
|
297
|
-
frameSize: INTERNAL_FRAME_SIZE,
|
|
298
252
|
mimeType: streamMimeType,
|
|
299
|
-
recorderOptions,
|
|
300
253
|
videoConstraints: resolvedVideoConstraints,
|
|
301
254
|
audioConstraints: audioOpts.constraints
|
|
302
255
|
}
|
|
@@ -304,7 +257,6 @@ module.exports = async (page, opts, viewport) => {
|
|
|
304
257
|
{ index, tabId }
|
|
305
258
|
)
|
|
306
259
|
isRecordingStarted = true
|
|
307
|
-
await runWithDuration('durationwait', () => setTimeout(duration), { duration })
|
|
308
260
|
} catch (error) {
|
|
309
261
|
if (!worker && workerPromise) {
|
|
310
262
|
worker = await runWithDuration('awaitWorkerAfterError', () => workerPromise.catch(NOOP))
|
|
@@ -317,33 +269,33 @@ module.exports = async (page, opts, viewport) => {
|
|
|
317
269
|
}
|
|
318
270
|
captureError = error
|
|
319
271
|
} finally {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
'extension.stopRecording',
|
|
323
|
-
() => extension.stopRecording({ extension: worker, index }).catch(NOOP),
|
|
324
|
-
{ index }
|
|
325
|
-
)
|
|
326
|
-
: Promise.resolve()
|
|
327
|
-
|
|
328
|
-
if (recordingPromise) {
|
|
329
|
-
if (isRecordingStarted) {
|
|
272
|
+
try {
|
|
273
|
+
if (recordingPromise && isRecordingStarted) {
|
|
330
274
|
const recordingResultPromise = recordingPromise.catch(error => {
|
|
331
275
|
if (!captureError) throw error
|
|
332
276
|
return Buffer.alloc(0)
|
|
333
277
|
})
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
278
|
+
const safetyTimeoutMs = Math.ceil(duration * 1.5)
|
|
279
|
+
const recordingWithTimeout = Promise.race([
|
|
280
|
+
recordingResultPromise,
|
|
281
|
+
setTimeout(safetyTimeoutMs).then(() => {
|
|
282
|
+
throw new Error('Recording timed out')
|
|
283
|
+
})
|
|
284
|
+
])
|
|
285
|
+
buffer = await runWithDuration('recordingPromise', () => recordingWithTimeout)
|
|
286
|
+
} else if (recordingPromise) {
|
|
339
287
|
recordingPromise.catch(NOOP)
|
|
340
|
-
await runWithDuration('stopWithoutRecording', () => stopPromise)
|
|
341
288
|
}
|
|
342
|
-
}
|
|
343
|
-
|
|
289
|
+
} finally {
|
|
290
|
+
if (worker) {
|
|
291
|
+
await runWithDuration(
|
|
292
|
+
'extension.stopRecording',
|
|
293
|
+
() => extension.stopRecording({ extension: worker, index }).catch(NOOP),
|
|
294
|
+
{ index }
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
await runWithDuration('closeServer', () => closeServer(wss))
|
|
344
298
|
}
|
|
345
|
-
|
|
346
|
-
await runWithDuration('closeServer', () => closeServer(wss))
|
|
347
299
|
}
|
|
348
300
|
|
|
349
301
|
if (captureError) throw captureError
|
package/src/constants.js
CHANGED
|
@@ -3,23 +3,13 @@
|
|
|
3
3
|
const path = require('path')
|
|
4
4
|
|
|
5
5
|
module.exports = {
|
|
6
|
-
|
|
7
|
-
DEFAULT: Object.freeze({ duration: 3000, type: 'mp4', quality: 'high' }),
|
|
6
|
+
DEFAULT: Object.freeze({ duration: 3000, type: 'mp4' }),
|
|
8
7
|
DEFAULT_CODEC_BY_TYPE: Object.freeze({
|
|
9
8
|
webm: 'vp9',
|
|
10
|
-
mp4: 'avc1.
|
|
9
|
+
mp4: 'avc1.640028'
|
|
11
10
|
}),
|
|
12
11
|
EXTENSION_ID: 'jjndjgheafjngoipoacpjgeicjeomjli',
|
|
13
12
|
EXTENSION_PATH: path.join(__dirname, '..', 'extension'),
|
|
14
13
|
TYPES: Object.freeze(['webm', 'mp4']),
|
|
15
|
-
QUALITIES: Object.freeze(['extra-high', 'high', 'medium', 'low', 'extra-low']),
|
|
16
|
-
VIDEO_BITS_PER_SECOND_BY_QUALITY: Object.freeze({
|
|
17
|
-
'extra-high': 20_000_000,
|
|
18
|
-
high: 8_000_000,
|
|
19
|
-
medium: 5_000_000,
|
|
20
|
-
low: 2_500_000,
|
|
21
|
-
'extra-low': 1_000_000
|
|
22
|
-
}),
|
|
23
|
-
INTERNAL_FRAME_SIZE: 250,
|
|
24
14
|
NOOP: () => {}
|
|
25
15
|
}
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
const debug = require('debug-logfmt')('browserless:capture')
|
|
4
4
|
const createGoto = require('@browserless/goto')
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
DEFAULT,
|
|
8
|
+
DEFAULT_CODEC_BY_TYPE,
|
|
9
|
+
EXTENSION_ID,
|
|
10
|
+
EXTENSION_PATH,
|
|
11
|
+
TYPES
|
|
12
|
+
} = require('./constants')
|
|
7
13
|
const runCapture = require('./capture')
|
|
8
14
|
|
|
9
15
|
module.exports = ({ goto, ...gotoOpts } = {}) => {
|
|
@@ -21,6 +27,6 @@ module.exports = ({ goto, ...gotoOpts } = {}) => {
|
|
|
21
27
|
|
|
22
28
|
module.exports.extensionPath = EXTENSION_PATH
|
|
23
29
|
module.exports.extensionId = EXTENSION_ID
|
|
24
|
-
module.exports.
|
|
25
|
-
module.exports.qualities = QUALITIES
|
|
30
|
+
module.exports.TYPES = TYPES
|
|
26
31
|
module.exports.DEFAULT = DEFAULT
|
|
32
|
+
module.exports.DEFAULT_CODEC_BY_TYPE = DEFAULT_CODEC_BY_TYPE
|