browsertime 22.2.0 → 22.4.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 +12 -0
- package/bin/browsertime.js +26 -13
- package/lib/android/index.js +37 -0
- package/lib/chrome/webdriver/chromium.js +33 -6
- package/lib/chrome/webdriver/setupChromiumOptions.js +4 -0
- package/lib/core/engine/iteration.js +10 -1
- package/lib/firefox/settings/firefoxPreferences.js +1 -1
- package/lib/firefox/webdriver/firefox.js +33 -6
- package/lib/support/cli.js +25 -0
- package/lib/support/util.js +23 -0
- package/lib/video/postprocessing/visualmetrics/visualMetrics.js +9 -0
- package/package.json +2 -1
- package/types/android/index.d.ts +5 -0
- package/types/android/index.d.ts.map +1 -1
- package/types/support/util.d.ts +1 -0
- package/types/support/util.d.ts.map +1 -1
- package/types/video/postprocessing/visualmetrics/visualMetrics.d.ts.map +1 -1
- package/visualmetrics/visualmetrics-portable.py +131 -3
- package/visualmetrics/visualmetrics.py +130 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Browsertime changelog (we do [semantic versioning](https://semver.org))
|
|
2
2
|
|
|
3
|
+
## 22.3.0 - 2024-06-06
|
|
4
|
+
### Added
|
|
5
|
+
* Use `--enableVideoRun` to get one extra run with a video and visual metrics [#2139](https://github.com/sitespeedio/browsertime/pull/2139)
|
|
6
|
+
|
|
7
|
+
## 22.3.0 - 2024-06-04
|
|
8
|
+
### Added
|
|
9
|
+
* Add the ability to gather power usage measurements on Android from USB power meters, thank you [Gregory Mierzwinski](https://github.com/gmierz) for PR [#2134](https://github.com/sitespeedio/browsertime/pull/2134).
|
|
10
|
+
* Add support to visualmetrics to identify key frames matching the given colors, thank you [aosmond](https://github.com/aosmond) for PR [#2119](https://github.com/sitespeedio/browsertime/pull/2119).
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
* Removed DOMContentFlushed for Firefox thank you [florinbilt](https://github.com/florinbilt) for PR [#2138](https://github.com/sitespeedio/browsertime/pull/2138).
|
|
14
|
+
|
|
3
15
|
## 22.2.0 - 2024-05-24
|
|
4
16
|
### Added
|
|
5
17
|
* New command: Mouse single click on a element with a specific id `commands.mouse.singleClick.byId(id)` and `commands.mouse.singleClick.byIdAndWait(id)` [#2135](https://github.com/sitespeedio/browsertime/pull/2135).
|
package/bin/browsertime.js
CHANGED
|
@@ -123,21 +123,34 @@ async function run(urls, options) {
|
|
|
123
123
|
);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
if (options.enableProfileRun) {
|
|
127
|
-
log.info('Make one extra run to collect trace information');
|
|
126
|
+
if (options.enableProfileRun || options.enableVideoRun) {
|
|
127
|
+
log.info('Make one extra run to collect trace/video information');
|
|
128
128
|
options.iterations = 1;
|
|
129
|
-
if (options.
|
|
130
|
-
options.firefox
|
|
131
|
-
|
|
132
|
-
options.chrome
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
129
|
+
if (options.enableProfileRun) {
|
|
130
|
+
if (options.browser === 'firefox') {
|
|
131
|
+
options.firefox.geckoProfiler = true;
|
|
132
|
+
} else if (options.browser === 'chrome') {
|
|
133
|
+
options.chrome.timeline = true;
|
|
134
|
+
options.cpu = true;
|
|
135
|
+
options.chrome.enableTraceScreenshots = true;
|
|
136
|
+
options.chrome.traceCategory = [
|
|
137
|
+
'disabled-by-default-v8.cpu_profiler'
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (options.enableVideoRun) {
|
|
142
|
+
if (options.video === true) {
|
|
143
|
+
log.error(
|
|
144
|
+
'You can only configure video run if you do not collect any video'
|
|
145
|
+
);
|
|
146
|
+
// This is a hack to not get an error
|
|
147
|
+
options.video = false;
|
|
148
|
+
options.visualMetrics = false;
|
|
149
|
+
} else {
|
|
150
|
+
options.video = true;
|
|
151
|
+
options.visualMetrics = true;
|
|
152
|
+
}
|
|
138
153
|
}
|
|
139
|
-
options.video = false;
|
|
140
|
-
options.visualMetrics = false;
|
|
141
154
|
const traceEngine = new Engine(options);
|
|
142
155
|
await traceEngine.start();
|
|
143
156
|
await traceEngine.runMultiple(urls, scriptsByCategory);
|
package/lib/android/index.js
CHANGED
|
@@ -5,8 +5,10 @@ import { EOL as endOfLine } from 'node:os';
|
|
|
5
5
|
import { execa } from 'execa';
|
|
6
6
|
import intel from 'intel';
|
|
7
7
|
import pkg from '@devicefarmer/adbkit';
|
|
8
|
+
import usbPowerProfiler from 'usb-power-profiling/usb-power-profiling.js';
|
|
8
9
|
const { Adb } = pkg;
|
|
9
10
|
import get from 'lodash.get';
|
|
11
|
+
import { pathToFolder } from '../support/pathToFolder.js';
|
|
10
12
|
const log = intel.getLogger('browsertime.android');
|
|
11
13
|
const mkdir = promisify(_mkdir);
|
|
12
14
|
const delay = ms => new Promise(res => setTimeout(res, ms));
|
|
@@ -466,6 +468,41 @@ export class Android {
|
|
|
466
468
|
const batterystats = await this._runCommandAndGet('dumpsys batterystats');
|
|
467
469
|
return parsePowerMetrics(batterystats, packageName);
|
|
468
470
|
}
|
|
471
|
+
|
|
472
|
+
async measureUsbPowerUsage(startTime, endTime) {
|
|
473
|
+
return getUsbPowerUsage(startTime, endTime);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async getUsbPowerUsageProfile(index, url, result, options, storageManager) {
|
|
477
|
+
let profileData = await usbPowerProfiler.profileFromData();
|
|
478
|
+
let destinationFilename = join(
|
|
479
|
+
await pathToFolder(url, options),
|
|
480
|
+
`powerProfile-${index}.json`
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
await storageManager.writeJson(destinationFilename, profileData);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function getUsbPowerUsage(startTime, endTime) {
|
|
488
|
+
let baselineUsageData = await usbPowerProfiler.getPowerData(
|
|
489
|
+
startTime - 2,
|
|
490
|
+
endTime - 1
|
|
491
|
+
);
|
|
492
|
+
let baselineUsageTotal = baselineUsageData[0]['samples']['data'].reduce(
|
|
493
|
+
(currSum, currVal) => currSum + Number.parseInt(currVal[1]),
|
|
494
|
+
0
|
|
495
|
+
);
|
|
496
|
+
let baselineUsage =
|
|
497
|
+
baselineUsageTotal / baselineUsageData[0]['samples']['data'].length;
|
|
498
|
+
|
|
499
|
+
let powerUsageData = await usbPowerProfiler.getPowerData(startTime, endTime);
|
|
500
|
+
let powerUsage = powerUsageData[0]['samples']['data'].reduce(
|
|
501
|
+
(currSum, currVal) => currSum + Number.parseInt(currVal[1]),
|
|
502
|
+
0
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
return { powerUsage, baselineUsage };
|
|
469
506
|
}
|
|
470
507
|
|
|
471
508
|
async function parsePowerMetrics(batterystats, packageName) {
|
|
@@ -3,6 +3,7 @@ import { unlink as _unlink, rm as _rm } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { logging } from 'selenium-webdriver';
|
|
5
5
|
import intel from 'intel';
|
|
6
|
+
import usbPowerProfiler from 'usb-power-profiling/usb-power-profiling.js';
|
|
6
7
|
const log = intel.getLogger('browsertime.chrome');
|
|
7
8
|
const { Type } = logging;
|
|
8
9
|
import { longTaskMetrics } from '../longTaskMetrics.js';
|
|
@@ -36,6 +37,7 @@ export class Chromium {
|
|
|
36
37
|
// Keep the HAR file for all runs
|
|
37
38
|
this.hars = [];
|
|
38
39
|
this.androidTmpDir = '/data/local/tmp/';
|
|
40
|
+
this.testStartTime = undefined;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -68,6 +70,10 @@ export class Chromium {
|
|
|
68
70
|
// https://github.com/cyrus-and/chrome-remote-interface/issues/332
|
|
69
71
|
if (isAndroidConfigured(this.options)) {
|
|
70
72
|
await this.android.addDevtoolsFw();
|
|
73
|
+
|
|
74
|
+
if (this.options.androidUsbPower) {
|
|
75
|
+
usbPowerProfiler.startSampling();
|
|
76
|
+
}
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
this.cdpClient = new ChromeDevtoolsProtocol(this.options);
|
|
@@ -153,8 +159,12 @@ export class Chromium {
|
|
|
153
159
|
log.info('Could not set cookie because the URL is unknown');
|
|
154
160
|
}
|
|
155
161
|
|
|
156
|
-
if (this.android
|
|
157
|
-
|
|
162
|
+
if (this.android) {
|
|
163
|
+
if (this.options.androidPower) {
|
|
164
|
+
await this.android.resetPowerUsage();
|
|
165
|
+
} else if (this.options.androidUsbPower) {
|
|
166
|
+
await usbPowerProfiler.resetPowerData();
|
|
167
|
+
}
|
|
158
168
|
}
|
|
159
169
|
|
|
160
170
|
if (
|
|
@@ -165,6 +175,8 @@ export class Chromium {
|
|
|
165
175
|
this.isTracing = true;
|
|
166
176
|
return this.cdpClient.startTrace();
|
|
167
177
|
}
|
|
178
|
+
|
|
179
|
+
this.testStartTime = Date.now();
|
|
168
180
|
}
|
|
169
181
|
|
|
170
182
|
/**
|
|
@@ -218,10 +230,25 @@ export class Chromium {
|
|
|
218
230
|
'GET_LONG_TASKS'
|
|
219
231
|
);
|
|
220
232
|
|
|
221
|
-
if (this.android
|
|
222
|
-
|
|
223
|
-
this.
|
|
224
|
-
|
|
233
|
+
if (this.android) {
|
|
234
|
+
if (this.options.androidPower) {
|
|
235
|
+
result.power = await this.android.measurePowerUsage(
|
|
236
|
+
this.chrome.android.package
|
|
237
|
+
);
|
|
238
|
+
} else if (this.options.androidUsbPower) {
|
|
239
|
+
result.power = await this.android.measureUsbPowerUsage(
|
|
240
|
+
this.testStartTime,
|
|
241
|
+
Date.now()
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
await this.android.getUsbPowerUsageProfile(
|
|
245
|
+
index,
|
|
246
|
+
url,
|
|
247
|
+
result,
|
|
248
|
+
this.options,
|
|
249
|
+
this.storageManager
|
|
250
|
+
);
|
|
251
|
+
}
|
|
225
252
|
}
|
|
226
253
|
|
|
227
254
|
if (this.options.verbose >= 2 || this.chrome.enableChromeDriverLog) {
|
|
@@ -172,6 +172,10 @@ export function setupChromiumOptions(
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
if (browserOptions.enableVideoAutoplay) {
|
|
176
|
+
seleniumOptions.addArguments('--autoplay-policy=no-user-gesture-required');
|
|
177
|
+
}
|
|
178
|
+
|
|
175
179
|
// It's a new splash screen introduced in Chrome 98
|
|
176
180
|
// for new profiles
|
|
177
181
|
// disable it with ChromeWhatsNewUI
|
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
addConnectivity,
|
|
16
16
|
removeConnectivity
|
|
17
17
|
} from '../../connectivity/index.js';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
jsonifyVisualProgress,
|
|
20
|
+
jsonifyKeyColorFrames
|
|
21
|
+
} from '../../support/util.js';
|
|
19
22
|
import { flushDNS } from '../../support/dns.js';
|
|
20
23
|
|
|
21
24
|
import { getNumberOfRunningProcesses } from '../../support/processes.js';
|
|
@@ -232,6 +235,12 @@ export class Iteration {
|
|
|
232
235
|
);
|
|
233
236
|
}
|
|
234
237
|
}
|
|
238
|
+
if (videoMetrics.visualMetrics['KeyColorFrames']) {
|
|
239
|
+
videoMetrics.visualMetrics['KeyColorFrames'] =
|
|
240
|
+
jsonifyKeyColorFrames(
|
|
241
|
+
videoMetrics.visualMetrics['KeyColorFrames']
|
|
242
|
+
);
|
|
243
|
+
}
|
|
235
244
|
result[index_].videoRecordingStart =
|
|
236
245
|
videoMetrics.videoRecordingStart;
|
|
237
246
|
result[index_].visualMetrics = videoMetrics.visualMetrics;
|
|
@@ -151,7 +151,7 @@ export const defaultFirefoxPreferences = {
|
|
|
151
151
|
// Preferences file used by the raptor harness
|
|
152
152
|
'dom.performance.time_to_non_blank_paint.enabled': true,
|
|
153
153
|
'dom.performance.time_to_contentful_paint.enabled': true,
|
|
154
|
-
'dom.performance.time_to_dom_content_flushed.enabled':
|
|
154
|
+
'dom.performance.time_to_dom_content_flushed.enabled': false,
|
|
155
155
|
'dom.performance.time_to_first_interactive.enabled': true,
|
|
156
156
|
|
|
157
157
|
// required for geckoview logging
|
|
@@ -3,6 +3,7 @@ import { promisify } from 'node:util';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import intel from 'intel';
|
|
5
5
|
import get from 'lodash.get';
|
|
6
|
+
import usbPowerProfiler from 'usb-power-profiling/usb-power-profiling.js';
|
|
6
7
|
import { adapters } from 'ff-test-bidi-har-export';
|
|
7
8
|
import { getEmptyHAR, mergeHars } from '../../support/har/index.js';
|
|
8
9
|
import { pathToFolder } from '../../support/pathToFolder.js';
|
|
@@ -32,6 +33,7 @@ export class Firefox {
|
|
|
32
33
|
this.aliasAndUrl = {};
|
|
33
34
|
// This keep the HAR files for all runs
|
|
34
35
|
this.hars = [];
|
|
36
|
+
this.testStartTime = undefined;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/**
|
|
@@ -71,6 +73,10 @@ export class Firefox {
|
|
|
71
73
|
runner.getDriver(),
|
|
72
74
|
this.options
|
|
73
75
|
);
|
|
76
|
+
|
|
77
|
+
if (isAndroidConfigured(this.options) && this.options.androidUsbPower) {
|
|
78
|
+
usbPowerProfiler.startSampling();
|
|
79
|
+
}
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
/**
|
|
@@ -131,8 +137,12 @@ export class Firefox {
|
|
|
131
137
|
await this.har.startRecording();
|
|
132
138
|
}
|
|
133
139
|
|
|
134
|
-
if (isAndroidConfigured(this.options)
|
|
135
|
-
|
|
140
|
+
if (isAndroidConfigured(this.options)) {
|
|
141
|
+
if (this.options.androidPower) {
|
|
142
|
+
await this.android.resetPowerUsage();
|
|
143
|
+
} else if (this.options.androidUsbPower) {
|
|
144
|
+
await usbPowerProfiler.resetPowerData();
|
|
145
|
+
}
|
|
136
146
|
}
|
|
137
147
|
|
|
138
148
|
if (
|
|
@@ -158,6 +168,8 @@ export class Firefox {
|
|
|
158
168
|
this.perfStats = new PerfStats(runner, this.firefoxConfig);
|
|
159
169
|
return this.perfStats.start();
|
|
160
170
|
}
|
|
171
|
+
|
|
172
|
+
this.testStartTime = Date.now();
|
|
161
173
|
}
|
|
162
174
|
|
|
163
175
|
/**
|
|
@@ -169,10 +181,25 @@ export class Firefox {
|
|
|
169
181
|
async afterPageCompleteCheck(runner, index, url, alias) {
|
|
170
182
|
const result = { url, alias };
|
|
171
183
|
|
|
172
|
-
if (isAndroidConfigured(this.options)
|
|
173
|
-
|
|
174
|
-
this.
|
|
175
|
-
|
|
184
|
+
if (isAndroidConfigured(this.options)) {
|
|
185
|
+
if (this.options.androidPower) {
|
|
186
|
+
result.power = await this.android.measurePowerUsage(
|
|
187
|
+
this.firefoxConfig.android.package
|
|
188
|
+
);
|
|
189
|
+
} else if (this.options.androidUsbPower) {
|
|
190
|
+
result.power = await this.android.measureUsbPowerUsage(
|
|
191
|
+
this.testStartTime,
|
|
192
|
+
Date.now()
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
await this.android.getUsbPowerUsageProfile(
|
|
196
|
+
index,
|
|
197
|
+
url,
|
|
198
|
+
result,
|
|
199
|
+
this.options,
|
|
200
|
+
this.storageManager
|
|
201
|
+
);
|
|
202
|
+
}
|
|
176
203
|
}
|
|
177
204
|
|
|
178
205
|
if (
|
package/lib/support/cli.js
CHANGED
|
@@ -274,6 +274,11 @@ export function parseCommandLine() {
|
|
|
274
274
|
type: 'boolean',
|
|
275
275
|
group: 'chrome'
|
|
276
276
|
})
|
|
277
|
+
.option('chrome.enableVideoAutoplay', {
|
|
278
|
+
describe: 'Allow videos to autoplay.',
|
|
279
|
+
type: 'boolean',
|
|
280
|
+
group: 'chrome'
|
|
281
|
+
})
|
|
277
282
|
.option('chrome.timeline', {
|
|
278
283
|
alias: 'chrome.trace',
|
|
279
284
|
describe:
|
|
@@ -341,6 +346,15 @@ export function parseCommandLine() {
|
|
|
341
346
|
'(You have to disable charging yourself for this - it depends on the phone model).',
|
|
342
347
|
group: 'android'
|
|
343
348
|
})
|
|
349
|
+
.option('android.usbPowerTesting', {
|
|
350
|
+
alias: 'androidUsbPower',
|
|
351
|
+
type: 'boolean',
|
|
352
|
+
describe:
|
|
353
|
+
'Enables android power testing using usb-power-profiling. Assumes that ' +
|
|
354
|
+
'a valid device is attached to the phone. See here for supported devices: ' +
|
|
355
|
+
'https://github.com/fqueze/usb-power-profiling?tab=readme-ov-file#supported-devices',
|
|
356
|
+
group: 'android'
|
|
357
|
+
})
|
|
344
358
|
.option('chrome.CPUThrottlingRate', {
|
|
345
359
|
type: 'number',
|
|
346
360
|
describe:
|
|
@@ -600,6 +614,11 @@ export function parseCommandLine() {
|
|
|
600
614
|
describe:
|
|
601
615
|
'Make one extra run that collects the profiling trace log (no other metrics is collected). For Chrome it will collect the timeline trace, for Firefox it will get the Geckoprofiler trace. This means you do not need to get the trace for all runs and can skip the overhead it produces.'
|
|
602
616
|
})
|
|
617
|
+
.option('enableVideoRun', {
|
|
618
|
+
type: 'boolean',
|
|
619
|
+
describe:
|
|
620
|
+
'Make one extra run that collects video and visual metrics. This means you can do your runs with --visualMetrics true --video false --enableVideoRun true to collect visual metrics from all runs and save a video from the profile/video run. If you run it together with --enableProfileRun it will also collect profiling trace.'
|
|
621
|
+
})
|
|
603
622
|
.option('video', {
|
|
604
623
|
type: 'boolean',
|
|
605
624
|
describe:
|
|
@@ -709,6 +728,12 @@ export function parseCommandLine() {
|
|
|
709
728
|
describe:
|
|
710
729
|
'Use the portable visual-metrics processing script (no ImageMagick dependencies).'
|
|
711
730
|
})
|
|
731
|
+
.option('visualMetricsKeyColor', {
|
|
732
|
+
type: 'array',
|
|
733
|
+
nargs: 8,
|
|
734
|
+
describe:
|
|
735
|
+
'Collect Key Color frame metrics when you run --visualMetrics. Each --visualMetricsKeyColor supplied must have 8 arguments: key name, red channel (0-255) low and high, green channel (0-255) low and high, blue channel (0-255) low and high, fraction (0.0-1.0) of pixels that must match each channel.'
|
|
736
|
+
})
|
|
712
737
|
.option('scriptInput.visualElements', {
|
|
713
738
|
describe:
|
|
714
739
|
'Include specific elements in visual elements. Give the element a name and select it with document.body.querySelector. Use like this: --scriptInput.visualElements name:domSelector see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors. Add multiple instances to measure multiple elements. Visual Metrics will use these elements and calculate when they are visible and fully rendered.'
|
package/lib/support/util.js
CHANGED
|
@@ -230,6 +230,29 @@ export function jsonifyVisualProgress(visualProgress) {
|
|
|
230
230
|
}
|
|
231
231
|
return visualProgress;
|
|
232
232
|
}
|
|
233
|
+
export function jsonifyKeyColorFrames(keyColorFrames) {
|
|
234
|
+
// Original data looks like
|
|
235
|
+
// "FrameName1=[0-133 255-300], FrameName2=[133-255] FrameName3=[]"
|
|
236
|
+
if (typeof keyColorFrames === 'string') {
|
|
237
|
+
const keyColorFramesObject = {};
|
|
238
|
+
for (const keyColorPair of keyColorFrames.split(', ')) {
|
|
239
|
+
const [name, values] = keyColorPair.split('=');
|
|
240
|
+
keyColorFramesObject[name] = [];
|
|
241
|
+
const rangePairs = values.replace('[', '').replace(']', '');
|
|
242
|
+
if (rangePairs) {
|
|
243
|
+
for (const rangePair of rangePairs.split(' ')) {
|
|
244
|
+
const [start, end] = rangePair.split('-');
|
|
245
|
+
keyColorFramesObject[name].push({
|
|
246
|
+
startTimestamp: Number.parseInt(start, 10),
|
|
247
|
+
endTimestamp: Number.parseInt(end, 10)
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return keyColorFramesObject;
|
|
253
|
+
}
|
|
254
|
+
return keyColorFrames;
|
|
255
|
+
}
|
|
233
256
|
export function adjustVisualProgressTimestamps(
|
|
234
257
|
visualProgress,
|
|
235
258
|
profilerStartTime,
|
|
@@ -84,6 +84,15 @@ export async function run(
|
|
|
84
84
|
scriptArguments.push('--contentful');
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
if (options.visualMetricsKeyColor) {
|
|
88
|
+
for (let i = 0; i < options.visualMetricsKeyColor.length; ++i) {
|
|
89
|
+
if (i % 8 == 0) {
|
|
90
|
+
scriptArguments.push('--keycolor');
|
|
91
|
+
}
|
|
92
|
+
scriptArguments.push(options.visualMetricsKeyColor[i]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
// There seems to be a bug with --startwhite that makes VM bail out
|
|
88
97
|
// 11:20:14.950 - Calculating image histograms
|
|
89
98
|
// 11:20:14.951 - No video frames found in /private/var/folders/27/xpnvcsbs0nlfbb4qq397z3rh0000gn/T/vis-cn_JMf
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browsertime",
|
|
3
3
|
"description": "Get performance metrics from your web page using Browsertime.",
|
|
4
|
-
"version": "22.
|
|
4
|
+
"version": "22.4.0",
|
|
5
5
|
"bin": "./bin/browsertime.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"types": "./types/scripting.d.ts",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"lodash.pick": "4.4.0",
|
|
32
32
|
"lodash.set": "4.3.2",
|
|
33
33
|
"selenium-webdriver": "4.21.0",
|
|
34
|
+
"usb-power-profiling": "^1.2.0",
|
|
34
35
|
"yargs": "17.7.2"
|
|
35
36
|
},
|
|
36
37
|
"optionalDependencies": {
|
package/types/android/index.d.ts
CHANGED
|
@@ -66,5 +66,10 @@ export class Android {
|
|
|
66
66
|
'full-wifi': number;
|
|
67
67
|
total: number;
|
|
68
68
|
}>;
|
|
69
|
+
measureUsbPowerUsage(startTime: any, endTime: any): Promise<{
|
|
70
|
+
powerUsage: any;
|
|
71
|
+
baselineUsage: number;
|
|
72
|
+
}>;
|
|
73
|
+
getUsbPowerUsageProfile(index: any, url: any, result: any, options: any, storageManager: any): Promise<void>;
|
|
69
74
|
}
|
|
70
75
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/android/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/android/index.js"],"names":[],"mappings":"AAskBA,2DAWC;AAlkBD;IACE,0BAwBC;IAfC,YAAgC;IAEhC,QAIC;IACD,UAAgC;IAGhC,6BAA6B;IAC7B,yBAA2B;IAC3B,eAAgC;IAKlC,uBAgBC;IALC,YAA4C;IAG1C,YAAoE;IAIxE,wCAEC;IAED,8CAIC;IAED,6CAIC;IAED,2CAUC;IAED,mEAYC;IAED,mEAcC;IAED,uCAEC;IAED,0CAGC;IAED,4CAMC;IAED,4CAMC;IAED,uBAGC;IAED,kCAOC;IAED;;;;;;;OAuBC;IAED,2CAKC;IAED,8BAKC;IAED;;;oDAuBC;IAED,2BAIC;IAED,qCAGC;IAED,iCAGC;IAED,wBASC;IAED,2CAOC;IAED,gCAEC;IAED,0BAIC;IAED,iCAGC;IAED,8CAIC;IAED,4BAEC;IAED,sCAYC;IAED,mDAsBC;IAED,gDA+CC;IAED;;;;;;;OAqDC;IAED,4DAQC;IAED,mCAcC;IAED,kCAQC;IAED,iCAIC;IAED;;;;OAGC;IAED;;;OAEC;IAED,6GAQC;CACF"}
|
package/types/support/util.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export function formatMetric(name: any, metric: any, multiple: any, inMs: any, e
|
|
|
2
2
|
export function logResultLogLine(results: any): void;
|
|
3
3
|
export function toArray(arrayLike: any): any[];
|
|
4
4
|
export function jsonifyVisualProgress(visualProgress: any): any;
|
|
5
|
+
export function jsonifyKeyColorFrames(keyColorFrames: any): any;
|
|
5
6
|
export function adjustVisualProgressTimestamps(visualProgress: any, profilerStartTime: any, recordingStartTime: any): any;
|
|
6
7
|
//# sourceMappingURL=util.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../lib/support/util.js"],"names":[],"mappings":"AAIA,oGAeC;AACD,qDA0LC;AACD,+CAQC;AACD,gEAeC;AACD,0HAWC"}
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../lib/support/util.js"],"names":[],"mappings":"AAIA,oGAeC;AACD,qDA0LC;AACD,+CAQC;AACD,gEAeC;AACD,gEAsBC;AACD,0HAWC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"visualMetrics.d.ts","sourceRoot":"","sources":["../../../../lib/video/postprocessing/visualmetrics/visualMetrics.js"],"names":[],"mappings":"AAsCA,mGAEC;AACD,
|
|
1
|
+
{"version":3,"file":"visualMetrics.d.ts","sourceRoot":"","sources":["../../../../lib/video/postprocessing/visualmetrics/visualMetrics.js"],"names":[],"mappings":"AAsCA,mGAEC;AACD,0KA0HC"}
|
|
@@ -1104,7 +1104,7 @@ def calculate_histograms(directory, histograms_file, force):
|
|
|
1104
1104
|
m = re.search(match, frame)
|
|
1105
1105
|
if m is not None:
|
|
1106
1106
|
frame_time = int(m.groupdict().get("ms"))
|
|
1107
|
-
histogram = calculate_image_histogram(frame)
|
|
1107
|
+
histogram, total, dropped = calculate_image_histogram(frame)
|
|
1108
1108
|
gc.collect()
|
|
1109
1109
|
if histogram is not None:
|
|
1110
1110
|
histograms.append(
|
|
@@ -1112,6 +1112,8 @@ def calculate_histograms(directory, histograms_file, force):
|
|
|
1112
1112
|
"time": frame_time,
|
|
1113
1113
|
"file": os.path.basename(frame),
|
|
1114
1114
|
"histogram": histogram,
|
|
1115
|
+
"total_pixels": total,
|
|
1116
|
+
"dropped_pixels": dropped,
|
|
1115
1117
|
}
|
|
1116
1118
|
)
|
|
1117
1119
|
if os.path.isfile(histograms_file):
|
|
@@ -1130,12 +1132,14 @@ def calculate_histograms(directory, histograms_file, force):
|
|
|
1130
1132
|
|
|
1131
1133
|
def calculate_image_histogram(file):
|
|
1132
1134
|
logging.debug("Calculating histogram for " + file)
|
|
1135
|
+
dropped = 0
|
|
1133
1136
|
try:
|
|
1134
1137
|
from PIL import Image
|
|
1135
1138
|
|
|
1136
1139
|
im = Image.open(file)
|
|
1137
1140
|
width, height = im.size
|
|
1138
|
-
|
|
1141
|
+
total = width * height
|
|
1142
|
+
colors = im.getcolors(total)
|
|
1139
1143
|
histogram = {
|
|
1140
1144
|
"r": [0 for i in range(256)],
|
|
1141
1145
|
"g": [0 for i in range(256)],
|
|
@@ -1151,13 +1155,16 @@ def calculate_image_histogram(file):
|
|
|
1151
1155
|
histogram["r"][pixel[0]] += count
|
|
1152
1156
|
histogram["g"][pixel[1]] += count
|
|
1153
1157
|
histogram["b"][pixel[2]] += count
|
|
1158
|
+
else:
|
|
1159
|
+
dropped += 1
|
|
1154
1160
|
except Exception:
|
|
1155
1161
|
pass
|
|
1156
1162
|
colors = None
|
|
1157
1163
|
except Exception:
|
|
1164
|
+
total = 0
|
|
1158
1165
|
histogram = None
|
|
1159
1166
|
logging.exception("Error calculating histogram for " + file)
|
|
1160
|
-
return histogram
|
|
1167
|
+
return histogram, total, dropped
|
|
1161
1168
|
|
|
1162
1169
|
|
|
1163
1170
|
##########################################################################
|
|
@@ -1212,6 +1219,7 @@ def calculate_visual_metrics(
|
|
|
1212
1219
|
dirs,
|
|
1213
1220
|
progress_file,
|
|
1214
1221
|
hero_elements_file,
|
|
1222
|
+
key_colors,
|
|
1215
1223
|
):
|
|
1216
1224
|
metrics = None
|
|
1217
1225
|
histograms = load_histograms(histograms_file, start, end)
|
|
@@ -1225,6 +1233,7 @@ def calculate_visual_metrics(
|
|
|
1225
1233
|
f = open(progress_file, "w")
|
|
1226
1234
|
json.dump(progress, f)
|
|
1227
1235
|
f.close()
|
|
1236
|
+
key_color_frames = calculate_key_color_frames(histograms, key_colors)
|
|
1228
1237
|
if len(histograms) > 1:
|
|
1229
1238
|
metrics = [
|
|
1230
1239
|
{"name": "First Visual Change", "value": histograms[1]["time"]},
|
|
@@ -1315,6 +1324,20 @@ def calculate_visual_metrics(
|
|
|
1315
1324
|
metrics.append({"name": "Perceptual Speed Index", "value": 0})
|
|
1316
1325
|
if contentful:
|
|
1317
1326
|
metrics.append({"name": "Contentful Speed Index", "value": 0})
|
|
1327
|
+
if key_color_frames:
|
|
1328
|
+
keysum = ""
|
|
1329
|
+
for key in key_color_frames:
|
|
1330
|
+
if len(keysum):
|
|
1331
|
+
keysum += ", "
|
|
1332
|
+
framesum = ""
|
|
1333
|
+
for frame in key_color_frames[key]:
|
|
1334
|
+
if len(framesum):
|
|
1335
|
+
framesum += " "
|
|
1336
|
+
framesum += "{0:d}-{1:d}".format(
|
|
1337
|
+
frame["start_time"], frame["end_time"]
|
|
1338
|
+
)
|
|
1339
|
+
keysum += "{0}=[{1}]".format(key, framesum)
|
|
1340
|
+
metrics.append({"name": "Key Color Frames", "value": keysum})
|
|
1318
1341
|
prog = ""
|
|
1319
1342
|
for p in progress:
|
|
1320
1343
|
if len(prog):
|
|
@@ -1346,6 +1369,79 @@ def load_histograms(histograms_file, start, end):
|
|
|
1346
1369
|
return histograms
|
|
1347
1370
|
|
|
1348
1371
|
|
|
1372
|
+
def is_key_color_frame(histogram, key_color):
|
|
1373
|
+
# The fraction is measured against the entire image, not just the sampled
|
|
1374
|
+
# pixels. This helps avoid matching frames with only a few pixels that
|
|
1375
|
+
# happen to be in the acceptable range.
|
|
1376
|
+
total_fraction = histogram["total_pixels"] * key_color["fraction"]
|
|
1377
|
+
if total_fraction < histogram["total_pixels"] - histogram["dropped_pixels"]:
|
|
1378
|
+
for channel in ["r", "g", "b"]:
|
|
1379
|
+
# Find the acceptable range around the target channel value
|
|
1380
|
+
max_channel = len(histogram["histogram"][channel])
|
|
1381
|
+
low = min(max_channel - 1, max(0, key_color[channel + "_low"]))
|
|
1382
|
+
high = min(max_channel, max(1, key_color[channel + "_high"] + 1))
|
|
1383
|
+
target_total = 0
|
|
1384
|
+
for i in histogram["histogram"][channel][low:high]:
|
|
1385
|
+
target_total += i
|
|
1386
|
+
if target_total < total_fraction:
|
|
1387
|
+
return False
|
|
1388
|
+
return True
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
def calculate_key_color_frames(histograms, key_colors):
|
|
1392
|
+
if not key_colors:
|
|
1393
|
+
return {}
|
|
1394
|
+
|
|
1395
|
+
key_color_frames = {}
|
|
1396
|
+
for key in key_colors:
|
|
1397
|
+
key_color_frames[key] = []
|
|
1398
|
+
|
|
1399
|
+
current = None
|
|
1400
|
+
current_key = None
|
|
1401
|
+
total = 0
|
|
1402
|
+
matched = 0
|
|
1403
|
+
buckets = 256
|
|
1404
|
+
channels = ["r", "g", "b"]
|
|
1405
|
+
histograms = histograms.copy()
|
|
1406
|
+
|
|
1407
|
+
while len(histograms) > 0:
|
|
1408
|
+
histogram = histograms.pop(0)
|
|
1409
|
+
matching_key = None
|
|
1410
|
+
for key in key_colors:
|
|
1411
|
+
if is_key_color_frame(histogram, key_colors[key]):
|
|
1412
|
+
matching_key = key
|
|
1413
|
+
break
|
|
1414
|
+
|
|
1415
|
+
if matching_key is None:
|
|
1416
|
+
continue
|
|
1417
|
+
|
|
1418
|
+
last_histogram = histogram
|
|
1419
|
+
frame_count = 1
|
|
1420
|
+
while len(histograms) > 0:
|
|
1421
|
+
last_histogram = histograms[0]
|
|
1422
|
+
if is_key_color_frame(last_histogram, key_colors[matching_key]):
|
|
1423
|
+
frame_count += 1
|
|
1424
|
+
histograms.pop(0)
|
|
1425
|
+
else:
|
|
1426
|
+
break
|
|
1427
|
+
|
|
1428
|
+
logging.debug(
|
|
1429
|
+
"{0:d}ms to {1:d}ms - Matched key color frame {2}".format(
|
|
1430
|
+
histogram["time"], last_histogram["time"], matching_key
|
|
1431
|
+
)
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1434
|
+
key_color_frames[matching_key].append(
|
|
1435
|
+
{
|
|
1436
|
+
"frame_count": frame_count,
|
|
1437
|
+
"start_time": histogram["time"],
|
|
1438
|
+
"end_time": last_histogram["time"],
|
|
1439
|
+
}
|
|
1440
|
+
)
|
|
1441
|
+
|
|
1442
|
+
return key_color_frames
|
|
1443
|
+
|
|
1444
|
+
|
|
1349
1445
|
def calculate_visual_progress(histograms):
|
|
1350
1446
|
progress = []
|
|
1351
1447
|
first = histograms[0]["histogram"]
|
|
@@ -1760,6 +1856,24 @@ def main():
|
|
|
1760
1856
|
default=False,
|
|
1761
1857
|
help="Remove orange-colored frames from the beginning of the video.",
|
|
1762
1858
|
)
|
|
1859
|
+
parser.add_argument(
|
|
1860
|
+
"--keycolor",
|
|
1861
|
+
action="append",
|
|
1862
|
+
nargs=8,
|
|
1863
|
+
metavar=(
|
|
1864
|
+
"key",
|
|
1865
|
+
"red_low",
|
|
1866
|
+
"red_high",
|
|
1867
|
+
"green_low",
|
|
1868
|
+
"green_high",
|
|
1869
|
+
"blue_low",
|
|
1870
|
+
"blue_high",
|
|
1871
|
+
"fraction",
|
|
1872
|
+
),
|
|
1873
|
+
help="Identify frames that match the given channel (0-255) low and "
|
|
1874
|
+
"high. Fraction is the percentage of the pixels per channel that "
|
|
1875
|
+
"must be in the given range (0-1).",
|
|
1876
|
+
)
|
|
1763
1877
|
parser.add_argument(
|
|
1764
1878
|
"-p",
|
|
1765
1879
|
"--viewport",
|
|
@@ -1916,6 +2030,19 @@ def main():
|
|
|
1916
2030
|
options.full,
|
|
1917
2031
|
)
|
|
1918
2032
|
|
|
2033
|
+
key_colors = {}
|
|
2034
|
+
if options.keycolor:
|
|
2035
|
+
for key_params in options.keycolor:
|
|
2036
|
+
key_colors[key_params[0]] = {
|
|
2037
|
+
"r_low": int(key_params[1]),
|
|
2038
|
+
"r_high": int(key_params[2]),
|
|
2039
|
+
"g_low": int(key_params[3]),
|
|
2040
|
+
"g_high": int(key_params[4]),
|
|
2041
|
+
"b_low": int(key_params[5]),
|
|
2042
|
+
"b_high": int(key_params[6]),
|
|
2043
|
+
"fraction": float(key_params[7]),
|
|
2044
|
+
}
|
|
2045
|
+
|
|
1919
2046
|
# Calculate the histograms and visual metrics
|
|
1920
2047
|
calculate_histograms(directory, histogram_file, options.force)
|
|
1921
2048
|
metrics = calculate_visual_metrics(
|
|
@@ -1927,6 +2054,7 @@ def main():
|
|
|
1927
2054
|
directory,
|
|
1928
2055
|
options.progress,
|
|
1929
2056
|
options.herodata,
|
|
2057
|
+
key_colors,
|
|
1930
2058
|
)
|
|
1931
2059
|
|
|
1932
2060
|
if options.screenshot is not None:
|
|
@@ -1339,7 +1339,7 @@ def calculate_histograms(directory, histograms_file, force):
|
|
|
1339
1339
|
m = re.search(match, frame)
|
|
1340
1340
|
if m is not None:
|
|
1341
1341
|
frame_time = int(m.groupdict().get("ms"))
|
|
1342
|
-
histogram = calculate_image_histogram(frame)
|
|
1342
|
+
histogram, total, dropped = calculate_image_histogram(frame)
|
|
1343
1343
|
gc.collect()
|
|
1344
1344
|
if histogram is not None:
|
|
1345
1345
|
histograms.append(
|
|
@@ -1347,6 +1347,8 @@ def calculate_histograms(directory, histograms_file, force):
|
|
|
1347
1347
|
"time": frame_time,
|
|
1348
1348
|
"file": os.path.basename(frame),
|
|
1349
1349
|
"histogram": histogram,
|
|
1350
|
+
"total_pixels": total,
|
|
1351
|
+
"dropped_pixels": dropped,
|
|
1350
1352
|
}
|
|
1351
1353
|
)
|
|
1352
1354
|
if os.path.isfile(histograms_file):
|
|
@@ -1365,12 +1367,14 @@ def calculate_histograms(directory, histograms_file, force):
|
|
|
1365
1367
|
|
|
1366
1368
|
def calculate_image_histogram(file):
|
|
1367
1369
|
logging.debug("Calculating histogram for " + file)
|
|
1370
|
+
dropped = 0
|
|
1368
1371
|
try:
|
|
1369
1372
|
from PIL import Image
|
|
1370
1373
|
|
|
1371
1374
|
im = Image.open(file)
|
|
1372
1375
|
width, height = im.size
|
|
1373
|
-
|
|
1376
|
+
total = width * height
|
|
1377
|
+
colors = im.getcolors(total)
|
|
1374
1378
|
histogram = {
|
|
1375
1379
|
"r": [0 for i in range(256)],
|
|
1376
1380
|
"g": [0 for i in range(256)],
|
|
@@ -1386,13 +1390,16 @@ def calculate_image_histogram(file):
|
|
|
1386
1390
|
histogram["r"][pixel[0]] += count
|
|
1387
1391
|
histogram["g"][pixel[1]] += count
|
|
1388
1392
|
histogram["b"][pixel[2]] += count
|
|
1393
|
+
else:
|
|
1394
|
+
dropped += 1
|
|
1389
1395
|
except Exception:
|
|
1390
1396
|
pass
|
|
1391
1397
|
colors = None
|
|
1392
1398
|
except Exception:
|
|
1399
|
+
total = 0
|
|
1393
1400
|
histogram = None
|
|
1394
1401
|
logging.exception("Error calculating histogram for " + file)
|
|
1395
|
-
return histogram
|
|
1402
|
+
return histogram, total, dropped
|
|
1396
1403
|
|
|
1397
1404
|
|
|
1398
1405
|
##########################################################################
|
|
@@ -1618,6 +1625,7 @@ def calculate_visual_metrics(
|
|
|
1618
1625
|
dirs,
|
|
1619
1626
|
progress_file,
|
|
1620
1627
|
hero_elements_file,
|
|
1628
|
+
key_colors,
|
|
1621
1629
|
):
|
|
1622
1630
|
metrics = None
|
|
1623
1631
|
histograms = load_histograms(histograms_file, start, end)
|
|
@@ -1631,6 +1639,7 @@ def calculate_visual_metrics(
|
|
|
1631
1639
|
f = open(progress_file, "w")
|
|
1632
1640
|
json.dump(progress, f)
|
|
1633
1641
|
f.close()
|
|
1642
|
+
key_color_frames = calculate_key_color_frames(histograms, key_colors)
|
|
1634
1643
|
if len(histograms) > 1:
|
|
1635
1644
|
metrics = [
|
|
1636
1645
|
{"name": "First Visual Change", "value": histograms[1]["time"]},
|
|
@@ -1720,6 +1729,20 @@ def calculate_visual_metrics(
|
|
|
1720
1729
|
metrics.append({"name": "Perceptual Speed Index", "value": 0})
|
|
1721
1730
|
if contentful:
|
|
1722
1731
|
metrics.append({"name": "Contentful Speed Index", "value": 0})
|
|
1732
|
+
if key_color_frames:
|
|
1733
|
+
keysum = ""
|
|
1734
|
+
for key in key_color_frames:
|
|
1735
|
+
if len(keysum):
|
|
1736
|
+
keysum += ", "
|
|
1737
|
+
framesum = ""
|
|
1738
|
+
for frame in key_color_frames[key]:
|
|
1739
|
+
if len(framesum):
|
|
1740
|
+
framesum += " "
|
|
1741
|
+
framesum += "{0:d}-{1:d}".format(
|
|
1742
|
+
frame["start_time"], frame["end_time"]
|
|
1743
|
+
)
|
|
1744
|
+
keysum += "{0}=[{1}]".format(key, framesum)
|
|
1745
|
+
metrics.append({"name": "Key Color Frames", "value": keysum})
|
|
1723
1746
|
prog = ""
|
|
1724
1747
|
for p in progress:
|
|
1725
1748
|
if len(prog):
|
|
@@ -1751,6 +1774,79 @@ def load_histograms(histograms_file, start, end):
|
|
|
1751
1774
|
return histograms
|
|
1752
1775
|
|
|
1753
1776
|
|
|
1777
|
+
def is_key_color_frame(histogram, key_color):
|
|
1778
|
+
# The fraction is measured against the entire image, not just the sampled
|
|
1779
|
+
# pixels. This helps avoid matching frames with only a few pixels that
|
|
1780
|
+
# happen to be in the acceptable range.
|
|
1781
|
+
total_fraction = histogram["total_pixels"] * key_color["fraction"]
|
|
1782
|
+
if total_fraction < histogram["total_pixels"] - histogram["dropped_pixels"]:
|
|
1783
|
+
for channel in ["r", "g", "b"]:
|
|
1784
|
+
# Find the acceptable range around the target channel value
|
|
1785
|
+
max_channel = len(histogram["histogram"][channel])
|
|
1786
|
+
low = min(max_channel - 1, max(0, key_color[channel + "_low"]))
|
|
1787
|
+
high = min(max_channel, max(1, key_color[channel + "_high"] + 1))
|
|
1788
|
+
target_total = 0
|
|
1789
|
+
for i in histogram["histogram"][channel][low:high]:
|
|
1790
|
+
target_total += i
|
|
1791
|
+
if target_total < total_fraction:
|
|
1792
|
+
return False
|
|
1793
|
+
return True
|
|
1794
|
+
|
|
1795
|
+
|
|
1796
|
+
def calculate_key_color_frames(histograms, key_colors):
|
|
1797
|
+
if not key_colors:
|
|
1798
|
+
return {}
|
|
1799
|
+
|
|
1800
|
+
key_color_frames = {}
|
|
1801
|
+
for key in key_colors:
|
|
1802
|
+
key_color_frames[key] = []
|
|
1803
|
+
|
|
1804
|
+
current = None
|
|
1805
|
+
current_key = None
|
|
1806
|
+
total = 0
|
|
1807
|
+
matched = 0
|
|
1808
|
+
buckets = 256
|
|
1809
|
+
channels = ["r", "g", "b"]
|
|
1810
|
+
histograms = histograms.copy()
|
|
1811
|
+
|
|
1812
|
+
while len(histograms) > 0:
|
|
1813
|
+
histogram = histograms.pop(0)
|
|
1814
|
+
matching_key = None
|
|
1815
|
+
for key in key_colors:
|
|
1816
|
+
if is_key_color_frame(histogram, key_colors[key]):
|
|
1817
|
+
matching_key = key
|
|
1818
|
+
break
|
|
1819
|
+
|
|
1820
|
+
if matching_key is None:
|
|
1821
|
+
continue
|
|
1822
|
+
|
|
1823
|
+
last_histogram = histogram
|
|
1824
|
+
frame_count = 1
|
|
1825
|
+
while len(histograms) > 0:
|
|
1826
|
+
last_histogram = histograms[0]
|
|
1827
|
+
if is_key_color_frame(last_histogram, key_colors[matching_key]):
|
|
1828
|
+
frame_count += 1
|
|
1829
|
+
histograms.pop(0)
|
|
1830
|
+
else:
|
|
1831
|
+
break
|
|
1832
|
+
|
|
1833
|
+
logging.debug(
|
|
1834
|
+
"{0:d}ms to {1:d}ms - Matched key color frame {2}".format(
|
|
1835
|
+
histogram["time"], last_histogram["time"], matching_key
|
|
1836
|
+
)
|
|
1837
|
+
)
|
|
1838
|
+
|
|
1839
|
+
key_color_frames[matching_key].append(
|
|
1840
|
+
{
|
|
1841
|
+
"frame_count": frame_count,
|
|
1842
|
+
"start_time": histogram["time"],
|
|
1843
|
+
"end_time": last_histogram["time"],
|
|
1844
|
+
}
|
|
1845
|
+
)
|
|
1846
|
+
|
|
1847
|
+
return key_color_frames
|
|
1848
|
+
|
|
1849
|
+
|
|
1754
1850
|
def calculate_visual_progress(histograms):
|
|
1755
1851
|
progress = []
|
|
1756
1852
|
first = histograms[0]["histogram"]
|
|
@@ -2200,6 +2296,24 @@ def main():
|
|
|
2200
2296
|
help="Wait for a full white frame after a non-white frame "
|
|
2201
2297
|
"at the beginning of the video.",
|
|
2202
2298
|
)
|
|
2299
|
+
parser.add_argument(
|
|
2300
|
+
"--keycolor",
|
|
2301
|
+
action="append",
|
|
2302
|
+
nargs=8,
|
|
2303
|
+
metavar=(
|
|
2304
|
+
"key",
|
|
2305
|
+
"red_low",
|
|
2306
|
+
"red_high",
|
|
2307
|
+
"green_low",
|
|
2308
|
+
"green_high",
|
|
2309
|
+
"blue_low",
|
|
2310
|
+
"blue_high",
|
|
2311
|
+
"fraction",
|
|
2312
|
+
),
|
|
2313
|
+
help="Identify frames that match the given channel (0-255) low and "
|
|
2314
|
+
"high. Fraction is the percentage of the pixels per channel that "
|
|
2315
|
+
"must be in the given range (0-1).",
|
|
2316
|
+
)
|
|
2203
2317
|
parser.add_argument(
|
|
2204
2318
|
"--multiple",
|
|
2205
2319
|
action="store_true",
|
|
@@ -2464,6 +2578,18 @@ def main():
|
|
|
2464
2578
|
if not options.multiple:
|
|
2465
2579
|
if options.render is not None:
|
|
2466
2580
|
render_video(directory, options.render)
|
|
2581
|
+
key_colors = {}
|
|
2582
|
+
if options.keycolor:
|
|
2583
|
+
for key_params in options.keycolor:
|
|
2584
|
+
key_colors[key_params[0]] = {
|
|
2585
|
+
"r_low": int(key_params[1]),
|
|
2586
|
+
"r_high": int(key_params[2]),
|
|
2587
|
+
"g_low": int(key_params[3]),
|
|
2588
|
+
"g_high": int(key_params[4]),
|
|
2589
|
+
"b_low": int(key_params[5]),
|
|
2590
|
+
"b_high": int(key_params[6]),
|
|
2591
|
+
"fraction": float(key_params[7]),
|
|
2592
|
+
}
|
|
2467
2593
|
|
|
2468
2594
|
# Calculate the histograms and visual metrics
|
|
2469
2595
|
calculate_histograms(directory, histogram_file, options.force)
|
|
@@ -2476,6 +2602,7 @@ def main():
|
|
|
2476
2602
|
directory,
|
|
2477
2603
|
options.progress,
|
|
2478
2604
|
options.herodata,
|
|
2605
|
+
key_colors,
|
|
2479
2606
|
)
|
|
2480
2607
|
|
|
2481
2608
|
if options.screenshot is not None:
|