browsertime 16.16.0 → 17.0.0-beta.0
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/CHANGELOG.md +52 -0
- package/bin/browsertime.js +43 -43
- package/bin/browsertimeWebPageReplay.js +10 -9
- package/browserscripts/pageinfo/generator.js +1 -1
- package/index.js +14 -13
- package/lib/android/gnirehtet.js +14 -15
- package/lib/android/index.js +92 -89
- package/lib/android/root.js +6 -9
- package/lib/chrome/chromeDevtoolsProtocol.js +46 -45
- package/lib/chrome/har.js +12 -13
- package/lib/chrome/longTaskMetrics.js +3 -5
- package/lib/chrome/parseCpuTrace.js +13 -10
- package/lib/chrome/settings/chromeAndroidOptions.js +1 -3
- package/lib/chrome/settings/chromeDesktopOptions.js +1 -3
- package/lib/chrome/settings/traceCategories.js +1 -3
- package/lib/chrome/traceCategoriesParser.js +35 -36
- package/lib/chrome/webdriver/builder.js +18 -26
- package/lib/chrome/webdriver/chromium.js +56 -46
- package/lib/chrome/webdriver/setupChromiumOptions.js +32 -27
- package/lib/connectivity/humble.js +13 -14
- package/lib/connectivity/index.js +74 -77
- package/lib/connectivity/trafficShapeParser.js +31 -35
- package/lib/core/engine/collector.js +25 -22
- package/lib/core/engine/command/addText.js +24 -27
- package/lib/core/engine/command/android.js +7 -11
- package/lib/core/engine/command/cache.js +3 -4
- package/lib/core/engine/command/chromeDevToolsProtocol.js +20 -20
- package/lib/core/engine/command/click.js +48 -49
- package/lib/core/engine/command/debug.js +3 -4
- package/lib/core/engine/command/javaScript.js +18 -19
- package/lib/core/engine/command/measure.js +31 -37
- package/lib/core/engine/command/meta.js +1 -4
- package/lib/core/engine/command/mouse/clickAndHold.js +36 -39
- package/lib/core/engine/command/mouse/contextClick.js +17 -18
- package/lib/core/engine/command/mouse/doubleClick.js +17 -19
- package/lib/core/engine/command/mouse/index.js +5 -9
- package/lib/core/engine/command/mouse/mouseMove.js +19 -23
- package/lib/core/engine/command/mouse/singleClick.js +15 -19
- package/lib/core/engine/command/navigation.js +12 -14
- package/lib/core/engine/command/screenshot.js +1 -4
- package/lib/core/engine/command/scroll.js +4 -7
- package/lib/core/engine/command/select.js +26 -26
- package/lib/core/engine/command/set.js +25 -23
- package/lib/core/engine/command/stopWatch.js +5 -8
- package/lib/core/engine/command/switch.js +18 -20
- package/lib/core/engine/command/util/lcpHighlightScript.js +1 -2
- package/lib/core/engine/command/util/lsHighlightScript.js +1 -2
- package/lib/core/engine/command/wait.js +18 -18
- package/lib/core/engine/index.js +81 -71
- package/lib/core/engine/iteration.js +93 -79
- package/lib/core/engine/run.js +5 -9
- package/lib/core/pageCompleteChecks/defaultPageCompleteCheck.js +1 -2
- package/lib/core/pageCompleteChecks/pageCompleteCheckByInactivity.js +1 -3
- package/lib/core/pageCompleteChecks/spaInactivity.js +1 -3
- package/lib/core/seleniumRunner.js +92 -100
- package/lib/core/webdriver/index.js +24 -21
- package/lib/edge/webdriver/builder.js +21 -26
- package/lib/extensionserver/httpServer.js +17 -21
- package/lib/extensionserver/index.js +8 -12
- package/lib/extensionserver/setup.js +12 -12
- package/lib/firefox/geckoProfiler.js +27 -21
- package/lib/firefox/getHAR.js +7 -8
- package/lib/firefox/memoryReport.js +9 -11
- package/lib/firefox/perfStats.js +5 -7
- package/lib/firefox/settings/disableSafeBrowsingPreferences.js +1 -3
- package/lib/firefox/settings/disableTrackingProtectionPreferences.js +1 -3
- package/lib/firefox/settings/firefoxPreferences.js +2 -4
- package/lib/firefox/settings/geckoProfilerDefaults.js +5 -9
- package/lib/firefox/webdriver/builder.js +66 -63
- package/lib/firefox/webdriver/firefox.js +43 -48
- package/lib/safari/webdriver/builder.js +4 -5
- package/lib/safari/webdriver/safari.js +4 -8
- package/lib/screenshot/defaults.js +1 -3
- package/lib/screenshot/index.js +24 -21
- package/lib/support/browserScript.js +65 -56
- package/lib/support/cli.js +123 -85
- package/lib/support/dns.js +6 -7
- package/lib/support/engineUtils.js +85 -38
- package/lib/support/errors.js +4 -13
- package/lib/support/fileUtil.js +81 -68
- package/lib/support/filters.js +15 -20
- package/lib/support/getViewPort.js +6 -7
- package/lib/support/har/index.js +208 -216
- package/lib/support/images/index.js +54 -26
- package/lib/support/logging.js +20 -17
- package/lib/support/pathToFolder.js +18 -23
- package/lib/support/preURL.js +5 -6
- package/lib/support/processes.js +4 -6
- package/lib/support/setResourceTimingBufferSize.js +2 -4
- package/lib/support/statistics.js +58 -56
- package/lib/support/stop.js +7 -8
- package/lib/support/storageManager.js +44 -44
- package/lib/support/tcpdump.js +22 -16
- package/lib/support/userTiming.js +7 -13
- package/lib/support/util.js +204 -224
- package/lib/support/xvfb.js +11 -13
- package/lib/video/defaults.js +6 -10
- package/lib/video/postprocessing/finetune/addTextToVideo.js +15 -15
- package/lib/video/postprocessing/finetune/convertFps.js +14 -8
- package/lib/video/postprocessing/finetune/getFont.js +7 -9
- package/lib/video/postprocessing/finetune/getTimingMetrics.js +19 -18
- package/lib/video/postprocessing/finetune/index.js +40 -36
- package/lib/video/postprocessing/finetune/removeOrange.js +10 -11
- package/lib/video/postprocessing/visualmetrics/extraMetrics.js +17 -13
- package/lib/video/postprocessing/visualmetrics/getVideoMetrics.js +12 -13
- package/lib/video/postprocessing/visualmetrics/visualMetrics.js +117 -128
- package/lib/video/screenRecording/android/recorder.js +12 -17
- package/lib/video/screenRecording/desktop/convert.js +13 -11
- package/lib/video/screenRecording/desktop/desktopRecorder.js +29 -24
- package/lib/video/screenRecording/desktop/ffmpegRecorder.js +60 -63
- package/lib/video/screenRecording/desktop/osx/getSPDisplaysDataType.js +4 -6
- package/lib/video/screenRecording/desktop/osx/getScreen.js +9 -10
- package/lib/video/screenRecording/firefox/firefoxWindowRecorder.js +54 -54
- package/lib/video/screenRecording/ios/convertToMp4.js +10 -11
- package/lib/video/screenRecording/ios/iosRecorder.js +23 -21
- package/lib/video/screenRecording/iosSimulator/convertToMp4.js +10 -11
- package/lib/video/screenRecording/iosSimulator/recorder.js +10 -9
- package/lib/video/screenRecording/recorder.js +8 -9
- package/lib/video/screenRecording/setOrangeBackground.js +5 -6
- package/lib/video/video.js +28 -34
- package/package.json +19 -17
|
@@ -1,24 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
import { promisify } from 'node:util';
|
|
2
|
+
import { unlink as _unlink, rename as _rename } from 'node:fs';
|
|
3
|
+
import intel from 'intel';
|
|
4
|
+
import get from 'lodash.get';
|
|
5
|
+
import { start as _start, stop as _stop } from './ffmpegRecorder.js';
|
|
6
|
+
import convert from './convert.js';
|
|
7
|
+
import {
|
|
8
|
+
xvfbDisplay,
|
|
9
|
+
framerate as _framerate,
|
|
10
|
+
crf as _crf,
|
|
11
|
+
convert as _convert,
|
|
12
|
+
threads as _threads
|
|
13
|
+
} from '../../defaults.js';
|
|
14
|
+
import getViewPort from '../../../support/getViewPort.js';
|
|
15
|
+
const unlink = promisify(_unlink);
|
|
16
|
+
const rename = promisify(_rename);
|
|
17
|
+
const log = intel.getLogger('browsertime.video');
|
|
2
18
|
|
|
3
|
-
|
|
4
|
-
const get = require('lodash.get');
|
|
5
|
-
const util = require('util');
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const ffmpegRecorder = require('./ffmpegRecorder');
|
|
8
|
-
const convert = require('./convert');
|
|
9
|
-
const defaults = require('../../defaults');
|
|
10
|
-
const getViewPort = require('../../../support/getViewPort');
|
|
11
|
-
const unlink = util.promisify(fs.unlink);
|
|
12
|
-
const rename = util.promisify(fs.rename);
|
|
13
|
-
|
|
14
|
-
module.exports = class DesktopRecorder {
|
|
19
|
+
export default class DesktopRecorder {
|
|
15
20
|
constructor(options) {
|
|
16
|
-
this.display = get(options, 'xvfbParams.display',
|
|
17
|
-
this.framerate = get(options, 'videoParams.framerate',
|
|
21
|
+
this.display = get(options, 'xvfbParams.display', xvfbDisplay);
|
|
22
|
+
this.framerate = get(options, 'videoParams.framerate', _framerate);
|
|
18
23
|
this.nice = get(options, 'videoParams.nice', 0);
|
|
19
|
-
this.crf = get(options, 'videoParams.crf',
|
|
20
|
-
this.convert = get(options, 'videoParams.convert',
|
|
21
|
-
this.threads = get(options, 'videoParams.threads',
|
|
24
|
+
this.crf = get(options, 'videoParams.crf', _crf);
|
|
25
|
+
this.convert = get(options, 'videoParams.convert', _convert);
|
|
26
|
+
this.threads = get(options, 'videoParams.threads', _threads);
|
|
22
27
|
this.viewPort = getViewPort(options);
|
|
23
28
|
this.origin = '0,0';
|
|
24
29
|
this.offset = { x: 0, y: 0 };
|
|
@@ -28,7 +33,7 @@ module.exports = class DesktopRecorder {
|
|
|
28
33
|
async start(file) {
|
|
29
34
|
this.filePath = file;
|
|
30
35
|
|
|
31
|
-
this.recording =
|
|
36
|
+
this.recording = _start({
|
|
32
37
|
display: this.display,
|
|
33
38
|
size: this.viewPort,
|
|
34
39
|
filePath: this.filePath,
|
|
@@ -45,14 +50,14 @@ module.exports = class DesktopRecorder {
|
|
|
45
50
|
|
|
46
51
|
async stop(destination) {
|
|
47
52
|
log.debug('Stop screen recording');
|
|
48
|
-
await
|
|
53
|
+
await _stop(this.recording);
|
|
49
54
|
// FIXME update to rename/move file
|
|
50
55
|
// The destination file could exixt of we use --resultDir
|
|
51
56
|
// so make sure we remove it first
|
|
52
57
|
if (this.options.resultDir) {
|
|
53
58
|
try {
|
|
54
59
|
await unlink(destination);
|
|
55
|
-
} catch
|
|
60
|
+
} catch {
|
|
56
61
|
// Nothing to see here
|
|
57
62
|
}
|
|
58
63
|
}
|
|
@@ -63,15 +68,15 @@ module.exports = class DesktopRecorder {
|
|
|
63
68
|
} else {
|
|
64
69
|
await rename(this.filePath, destination);
|
|
65
70
|
}
|
|
66
|
-
} catch (
|
|
71
|
+
} catch (error) {
|
|
67
72
|
log.error(
|
|
68
73
|
'Converting the video failed. Converting from ' +
|
|
69
74
|
this.filePath +
|
|
70
75
|
' to ' +
|
|
71
76
|
destination
|
|
72
77
|
);
|
|
73
|
-
throw
|
|
78
|
+
throw error;
|
|
74
79
|
}
|
|
75
80
|
log.debug(`Writing to ${destination}`);
|
|
76
81
|
}
|
|
77
|
-
}
|
|
82
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import intel from 'intel';
|
|
3
|
+
import getScreenOnOSX from './osx/getScreen.js';
|
|
4
|
+
import getSPDisplaysDataType from './osx/getSPDisplaysDataType.js';
|
|
5
|
+
const log = intel.getLogger('browsertime.video');
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
const log = require('intel').getLogger('browsertime.video');
|
|
5
|
-
const getScreenOnOSX = require('./osx/getScreen');
|
|
6
|
-
const getSPDisplaysDataType = require('./osx/getSPDisplaysDataType');
|
|
7
|
-
|
|
8
|
-
async function buildX11FfmpegArgs({
|
|
7
|
+
async function buildX11FfmpegArguments({
|
|
9
8
|
display,
|
|
10
9
|
screen = 0,
|
|
11
10
|
framerate = 30,
|
|
@@ -23,7 +22,7 @@ async function buildX11FfmpegArgs({
|
|
|
23
22
|
const uiregex = /(?<=UI Looks like:\s*)(\d+\s*x\s*\d+)/gm;
|
|
24
23
|
const uiLooksLike = SPDisplaysDataType.match(uiregex);
|
|
25
24
|
const widthAndHeight = size.split('x');
|
|
26
|
-
let devicePixelRatio = SPDisplaysDataType.
|
|
25
|
+
let devicePixelRatio = SPDisplaysDataType.includes('Retina') ? 2 : 1;
|
|
27
26
|
// Lets add better check in the future. Right now: If we have uilookslike and
|
|
28
27
|
// is not the same as the resolution, say it DPI 2
|
|
29
28
|
if (uiLooksLike && resolution[0].trim() !== uiLooksLike[0].trim()) {
|
|
@@ -85,12 +84,12 @@ async function buildX11FfmpegArgs({
|
|
|
85
84
|
}
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
async function startRecording(
|
|
87
|
+
async function startRecording(ffmpegArguments, nice, filePath) {
|
|
89
88
|
async function waitForRecording(readableStream) {
|
|
90
89
|
return new Promise((resolve, reject) => {
|
|
91
90
|
readableStream.on('data', data => {
|
|
92
91
|
log.trace(data.toString());
|
|
93
|
-
if (
|
|
92
|
+
if (/Press \[q] to stop/.test(data.toString())) {
|
|
94
93
|
// readableStream.removeAllListeners('data');
|
|
95
94
|
return resolve();
|
|
96
95
|
}
|
|
@@ -100,78 +99,76 @@ async function startRecording(ffmpegArgs, nice, filePath) {
|
|
|
100
99
|
}
|
|
101
100
|
let ffmpegProcess;
|
|
102
101
|
if (nice !== 0) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
log.debug('Start FFMPEG with %j',
|
|
107
|
-
ffmpegProcess = execa('nice',
|
|
102
|
+
ffmpegArguments.unshift('ffmpeg');
|
|
103
|
+
ffmpegArguments.unshift(`${nice}`);
|
|
104
|
+
ffmpegArguments.unshift('-n');
|
|
105
|
+
log.debug('Start FFMPEG with %j', ffmpegArguments);
|
|
106
|
+
ffmpegProcess = execa('nice', ffmpegArguments);
|
|
108
107
|
} else {
|
|
109
|
-
log.debug('Start FFMPEG with %j',
|
|
110
|
-
ffmpegProcess = execa('ffmpeg',
|
|
108
|
+
log.debug('Start FFMPEG with %j', ffmpegArguments);
|
|
109
|
+
ffmpegProcess = execa('ffmpeg', ffmpegArguments);
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
// Race to catch if ffmpeg exists early, or if recording has started
|
|
114
113
|
return Promise.race([ffmpegProcess, waitForRecording(ffmpegProcess.stderr)])
|
|
115
114
|
.then(() => {
|
|
116
|
-
log.debug('Started ffmpeg with ' +
|
|
115
|
+
log.debug('Started ffmpeg with ' + ffmpegArguments.join(' '));
|
|
117
116
|
|
|
118
117
|
return {
|
|
119
118
|
filePath,
|
|
120
119
|
ffmpegProcess
|
|
121
120
|
};
|
|
122
121
|
})
|
|
123
|
-
.catch(
|
|
124
|
-
log.error(
|
|
125
|
-
throw
|
|
122
|
+
.catch(error => {
|
|
123
|
+
log.error(error);
|
|
124
|
+
throw error;
|
|
126
125
|
});
|
|
127
126
|
}
|
|
128
127
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
export /**
|
|
129
|
+
* @returns A promise for a recording object. Pass it to stopRecording.
|
|
130
|
+
*/
|
|
131
|
+
async function start({
|
|
132
|
+
display,
|
|
133
|
+
origin,
|
|
134
|
+
size,
|
|
135
|
+
filePath,
|
|
136
|
+
offset,
|
|
137
|
+
framerate,
|
|
138
|
+
crf,
|
|
139
|
+
nice,
|
|
140
|
+
threads
|
|
141
|
+
}) {
|
|
142
|
+
const widthAndHeight = size.split('x');
|
|
143
|
+
const withoutTopBar =
|
|
144
|
+
Number.parseInt(widthAndHeight[0]) -
|
|
145
|
+
offset.x +
|
|
146
|
+
'x' +
|
|
147
|
+
(Number.parseInt(widthAndHeight[1]) - offset.y);
|
|
148
|
+
|
|
149
|
+
const ffmpegArguments = await buildX11FfmpegArguments({
|
|
134
150
|
display,
|
|
135
151
|
origin,
|
|
136
|
-
size,
|
|
152
|
+
size: withoutTopBar,
|
|
137
153
|
filePath,
|
|
138
|
-
offset,
|
|
139
154
|
framerate,
|
|
140
155
|
crf,
|
|
141
|
-
|
|
156
|
+
offset,
|
|
142
157
|
threads
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
crf,
|
|
158
|
-
offset,
|
|
159
|
-
threads
|
|
160
|
-
});
|
|
161
|
-
return startRecording(ffmpegArgs, nice, filePath);
|
|
162
|
-
},
|
|
163
|
-
/**
|
|
164
|
-
* @returns A promise for a recording result, with a filePath property.
|
|
165
|
-
*/
|
|
166
|
-
async stop(recording) {
|
|
167
|
-
return Promise.resolve(recording).then(rec => {
|
|
168
|
-
const process = rec.ffmpegProcess;
|
|
169
|
-
delete rec.ffmpegProcess;
|
|
170
|
-
process.stdin.write('q');
|
|
171
|
-
return Promise.resolve(process).then(() => {
|
|
172
|
-
log.debug('Stopped ffmpeg');
|
|
173
|
-
return rec;
|
|
174
|
-
});
|
|
158
|
+
});
|
|
159
|
+
return startRecording(ffmpegArguments, nice, filePath);
|
|
160
|
+
}
|
|
161
|
+
export /**
|
|
162
|
+
* @returns A promise for a recording result, with a filePath property.
|
|
163
|
+
*/
|
|
164
|
+
async function stop(recording) {
|
|
165
|
+
return Promise.resolve(recording).then(rec => {
|
|
166
|
+
const process = rec.ffmpegProcess;
|
|
167
|
+
delete rec.ffmpegProcess;
|
|
168
|
+
process.stdin.write('q');
|
|
169
|
+
return Promise.resolve(process).then(() => {
|
|
170
|
+
log.debug('Stopped ffmpeg');
|
|
171
|
+
return rec;
|
|
175
172
|
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { execaCommand as command } from 'execa';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
module.exports = async function () {
|
|
6
|
-
const output = await execa.command('system_profiler SPDisplaysDataType', {
|
|
3
|
+
export default async function () {
|
|
4
|
+
const output = await command('system_profiler SPDisplaysDataType', {
|
|
7
5
|
shell: true
|
|
8
6
|
});
|
|
9
7
|
return output.stdout;
|
|
10
|
-
}
|
|
8
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import intel from 'intel';
|
|
3
|
+
const log = intel.getLogger('browsertime.video');
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
module.exports = async function () {
|
|
7
|
-
const scriptArgs = [
|
|
5
|
+
export default async function () {
|
|
6
|
+
const scriptArguments = [
|
|
8
7
|
'-hide_banner',
|
|
9
8
|
'-f',
|
|
10
9
|
'avfoundation',
|
|
@@ -13,7 +12,7 @@ module.exports = async function () {
|
|
|
13
12
|
'-i',
|
|
14
13
|
0
|
|
15
14
|
];
|
|
16
|
-
log.debug('Getting screen on OS X using %j',
|
|
15
|
+
log.debug('Getting screen on OS X using %j', scriptArguments);
|
|
17
16
|
|
|
18
17
|
/*
|
|
19
18
|
ffmpeg -hide_banner -f avfoundation -list_devices true -i 0
|
|
@@ -25,8 +24,8 @@ module.exports = async function () {
|
|
|
25
24
|
[AVFoundation input device @ 0x7fb919e00780] [0] Built-in Microphone
|
|
26
25
|
*/
|
|
27
26
|
|
|
28
|
-
const output = await execa('ffmpeg',
|
|
27
|
+
const output = await execa('ffmpeg', scriptArguments, { reject: false });
|
|
29
28
|
log.debug('Output: %s', output.stderr);
|
|
30
|
-
const reg = /\[(\d+)
|
|
29
|
+
const reg = /\[(\d+)] Capture screen/;
|
|
31
30
|
return output.stderr.match(reg)[1];
|
|
32
|
-
}
|
|
31
|
+
}
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { promisify } from 'node:util';
|
|
2
|
+
import {
|
|
3
|
+
unlink as _unlink,
|
|
4
|
+
readdirSync,
|
|
5
|
+
statSync,
|
|
6
|
+
createWriteStream,
|
|
7
|
+
copyFileSync
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import intel from 'intel';
|
|
11
|
+
import { execa } from 'execa';
|
|
12
|
+
import { removeDirAndFiles } from '../../../support/fileUtil.js';
|
|
13
|
+
import { Android, isAndroidConfigured } from '../../../android/index.js';
|
|
14
|
+
const log = intel.getLogger('browsertime.video');
|
|
15
|
+
|
|
16
|
+
const unlink = promisify(_unlink);
|
|
12
17
|
|
|
13
18
|
async function enableWindowRecorder(enable, browser) {
|
|
14
|
-
|
|
15
|
-
enable = 'true';
|
|
16
|
-
} else {
|
|
17
|
-
enable = 'false';
|
|
18
|
-
}
|
|
19
|
+
enable = enable ? 'true' : 'false';
|
|
19
20
|
|
|
20
21
|
// We wrap windowUtils.setCompositionRecording(...) in Promise.resolve() to
|
|
21
22
|
// work on both the older (synchronous) and newer (asynchronous)
|
|
@@ -30,25 +31,24 @@ async function enableWindowRecorder(enable, browser) {
|
|
|
30
31
|
|
|
31
32
|
return browser.runPrivilegedAsyncScript(
|
|
32
33
|
script,
|
|
33
|
-
'toggle composition recorder'
|
|
34
|
-
undefined
|
|
34
|
+
'toggle composition recorder'
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function findRecordingDirectory(baseDir) {
|
|
39
39
|
let closest_mtime = 0;
|
|
40
|
-
let directory
|
|
40
|
+
let directory;
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
for (const file of readdirSync(baseDir)) {
|
|
43
43
|
if (file.startsWith('windowrecording-')) {
|
|
44
|
-
let fullPath =
|
|
45
|
-
let mtime =
|
|
44
|
+
let fullPath = join(baseDir, file);
|
|
45
|
+
let mtime = statSync(fullPath).mtime;
|
|
46
46
|
if (mtime > closest_mtime) {
|
|
47
47
|
closest_mtime = mtime;
|
|
48
48
|
directory = fullPath;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|
|
52
52
|
|
|
53
53
|
log.debug('Using window recording directory: ' + directory);
|
|
54
54
|
if (directory === undefined) {
|
|
@@ -60,36 +60,36 @@ function findRecordingDirectory(baseDir) {
|
|
|
60
60
|
function findRecordingStartTime(recordingDir) {
|
|
61
61
|
// Recording directory has the following format:
|
|
62
62
|
// .../browsertime-results/<web address>/<date time>/windowrecording-<unix recording start time>
|
|
63
|
-
return recordingDir.
|
|
63
|
+
return recordingDir.slice(recordingDir.lastIndexOf('-') + 1);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function findTimeToFirstFrame(recordingDir) {
|
|
67
|
-
const files =
|
|
68
|
-
if (files === undefined || files.length
|
|
67
|
+
const files = readdirSync(recordingDir);
|
|
68
|
+
if (files === undefined || files.length === 0) {
|
|
69
69
|
log.error('Could not find recordings in ' + recordingDir);
|
|
70
70
|
} else {
|
|
71
71
|
// Iterate through files, extract the time from the filenames, and return the smallest time
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
// eslint-disable-next-line unicorn/no-array-reduce
|
|
73
|
+
return files.reduce((accumulator, fileName) => {
|
|
74
|
+
const time = Number.parseInt(fileName.split('-')[2].split('.')[0], 10);
|
|
75
|
+
return Math.min(accumulator, time);
|
|
76
|
+
}, Number.POSITIVE_INFINITY);
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
function writeFrameDurationsToFile(directoryName, imageFiles) {
|
|
80
81
|
return new Promise((resolve, reject) => {
|
|
81
|
-
const stream =
|
|
82
|
-
path.join(directoryName, 'durations.txt')
|
|
83
|
-
);
|
|
82
|
+
const stream = createWriteStream(join(directoryName, 'durations.txt'));
|
|
84
83
|
stream.once('open', function () {
|
|
85
84
|
stream.write(
|
|
86
|
-
"file '" +
|
|
85
|
+
"file '" + join(directoryName, imageFiles[0].filename) + "'\n"
|
|
87
86
|
);
|
|
88
|
-
for (let
|
|
89
|
-
let duration =
|
|
87
|
+
for (let index = 1; index < imageFiles.length; index++) {
|
|
88
|
+
let duration =
|
|
89
|
+
(imageFiles[index].offset - imageFiles[index - 1].offset) / 1000;
|
|
90
90
|
stream.write('duration ' + duration.toString() + '\n');
|
|
91
91
|
stream.write(
|
|
92
|
-
"file '" +
|
|
92
|
+
"file '" + join(directoryName, imageFiles[index].filename) + "'\n"
|
|
93
93
|
);
|
|
94
94
|
}
|
|
95
95
|
stream.end();
|
|
@@ -97,7 +97,7 @@ function writeFrameDurationsToFile(directoryName, imageFiles) {
|
|
|
97
97
|
resolve(true);
|
|
98
98
|
});
|
|
99
99
|
stream.on('error', () => {
|
|
100
|
-
reject(Error('Error writing durations to file.'));
|
|
100
|
+
reject(new Error('Error writing durations to file.'));
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
103
|
});
|
|
@@ -106,19 +106,19 @@ function writeFrameDurationsToFile(directoryName, imageFiles) {
|
|
|
106
106
|
async function generateVideo(destination, recordingDirectoryName) {
|
|
107
107
|
let imageFiles = [];
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
for (const file of readdirSync(recordingDirectoryName)) {
|
|
110
110
|
// Format of the filenames are "frame-<num>-<offset>.png"
|
|
111
111
|
// where num is frame number and offset is time since capture start in ms.
|
|
112
112
|
let fields = file.split('-');
|
|
113
113
|
let frameno = ('0000' + fields[1]).slice(-4);
|
|
114
114
|
let newFilename = 'frame' + frameno + '.png';
|
|
115
115
|
let offset = fields[2].split('.')[0];
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
copyFileSync(
|
|
117
|
+
join(recordingDirectoryName, file),
|
|
118
|
+
join(recordingDirectoryName, newFilename)
|
|
119
119
|
);
|
|
120
120
|
imageFiles.push({ filename: newFilename, offset: offset });
|
|
121
|
-
}
|
|
121
|
+
}
|
|
122
122
|
|
|
123
123
|
imageFiles.sort(function (a, b) {
|
|
124
124
|
if (a.filename < b.filename) return -1;
|
|
@@ -127,13 +127,13 @@ async function generateVideo(destination, recordingDirectoryName) {
|
|
|
127
127
|
});
|
|
128
128
|
await writeFrameDurationsToFile(recordingDirectoryName, imageFiles);
|
|
129
129
|
|
|
130
|
-
const
|
|
130
|
+
const vfr_arguments = [
|
|
131
131
|
'-f',
|
|
132
132
|
'concat',
|
|
133
133
|
'-safe',
|
|
134
134
|
'0',
|
|
135
135
|
'-i',
|
|
136
|
-
|
|
136
|
+
join(recordingDirectoryName, 'durations.txt'),
|
|
137
137
|
'-vf',
|
|
138
138
|
'pad=ceil(iw/2)*2:ceil(ih/2)*2',
|
|
139
139
|
'-vsync',
|
|
@@ -142,24 +142,24 @@ async function generateVideo(destination, recordingDirectoryName) {
|
|
|
142
142
|
'yuv420p',
|
|
143
143
|
destination
|
|
144
144
|
];
|
|
145
|
-
log.debug('Executing command: ffmpeg ' +
|
|
146
|
-
await execa('ffmpeg',
|
|
145
|
+
log.debug('Executing command: ffmpeg ' + vfr_arguments.join(' '));
|
|
146
|
+
await execa('ffmpeg', vfr_arguments);
|
|
147
147
|
return removeDirAndFiles(recordingDirectoryName);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
export default class FirefoxWindowRecorder {
|
|
151
151
|
constructor(options, browser, baseDir) {
|
|
152
152
|
this.options = options;
|
|
153
153
|
this.browser = browser;
|
|
154
154
|
this.baseDir = baseDir;
|
|
155
|
-
this.recordingStartTime =
|
|
156
|
-
this.timeToFirstFrame =
|
|
155
|
+
this.recordingStartTime = undefined;
|
|
156
|
+
this.timeToFirstFrame = undefined;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
async start() {
|
|
160
160
|
log.info('Start firefox window recorder.');
|
|
161
161
|
|
|
162
|
-
if (this.options
|
|
162
|
+
if (isAndroidConfigured(this.options)) {
|
|
163
163
|
this.android = new Android(this.options);
|
|
164
164
|
await this.android.removePathOnSdCard(
|
|
165
165
|
'browsertime-firefox-windowrecording'
|
|
@@ -174,7 +174,7 @@ module.exports = class FirefoxWindowRecorder {
|
|
|
174
174
|
log.info('Stop firefox window recorder.');
|
|
175
175
|
await enableWindowRecorder(false, this.browser);
|
|
176
176
|
|
|
177
|
-
if (this.options
|
|
177
|
+
if (isAndroidConfigured(this.options)) {
|
|
178
178
|
const fullPathOnSdCard = this.android.getFullPathOnSdCard(
|
|
179
179
|
'browsertime-firefox-windowrecording'
|
|
180
180
|
);
|
|
@@ -187,7 +187,7 @@ module.exports = class FirefoxWindowRecorder {
|
|
|
187
187
|
if (this.options.resultDir) {
|
|
188
188
|
try {
|
|
189
189
|
await unlink(destination);
|
|
190
|
-
} catch
|
|
190
|
+
} catch {
|
|
191
191
|
// Nothing to see here
|
|
192
192
|
}
|
|
193
193
|
}
|
|
@@ -203,4 +203,4 @@ module.exports = class FirefoxWindowRecorder {
|
|
|
203
203
|
);
|
|
204
204
|
log.debug(`Writing to ${destination}`);
|
|
205
205
|
}
|
|
206
|
-
}
|
|
206
|
+
}
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import intel from 'intel';
|
|
3
|
+
const log = intel.getLogger('browsertime.video');
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
module.exports = async function convert(src, dest) {
|
|
7
|
-
const scriptArgs = [
|
|
5
|
+
export default async function convert(source, destination) {
|
|
6
|
+
const scriptArguments = [
|
|
8
7
|
'-framerate',
|
|
9
8
|
60,
|
|
10
9
|
'-i',
|
|
11
|
-
|
|
10
|
+
source,
|
|
12
11
|
'-c:v',
|
|
13
12
|
'copy',
|
|
14
13
|
'-f',
|
|
15
14
|
'mp4',
|
|
16
|
-
|
|
15
|
+
destination,
|
|
17
16
|
'-y'
|
|
18
17
|
];
|
|
19
18
|
|
|
20
|
-
log.debug('Converting video from h264 to mp4 %j',
|
|
19
|
+
log.debug('Converting video from h264 to mp4 %j', scriptArguments);
|
|
21
20
|
|
|
22
|
-
return execa('ffmpeg',
|
|
23
|
-
}
|
|
21
|
+
return execa('ffmpeg', scriptArguments);
|
|
22
|
+
}
|
|
@@ -1,36 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { resolve, join } from 'node:path';
|
|
4
|
+
import { execaCommand as command } from 'execa';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { unlink as _unlink } from 'node:fs';
|
|
7
|
+
import convert from './convertToMp4.js';
|
|
8
|
+
import intel from 'intel';
|
|
9
|
+
const log = intel.getLogger('browsertime.video');
|
|
10
|
+
const unlink = promisify(_unlink);
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const moduleRootPath = resolve(__dirname, '..', '..', '..', '..');
|
|
13
|
+
const QVH = resolve(moduleRootPath, 'vendor', 'mac', 'x86', 'qvh');
|
|
10
14
|
const delay = ms => new Promise(res => setTimeout(res, ms));
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
export default class IOSRecorder {
|
|
13
17
|
constructor(options, baseDir) {
|
|
14
18
|
this.options = options;
|
|
15
19
|
this.uuid = options.safari.deviceUDID;
|
|
16
|
-
this.tmpVideo =
|
|
17
|
-
this.tmpSound =
|
|
20
|
+
this.tmpVideo = join(baseDir, 'tmp.h264');
|
|
21
|
+
this.tmpSound = join(baseDir, 'tmp.wav');
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
static async activate() {
|
|
21
25
|
log.debug('Activate configuration for IOS recorder.');
|
|
22
|
-
return
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
.stdout.pipe(process.stdout);
|
|
26
|
+
return command(
|
|
27
|
+
QVH + ' activate ' + (this.uuid ? '--udid=' + this.uuid + ' ' : ''),
|
|
28
|
+
{ shell: true }
|
|
29
|
+
).stdout.pipe(process.stdout);
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
async start() {
|
|
31
33
|
log.info('Start IOS recorder.');
|
|
32
34
|
|
|
33
|
-
this.qvhProcessProcess =
|
|
35
|
+
this.qvhProcessProcess = command(
|
|
34
36
|
QVH +
|
|
35
37
|
' record ' +
|
|
36
38
|
(this.uuid ? '--udid=' + this.uuid + ' ' : '') +
|
|
@@ -62,6 +64,6 @@ module.exports = class IOSRecorder {
|
|
|
62
64
|
await unlink(this.tmpVideo);
|
|
63
65
|
await unlink(this.tmpSound);
|
|
64
66
|
// await rename(this.tmpVideo, destination);
|
|
65
|
-
return
|
|
67
|
+
return this.qvhProcessProcess;
|
|
66
68
|
}
|
|
67
|
-
}
|
|
69
|
+
}
|