diff --git a/packages/capture/src/create-capture.js b/packages/capture/src/create-capture.js index 628bef33d..c17caaf88 100644 --- a/packages/capture/src/create-capture.js +++ b/packages/capture/src/create-capture.js @@ -3,17 +3,24 @@ const debug = require('debug-logfmt')('browserless:capture') const createGoto = require('@browserless/goto') -// Resolve the device (and therefore the viewport) without navigating, so the -// recorder can be sized and started before `goto` runs. The viewport is derived -// from the `device`/`viewport` opts, which are known ahead of navigation. -const resolveViewport = (goto, page, opts) => { +// Resolve the device viewport and apply it before recording starts. The +// viewport is derived from the `device`/`viewport` opts (known ahead of +// navigation), so the recorder can be sized and started before `goto` runs. +// Applying it up front matters for tab capture: `getUserMedia` pins exact +// width/height from the target device, so the page must already match it — +// otherwise a custom `device` overconstrains against the browser's default +// viewport and fails before navigation begins. +const prepareViewport = async (goto, page, opts) => { if (typeof goto.getDevice === 'function') { const device = goto.getDevice({ headers: { ...(opts.headers || {}) }, device: opts.device ?? goto.defaultDevice, viewport: opts.viewport }) - if (device && device.viewport) return device.viewport + if (device && device.viewport) { + await page.setViewport(device.viewport) + return device.viewport + } } return page.viewport() } @@ -30,7 +37,7 @@ module.exports = return function capture (page) { return async (url, opts = {}) => { const duration = debug.duration({ url }) - const viewport = resolveViewport(goto, page, opts) + const viewport = await prepareViewport(goto, page, opts) const result = await runMode(page, opts, viewport, { onStarted: () => goto(page, { ...opts, url, animations: true }) }) diff --git a/packages/capture/src/recorder/index.js b/packages/capture/src/recorder/index.js index 069118b63..7337d124c 100644 --- a/packages/capture/src/recorder/index.js +++ b/packages/capture/src/recorder/index.js @@ -96,9 +96,9 @@ const record = async (page, opts, viewport, { onStarted, getSize, startSource, l // error surfaces immediately instead of after the full `duration`. const recordingWindow = new AbortController() try { - // Apply the viewport before the first frame so frames are captured at the - // intended size from the start (goto re-applies it idempotently during nav). - await page.setViewport(viewport).catch(NOOP) + // The viewport is already applied (by `prepareViewport`) before the first + // frame, so frames are captured at the intended size from the start; goto + // re-applies it idempotently during navigation. stop = await startSource({ page, muxer, width, height, fps, quality }) // The source is rolling: only now kick off navigation so the page's load and diff --git a/packages/capture/test/index.js b/packages/capture/test/index.js index 78535dfb1..35d938044 100644 --- a/packages/capture/test/index.js +++ b/packages/capture/test/index.js @@ -386,6 +386,41 @@ test('supports a custom `fps`', async t => { t.is(startRecordingPayload.videoConstraints.mandatory.maxFrameRate, 60) }) +test('applies the device viewport before recording starts', async t => { + const createCapture = loadCapture() + + const deviceViewport = { + width: 390, + height: 844, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false + } + + const { page } = createFixture() + const browser = page.browser() + + // Tab capture pins exact width/height from the device, so `getUserMedia` (run + // inside START_RECORDING) overconstrains unless the page already matches. Snap + // the viewport at that moment to prove it was applied before recording begins. + let viewportAtRecordingStart + browser.__setOnStartRecording(() => { + viewportAtRecordingStart = page.viewport() + }) + + const capture = createCapture({ + goto: createGoto(undefined, { + device: { ...DEFAULT_DEVICE, viewport: deviceViewport } + }) + }) + + await capture(page)('https://example.com', { duration: 20, audio: false, video: true }) + + t.deepEqual(viewportAtRecordingStart, deviceViewport) + t.deepEqual(page.viewport(), deviceViewport) +}) + test('starts recording before navigation', async t => { const createCapture = loadCapture() const events = []