@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 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`.
@@ -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, ...recorderOptions })
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(frameSize)
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.4",
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": "9ff2a0f0dad20d64a5c47a04a989ee77d1ee5ae8"
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
- const withMaxFrameRate = constraints => {
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 withMaxFrameRate({
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, quality, codec } = opts
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
- const stopPromise = worker
321
- ? runWithDuration(
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 [, recordingResult] = await runWithDuration('stop+recordingPromise', () =>
335
- Promise.all([stopPromise, recordingResultPromise])
336
- )
337
- buffer = recordingResult
338
- } else {
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
- } else {
343
- await runWithDuration('stopWithoutPromise', () => stopPromise)
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
- MAX_FRAME_RATE: 120,
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.4D401F'
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 { DEFAULT, EXTENSION_ID, EXTENSION_PATH, QUALITIES, TYPES } = require('./constants')
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.types = TYPES
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