browsertime 14.10.0 → 14.10.1
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 +8 -0
- package/browsertime/visualmetrics.py +172 -84
- package/lib/chrome/webdriver/builder.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
# Browsertime changelog (we do [semantic versioning](https://semver.org))
|
|
2
2
|
|
|
3
|
+
## 14.10.1 - 2021-11-19
|
|
4
|
+
### Fixed
|
|
5
|
+
* Disabled the automatic Chrome/Chromedriver version check in Chromedriver [#1676](https://github.com/sitespeedio/browsertime/pull/1676).
|
|
6
|
+
|
|
3
7
|
## 14.10.0 - 2021-11-16
|
|
4
8
|
### Added
|
|
5
9
|
* Updated to Chromedriver 96 and Chrome 96 in the Docker container [#1670](https://github.com/sitespeedio/browsertime/pull/1670).
|
|
6
10
|
|
|
11
|
+
### Fixed
|
|
12
|
+
* Added checks for Firefox HAR that it actually has a page.
|
|
13
|
+
* Updated day js.
|
|
14
|
+
|
|
7
15
|
## 14.9.0 - 2021-11-07
|
|
8
16
|
### Added
|
|
9
17
|
* Updated to new Chome HAR PR [#1666](https://github.com/sitespeedio/browsertime/pull/1666) that inlcudes chunk information.
|
|
@@ -44,12 +44,12 @@ import shutil
|
|
|
44
44
|
import subprocess
|
|
45
45
|
import tempfile
|
|
46
46
|
|
|
47
|
-
if
|
|
48
|
-
GZIP_TEXT =
|
|
49
|
-
GZIP_READ_TEXT =
|
|
47
|
+
if sys.version_info > (3, 0):
|
|
48
|
+
GZIP_TEXT = "wt"
|
|
49
|
+
GZIP_READ_TEXT = "rt"
|
|
50
50
|
else:
|
|
51
|
-
GZIP_TEXT =
|
|
52
|
-
GZIP_READ_TEXT =
|
|
51
|
+
GZIP_TEXT = "w"
|
|
52
|
+
GZIP_READ_TEXT = "r"
|
|
53
53
|
|
|
54
54
|
# Globals
|
|
55
55
|
options = None
|
|
@@ -72,6 +72,9 @@ def video_to_frames(
|
|
|
72
72
|
multiple,
|
|
73
73
|
find_viewport,
|
|
74
74
|
viewport_time,
|
|
75
|
+
viewport_retries,
|
|
76
|
+
viewport_min_height,
|
|
77
|
+
viewport_min_width,
|
|
75
78
|
full_resolution,
|
|
76
79
|
timeline_file,
|
|
77
80
|
trim_end,
|
|
@@ -93,7 +96,13 @@ def video_to_frames(
|
|
|
93
96
|
if os.path.isdir(directory):
|
|
94
97
|
directory = os.path.realpath(directory)
|
|
95
98
|
viewport = find_video_viewport(
|
|
96
|
-
video,
|
|
99
|
+
video,
|
|
100
|
+
directory,
|
|
101
|
+
find_viewport,
|
|
102
|
+
viewport_time,
|
|
103
|
+
viewport_retries,
|
|
104
|
+
viewport_min_height,
|
|
105
|
+
viewport_min_width,
|
|
97
106
|
)
|
|
98
107
|
gc.collect()
|
|
99
108
|
if extract_frames(video, directory, full_resolution, viewport):
|
|
@@ -169,10 +178,10 @@ def extract_frames(video, directory, full_resolution, viewport):
|
|
|
169
178
|
]
|
|
170
179
|
logging.debug(" ".join(command))
|
|
171
180
|
lines = []
|
|
172
|
-
if
|
|
173
|
-
|
|
181
|
+
if sys.version_info > (3, 0):
|
|
182
|
+
proc = subprocess.Popen(command, stderr=subprocess.PIPE, encoding="UTF-8")
|
|
174
183
|
else:
|
|
175
|
-
|
|
184
|
+
proc = subprocess.Popen(command, stderr=subprocess.PIPE)
|
|
176
185
|
while proc.poll() is None:
|
|
177
186
|
lines.extend(iter(proc.stderr.readline, ""))
|
|
178
187
|
|
|
@@ -354,65 +363,107 @@ def find_image_viewport(file):
|
|
|
354
363
|
return viewport
|
|
355
364
|
|
|
356
365
|
|
|
357
|
-
def find_video_viewport(
|
|
366
|
+
def find_video_viewport(
|
|
367
|
+
video,
|
|
368
|
+
directory,
|
|
369
|
+
find_viewport,
|
|
370
|
+
viewport_time,
|
|
371
|
+
viewport_retries,
|
|
372
|
+
viewport_min_height,
|
|
373
|
+
viewport_min_width,
|
|
374
|
+
):
|
|
358
375
|
logging.debug("Finding Video Viewport...")
|
|
359
376
|
viewport = None
|
|
360
377
|
try:
|
|
361
378
|
from PIL import Image
|
|
362
379
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
background = pixels[x, y]
|
|
384
|
-
top = None
|
|
385
|
-
while top is None and y < middle:
|
|
386
|
-
if not colors_are_similar(background, pixels[x, y]):
|
|
387
|
-
top = y
|
|
388
|
-
else:
|
|
389
|
-
y += 1
|
|
390
|
-
if top is None:
|
|
391
|
-
top = 0
|
|
392
|
-
logging.debug("Window top edge is {0:d}".format(top))
|
|
393
|
-
|
|
394
|
-
# Find the bottom edge
|
|
395
|
-
x = 0
|
|
396
|
-
y = height - 1
|
|
397
|
-
bottom = None
|
|
398
|
-
while bottom is None and y > middle:
|
|
399
|
-
if not colors_are_similar(background, pixels[x, y]):
|
|
400
|
-
bottom = y
|
|
401
|
-
else:
|
|
402
|
-
y -= 1
|
|
403
|
-
if bottom is None:
|
|
404
|
-
bottom = height - 1
|
|
405
|
-
logging.debug("Window bottom edge is {0:d}".format(bottom))
|
|
380
|
+
retries = -1
|
|
381
|
+
|
|
382
|
+
while (
|
|
383
|
+
viewport is None
|
|
384
|
+
or viewport["height"] <= viewport_min_height
|
|
385
|
+
or viewport["width"] <= viewport_min_width
|
|
386
|
+
):
|
|
387
|
+
retries += 1
|
|
388
|
+
if retries >= 1:
|
|
389
|
+
# In some cases, the first frame is not an orange screen or a screen
|
|
390
|
+
# with a solid color. In this case, we need to try finding the viewport
|
|
391
|
+
# using the next frame. The `viewport_retries` dictates the maximum number
|
|
392
|
+
# of frames to check.
|
|
393
|
+
if retries >= viewport_retries:
|
|
394
|
+
logging.exception(
|
|
395
|
+
"Could not calculate a viewport after %s tries.",
|
|
396
|
+
viewport_retries,
|
|
397
|
+
)
|
|
398
|
+
break
|
|
399
|
+
logging.info("Failed to find a good viewport. Retrying...")
|
|
406
400
|
|
|
407
|
-
|
|
401
|
+
logging.debug("Using frame " + str(retries))
|
|
408
402
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
viewport = {"x": 0, "y": 0, "width": width, "height": height}
|
|
413
|
-
os.remove(frame)
|
|
403
|
+
frame = os.path.join(directory, "viewport.png")
|
|
404
|
+
if os.path.isfile(frame):
|
|
405
|
+
os.remove(frame)
|
|
414
406
|
|
|
415
|
-
|
|
407
|
+
command = ["ffmpeg", "-i", video]
|
|
408
|
+
if viewport_time:
|
|
409
|
+
command.extend(["-ss", viewport_time])
|
|
410
|
+
|
|
411
|
+
# Pull one frame from the video starting with the frame at
|
|
412
|
+
# the `retries` index
|
|
413
|
+
command.extend(["-vf", "select=gte(n\\,%s)" % retries])
|
|
414
|
+
command.extend(["-frames:v", "1", frame])
|
|
415
|
+
subprocess.check_output(command)
|
|
416
|
+
|
|
417
|
+
if os.path.isfile(frame):
|
|
418
|
+
with Image.open(frame) as im:
|
|
419
|
+
width, height = im.size
|
|
420
|
+
logging.debug("%s is %dx%d", frame, width, height)
|
|
421
|
+
if options.notification:
|
|
422
|
+
im = Image.open(frame)
|
|
423
|
+
pixels = im.load()
|
|
424
|
+
middle = int(math.floor(height / 2))
|
|
425
|
+
# Find the top edge (at ~40% in to deal with browsers that
|
|
426
|
+
# color the notification area)
|
|
427
|
+
x = int(width * 0.4)
|
|
428
|
+
y = 0
|
|
429
|
+
background = pixels[x, y]
|
|
430
|
+
top = None
|
|
431
|
+
while top is None and y < middle:
|
|
432
|
+
if not colors_are_similar(background, pixels[x, y]):
|
|
433
|
+
top = y
|
|
434
|
+
else:
|
|
435
|
+
y += 1
|
|
436
|
+
if top is None:
|
|
437
|
+
top = 0
|
|
438
|
+
logging.debug("Window top edge is {0:d}".format(top))
|
|
439
|
+
|
|
440
|
+
# Find the bottom edge
|
|
441
|
+
x = 0
|
|
442
|
+
y = height - 1
|
|
443
|
+
bottom = None
|
|
444
|
+
while bottom is None and y > middle:
|
|
445
|
+
if not colors_are_similar(background, pixels[x, y]):
|
|
446
|
+
bottom = y
|
|
447
|
+
else:
|
|
448
|
+
y -= 1
|
|
449
|
+
if bottom is None:
|
|
450
|
+
bottom = height - 1
|
|
451
|
+
logging.debug("Window bottom edge is {0:d}".format(bottom))
|
|
452
|
+
|
|
453
|
+
viewport = {
|
|
454
|
+
"x": 0,
|
|
455
|
+
"y": top,
|
|
456
|
+
"width": width,
|
|
457
|
+
"height": (bottom - top),
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
elif find_viewport:
|
|
461
|
+
viewport = find_image_viewport(frame)
|
|
462
|
+
else:
|
|
463
|
+
viewport = {"x": 0, "y": 0, "width": width, "height": height}
|
|
464
|
+
os.remove(frame)
|
|
465
|
+
|
|
466
|
+
except Exception as e:
|
|
416
467
|
viewport = None
|
|
417
468
|
|
|
418
469
|
return viewport
|
|
@@ -787,10 +838,14 @@ def crop_viewport(directory):
|
|
|
787
838
|
def get_decimate_filter():
|
|
788
839
|
decimate = None
|
|
789
840
|
try:
|
|
790
|
-
if
|
|
791
|
-
filters = subprocess.check_output(
|
|
841
|
+
if sys.version_info > (3, 0):
|
|
842
|
+
filters = subprocess.check_output(
|
|
843
|
+
["ffmpeg", "-filters"], stderr=subprocess.STDOUT, encoding="UTF-8"
|
|
844
|
+
)
|
|
792
845
|
else:
|
|
793
|
-
filters = subprocess.check_output(
|
|
846
|
+
filters = subprocess.check_output(
|
|
847
|
+
["ffmpeg", "-filters"], stderr=subprocess.STDOUT
|
|
848
|
+
)
|
|
794
849
|
lines = filters.split("\n")
|
|
795
850
|
match = re.compile(
|
|
796
851
|
r"(?P<filter>[\w]*decimate).*V->V.*Remove near-duplicate frames"
|
|
@@ -864,10 +919,14 @@ def is_color_frame(file, color_file):
|
|
|
864
919
|
image_magick["compare"],
|
|
865
920
|
)
|
|
866
921
|
|
|
867
|
-
if
|
|
868
|
-
|
|
922
|
+
if sys.version_info > (3, 0):
|
|
923
|
+
compare = subprocess.Popen(
|
|
924
|
+
command, stderr=subprocess.PIPE, shell=True, encoding="UTF-8"
|
|
925
|
+
)
|
|
869
926
|
else:
|
|
870
|
-
compare = subprocess.Popen(
|
|
927
|
+
compare = subprocess.Popen(
|
|
928
|
+
command, stderr=subprocess.PIPE, shell=True
|
|
929
|
+
)
|
|
871
930
|
out, err = compare.communicate()
|
|
872
931
|
if re.match("^[0-9]+$", err):
|
|
873
932
|
different_pixels = int(err)
|
|
@@ -908,9 +967,11 @@ def is_white_frame(file, white_file):
|
|
|
908
967
|
).format(
|
|
909
968
|
image_magick["convert"], white_file, file, crop, image_magick["compare"]
|
|
910
969
|
)
|
|
911
|
-
if
|
|
912
|
-
compare = subprocess.Popen(
|
|
913
|
-
|
|
970
|
+
if sys.version_info > (3, 0):
|
|
971
|
+
compare = subprocess.Popen(
|
|
972
|
+
command, stderr=subprocess.PIPE, shell=True, encoding="UTF-8"
|
|
973
|
+
)
|
|
974
|
+
else:
|
|
914
975
|
compare = subprocess.Popen(command, stderr=subprocess.PIPE, shell=True)
|
|
915
976
|
out, err = compare.communicate()
|
|
916
977
|
if re.match("^[0-9]+$", err):
|
|
@@ -966,9 +1027,11 @@ def frames_match(image1, image2, fuzz_percent, max_differences, crop_region, mas
|
|
|
966
1027
|
)
|
|
967
1028
|
if platform.system() != "Windows":
|
|
968
1029
|
command = command.replace("(", "\\(").replace(")", "\\)")
|
|
969
|
-
if
|
|
970
|
-
compare = subprocess.Popen(
|
|
971
|
-
|
|
1030
|
+
if sys.version_info > (3, 0):
|
|
1031
|
+
compare = subprocess.Popen(
|
|
1032
|
+
command, stderr=subprocess.PIPE, shell=True, encoding="UTF-8"
|
|
1033
|
+
)
|
|
1034
|
+
else:
|
|
972
1035
|
compare = subprocess.Popen(command, stderr=subprocess.PIPE, shell=True)
|
|
973
1036
|
out, err = compare.communicate()
|
|
974
1037
|
if re.match("^[0-9]+$", err):
|
|
@@ -1686,7 +1749,7 @@ def calculate_contentful_speed_index(progress, directory):
|
|
|
1686
1749
|
command = "{0} {1} -canny 2x2+8%+8% -define histogram:unique-colors=true -format %c histogram:info:-".format(
|
|
1687
1750
|
image_magick["convert"], current_frame
|
|
1688
1751
|
)
|
|
1689
|
-
output = subprocess.check_output(command, shell=True).decode(
|
|
1752
|
+
output = subprocess.check_output(command, shell=True).decode("utf-8")
|
|
1690
1753
|
logging.debug("Output %s" % output)
|
|
1691
1754
|
|
|
1692
1755
|
# Take the last matching pixel count, which is the pixel count from the last
|
|
@@ -1887,28 +1950,28 @@ def calculate_hero_time(progress, directory, hero, viewport):
|
|
|
1887
1950
|
def check_config():
|
|
1888
1951
|
ok = True
|
|
1889
1952
|
|
|
1890
|
-
print("ffmpeg: "
|
|
1953
|
+
print("ffmpeg: ")
|
|
1891
1954
|
if get_decimate_filter() is not None:
|
|
1892
1955
|
print("OK")
|
|
1893
1956
|
else:
|
|
1894
1957
|
print("FAIL")
|
|
1895
1958
|
ok = False
|
|
1896
1959
|
|
|
1897
|
-
print("convert: "
|
|
1960
|
+
print("convert: ")
|
|
1898
1961
|
if check_process("{0} -version".format(image_magick["convert"]), "ImageMagick"):
|
|
1899
1962
|
print("OK")
|
|
1900
1963
|
else:
|
|
1901
1964
|
print("FAIL")
|
|
1902
1965
|
ok = False
|
|
1903
1966
|
|
|
1904
|
-
print("compare: "
|
|
1967
|
+
print("compare: ")
|
|
1905
1968
|
if check_process("{0} -version".format(image_magick["compare"]), "ImageMagick"):
|
|
1906
1969
|
print("OK")
|
|
1907
1970
|
else:
|
|
1908
1971
|
print("FAIL")
|
|
1909
1972
|
ok = False
|
|
1910
1973
|
|
|
1911
|
-
print("Pillow: "
|
|
1974
|
+
print("Pillow: ")
|
|
1912
1975
|
try:
|
|
1913
1976
|
from PIL import Image, ImageDraw # noqa
|
|
1914
1977
|
|
|
@@ -1917,7 +1980,7 @@ def check_config():
|
|
|
1917
1980
|
print("FAIL")
|
|
1918
1981
|
ok = False
|
|
1919
1982
|
|
|
1920
|
-
print("SSIM: "
|
|
1983
|
+
print("SSIM: ")
|
|
1921
1984
|
try:
|
|
1922
1985
|
from ssim import compute_ssim # noqa
|
|
1923
1986
|
|
|
@@ -1932,8 +1995,10 @@ def check_config():
|
|
|
1932
1995
|
def check_process(command, output):
|
|
1933
1996
|
ok = False
|
|
1934
1997
|
try:
|
|
1935
|
-
if
|
|
1936
|
-
out = subprocess.check_output(
|
|
1998
|
+
if sys.version_info > (3, 0):
|
|
1999
|
+
out = subprocess.check_output(
|
|
2000
|
+
command, stderr=subprocess.STDOUT, shell=True, encoding="UTF-8"
|
|
2001
|
+
)
|
|
1937
2002
|
else:
|
|
1938
2003
|
out = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True)
|
|
1939
2004
|
if out.find(output) > -1:
|
|
@@ -1976,7 +2041,7 @@ def main():
|
|
|
1976
2041
|
"--logfile", help="Write log messages to given file instead of stdout"
|
|
1977
2042
|
)
|
|
1978
2043
|
parser.add_argument(
|
|
1979
|
-
|
|
2044
|
+
"--logformat",
|
|
1980
2045
|
help="Formatting for the log messages",
|
|
1981
2046
|
default="%(asctime)s.%(msecs)03d - %(message)s",
|
|
1982
2047
|
)
|
|
@@ -2081,6 +2146,28 @@ def main():
|
|
|
2081
2146
|
help="Time of the video frame to use for identifying the viewport "
|
|
2082
2147
|
"(in HH:MM:SS.xx format).",
|
|
2083
2148
|
)
|
|
2149
|
+
parser.add_argument(
|
|
2150
|
+
"--viewportretries",
|
|
2151
|
+
type=int,
|
|
2152
|
+
default=5,
|
|
2153
|
+
help="Number of times to attempt to obtain a viewport. Analagous to the "
|
|
2154
|
+
"number of frames to try to find a viewport with. By default, up to the "
|
|
2155
|
+
"first 5 frames are used.",
|
|
2156
|
+
)
|
|
2157
|
+
parser.add_argument(
|
|
2158
|
+
"--viewportminheight",
|
|
2159
|
+
type=int,
|
|
2160
|
+
default=0,
|
|
2161
|
+
help="The minimum possible height (in pixels) for the viewport. Used when "
|
|
2162
|
+
"attempting to find the viewport size. Defaults to 0.",
|
|
2163
|
+
)
|
|
2164
|
+
parser.add_argument(
|
|
2165
|
+
"--viewportminwidth",
|
|
2166
|
+
type=int,
|
|
2167
|
+
default=0,
|
|
2168
|
+
help="The minimum possible width (in pixels) for the viewport. Used when "
|
|
2169
|
+
"attempting to find the viewport size. Defaults to 0.",
|
|
2170
|
+
)
|
|
2084
2171
|
parser.add_argument(
|
|
2085
2172
|
"-s",
|
|
2086
2173
|
"--start",
|
|
@@ -2213,9 +2300,7 @@ def main():
|
|
|
2213
2300
|
)
|
|
2214
2301
|
else:
|
|
2215
2302
|
logging.basicConfig(
|
|
2216
|
-
level=log_level,
|
|
2217
|
-
format=options.logformat,
|
|
2218
|
-
datefmt="%H:%M:%S",
|
|
2303
|
+
level=log_level, format=options.logformat, datefmt="%H:%M:%S"
|
|
2219
2304
|
)
|
|
2220
2305
|
|
|
2221
2306
|
if options.multiple:
|
|
@@ -2285,6 +2370,9 @@ def main():
|
|
|
2285
2370
|
options.multiple,
|
|
2286
2371
|
options.viewport,
|
|
2287
2372
|
options.viewporttime,
|
|
2373
|
+
options.viewportretries,
|
|
2374
|
+
options.viewportminheight,
|
|
2375
|
+
options.viewportminwidth,
|
|
2288
2376
|
options.full,
|
|
2289
2377
|
options.timeline,
|
|
2290
2378
|
options.trimend,
|
|
@@ -25,6 +25,10 @@ module.exports.configureBuilder = function (builder, baseDir, options) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const serviceBuilder = new chrome.ServiceBuilder(chromedriverPath);
|
|
28
|
+
|
|
29
|
+
// Remove the check that matches the Chromedriver version with Chrome version.
|
|
30
|
+
serviceBuilder.addArguments('--disable-build-check');
|
|
31
|
+
|
|
28
32
|
if (options.chrome && options.chrome.chromedriverPort) {
|
|
29
33
|
serviceBuilder.setPort(options.chrome.chromedriverPort);
|
|
30
34
|
}
|
|
@@ -38,7 +42,6 @@ module.exports.configureBuilder = function (builder, baseDir, options) {
|
|
|
38
42
|
serviceBuilder.enableVerboseLogging();
|
|
39
43
|
}
|
|
40
44
|
chrome.setDefaultService(serviceBuilder.build());
|
|
41
|
-
|
|
42
45
|
hasConfiguredChromeDriverService = true;
|
|
43
46
|
}
|
|
44
47
|
|