pw-repl 0.8.1 → 0.8.2
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/lib/emulation.js +1 -1
- package/lib/help.js +3 -1
- package/lib/record.js +53 -2
- package/package.json +1 -1
package/lib/emulation.js
CHANGED
|
@@ -220,4 +220,4 @@ const commands = {
|
|
|
220
220
|
},
|
|
221
221
|
};
|
|
222
222
|
|
|
223
|
-
module.exports = { commands, viewportText, EMULATE_KINDS, emulatedKinds, deviceMetrics, viewportMetrics, setEmulation, setViewport };
|
|
223
|
+
module.exports = { commands, viewportText, EMULATE_KINDS, emulatedKinds, deviceMetrics, viewportMetrics, setEmulation, setViewport, settled };
|
package/lib/help.js
CHANGED
|
@@ -378,7 +378,9 @@ odd width or height loses a pixel), a phone's too under emulate mobile: a Pixel
|
|
|
378
378
|
sharp than the phone draws it. It has no pointer (cursor on draws one) and no browser toolbars,
|
|
379
379
|
and a tab behind another, hidden or headless keeps recording. Set the size first with viewport
|
|
380
380
|
(1280x720 fills a 16:9 player); a page that changes size while recording is fitted into the first size.
|
|
381
|
-
Chrome sends a frame as the page draws, and a still page keeps its real length.
|
|
381
|
+
Chrome sends a frame as the page draws, and a still page keeps its real length. Under viewport or
|
|
382
|
+
emulate, a page the tab loads while recording holds the last frame until the new page's HTML is in,
|
|
383
|
+
and is then recorded at the viewport's size.
|
|
382
384
|
|
|
383
385
|
While a tab records, type sends keys at a pace the video can show (help type), and mousewheel scrolls in
|
|
384
386
|
small eased steps over up to 1s rather than at once; fill still sets the whole
|
package/lib/record.js
CHANGED
|
@@ -8,6 +8,8 @@ const out = require('./output');
|
|
|
8
8
|
const { tabState } = require('./tabstate');
|
|
9
9
|
const { clock, NO_TAB, STOPPED_RECORDING, ENDED_UNSEEN } = require('./util');
|
|
10
10
|
const { shellWords, splitSelector } = require('./syntax');
|
|
11
|
+
const { keptSession } = require('./cdp');
|
|
12
|
+
const { deviceMetrics, viewportMetrics, settled } = require('./emulation');
|
|
11
13
|
|
|
12
14
|
const FPS = 25;
|
|
13
15
|
const MAX_SECONDS = 3600;
|
|
@@ -126,6 +128,17 @@ function recordingOf(p) {
|
|
|
126
128
|
return tabState(p).recording || null;
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
async function restoreMetrics(p, rec) {
|
|
132
|
+
if (rec?.finishing) return;
|
|
133
|
+
const tab = tabState(p);
|
|
134
|
+
if (!tab.viewport && !tab.emulation?.device) return;
|
|
135
|
+
const kept = await keptSession(p);
|
|
136
|
+
if (rec?.finishing) return;
|
|
137
|
+
if (tab.viewport) await viewportMetrics(kept, tab.viewport);
|
|
138
|
+
else if (tab.emulation?.device) await deviceMetrics(kept, tab.emulation.device);
|
|
139
|
+
await settled(p);
|
|
140
|
+
}
|
|
141
|
+
|
|
129
142
|
// The tabs recording now, for the modes they count as.
|
|
130
143
|
function recordingPages() {
|
|
131
144
|
if (!state.browser) return [];
|
|
@@ -137,12 +150,17 @@ async function startRecording(p, file, seconds, format) {
|
|
|
137
150
|
// As screenshot does: a tab that has not been in front since it was emulated as a phone can
|
|
138
151
|
// be drawn at the window's size.
|
|
139
152
|
await p.bringToFront();
|
|
153
|
+
// Bringing an emulated tab to the front can leave Chrome's capture surface at the window's size
|
|
154
|
+
// after its next navigation. Reapply the active screen metrics on the kept session before measuring
|
|
155
|
+
// or starting this recording, then wait for the page to draw at them.
|
|
156
|
+
await restoreMetrics(p);
|
|
140
157
|
// Its own session: Chrome keeps one screencast per session, and the kept session's keeps the tab drawing.
|
|
141
158
|
const session = await p.context().newCDPSession(p);
|
|
142
159
|
const rec = {
|
|
143
160
|
page: p, file, format, exe, session, owner: state.client, startedAt: Date.now(), firstFrameAt: null,
|
|
144
161
|
size: null, resizedAt: null, written: 0, dropped: 0, latest: null, ff: null, timer: null, limit: null, finishing: null,
|
|
145
162
|
ffmpegError: '', starting: true, scale: 1, steps: [], step: null, stepsFile: null,
|
|
163
|
+
holdFrames: false, restarting: false, navigationVersion: 0, restoreError: null, mainFrameId: null, screencastParams: null,
|
|
146
164
|
};
|
|
147
165
|
// The size is the page's as the screen shows it, in CSS pixels (a phone's too, whose frames come in
|
|
148
166
|
// its device pixels), and Chrome scales each frame to fit it. Not the first frame's: just after a
|
|
@@ -150,11 +168,39 @@ async function startRecording(p, file, seconds, format) {
|
|
|
150
168
|
// frames have them (innerWidth has them; the viewport's clientWidth does not).
|
|
151
169
|
const even = n => Math.max(2, Math.round(n) - (Math.round(n) % 2));
|
|
152
170
|
try {
|
|
171
|
+
await session.send('Page.enable');
|
|
172
|
+
const tree = await session.send('Page.getFrameTree');
|
|
173
|
+
rec.mainFrameId = tree.frameTree.frame.id;
|
|
174
|
+
// Full document navigations reset Chrome's capture surface; SPA route changes do not.
|
|
175
|
+
session.on('Page.frameNavigated', ({ frame }) => {
|
|
176
|
+
if (frame.id !== rec.mainFrameId || rec.finishing) return;
|
|
177
|
+
rec.navigationVersion += 1;
|
|
178
|
+
const tab = tabState(p);
|
|
179
|
+
rec.holdFrames = !!(tab.viewport || tab.emulation?.device);
|
|
180
|
+
});
|
|
181
|
+
session.on('Page.domContentEventFired', async () => {
|
|
182
|
+
if (!rec.holdFrames || rec.finishing) return;
|
|
183
|
+
const version = rec.navigationVersion;
|
|
184
|
+
try {
|
|
185
|
+
await restoreMetrics(p, rec);
|
|
186
|
+
if (rec.finishing || version !== rec.navigationVersion) return;
|
|
187
|
+
await session.send('Page.stopScreencast');
|
|
188
|
+
if (rec.finishing || version !== rec.navigationVersion) return;
|
|
189
|
+
rec.holdFrames = false;
|
|
190
|
+
rec.restarting = true;
|
|
191
|
+
await session.send('Page.startScreencast', rec.screencastParams);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (rec.finishing || p.isClosed() || version !== rec.navigationVersion) return;
|
|
194
|
+
rec.restoreError = error.message;
|
|
195
|
+
out.notice(await finishRecording(rec, `Could not restore the recording size: ${error.message}`), p);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
153
198
|
const { cssVisualViewport: vv } = await session.send('Page.getLayoutMetrics');
|
|
154
199
|
const { result } = await session.send('Runtime.evaluate', { expression: '[innerWidth, innerHeight]', returnByValue: true });
|
|
155
200
|
const [width, height] = result.value;
|
|
156
201
|
rec.scale = vv.scale || 1;
|
|
157
202
|
rec.size = { width: even(width * rec.scale), height: even(height * rec.scale) };
|
|
203
|
+
rec.screencastParams = { format: 'jpeg', quality: 85, maxWidth: rec.size.width, maxHeight: rec.size.height };
|
|
158
204
|
} catch (error) {
|
|
159
205
|
session.detach().catch(() => {});
|
|
160
206
|
throw error;
|
|
@@ -180,6 +226,11 @@ async function startRecording(p, file, seconds, format) {
|
|
|
180
226
|
if (!size) return;
|
|
181
227
|
// A frame the size of the video, to a pixel (Chrome scales it to fit, keeping its shape).
|
|
182
228
|
const fits = Math.abs(size.width - rec.size.width) <= 1 && Math.abs(size.height - rec.size.height) <= 1;
|
|
229
|
+
if (rec.restarting && rec.latest) {
|
|
230
|
+
if (!fits) { tick(); return; }
|
|
231
|
+
rec.restarting = false;
|
|
232
|
+
}
|
|
233
|
+
if (rec.holdFrames && rec.latest) { tick(); return; }
|
|
183
234
|
if (!rec.ff) {
|
|
184
235
|
rec.firstFrameAt = Date.now();
|
|
185
236
|
rec.ff = spawnFfmpeg(rec);
|
|
@@ -195,7 +246,7 @@ async function startRecording(p, file, seconds, format) {
|
|
|
195
246
|
rec.latest = image;
|
|
196
247
|
});
|
|
197
248
|
try {
|
|
198
|
-
await session.send('Page.startScreencast',
|
|
249
|
+
await session.send('Page.startScreencast', rec.screencastParams);
|
|
199
250
|
} catch (error) {
|
|
200
251
|
delete tabState(p).recording;
|
|
201
252
|
session.detach().catch(() => {});
|
|
@@ -263,7 +314,7 @@ function finishRecording(rec, why = null, { shuttingDown = false } = {}) {
|
|
|
263
314
|
const text = typeof result === 'string' ? result : result.text;
|
|
264
315
|
// A recording that saved nothing, or that ffmpeg cut short, makes record off fail rather than
|
|
265
316
|
// report it as done.
|
|
266
|
-
rec.failed = typeof result !== 'string' || !!rec.exitedEarly;
|
|
317
|
+
rec.failed = typeof result !== 'string' || !!rec.exitedEarly || !!rec.restoreError;
|
|
267
318
|
const said = why ? `${why}. ${text}` : text;
|
|
268
319
|
// A failure is kept unreported until record off (or record on) has failed with it. One that
|
|
269
320
|
// ended by itself is unseen until record shows it, and the REPL stopping logs it for stop.
|