browsertime 26.3.2 → 27.0.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 +72 -0
- package/README.md +1 -1
- package/browserscripts/timings/softNavigations.js +94 -0
- package/lib/chrome/har.js +36 -5
- package/lib/chrome/settings/chromeAndroidOptions.js +2 -1
- package/lib/chrome/webdriver/chromium.js +40 -1
- package/lib/chrome/webdriver/setupChromiumOptions.js +86 -31
- package/lib/core/engine/collector.js +340 -317
- package/lib/core/engine/command/addText.js +93 -12
- package/lib/core/engine/command/click.js +268 -139
- package/lib/core/engine/command/commandHelper.js +45 -0
- package/lib/core/engine/command/cookie.js +85 -0
- package/lib/core/engine/command/element.js +37 -2
- package/lib/core/engine/command/javaScript.js +14 -24
- package/lib/core/engine/command/measure/screenshots.js +114 -0
- package/lib/core/engine/command/measure/video.js +85 -0
- package/lib/core/engine/command/measure.js +104 -180
- package/lib/core/engine/command/mouse/clickAndHold.js +6 -3
- package/lib/core/engine/command/mouse/contextClick.js +56 -4
- package/lib/core/engine/command/mouse/doubleClick.js +49 -1
- package/lib/core/engine/command/mouse/mouseMove.js +50 -1
- package/lib/core/engine/command/mouse/singleClick.js +165 -129
- package/lib/core/engine/command/navigation.js +6 -6
- package/lib/core/engine/command/select.js +95 -3
- package/lib/core/engine/command/selectorParser.js +58 -0
- package/lib/core/engine/command/set.js +55 -1
- package/lib/core/engine/command/wait.js +41 -2
- package/lib/core/engine/commands.js +550 -42
- package/lib/core/engine/index.js +206 -150
- package/lib/core/engine/iteration.js +277 -137
- package/lib/firefox/networkManager.js +22 -30
- package/lib/firefox/webdriver/builder.js +14 -0
- package/lib/safari/har.js +131 -0
- package/lib/safari/native/ios-capture +0 -0
- package/lib/safari/native/ios-capture.m +340 -0
- package/lib/safari/safariInspectorClient.js +322 -0
- package/lib/safari/webdriver/safari.js +303 -13
- package/lib/safari/webkitToCdpAdapter.js +241 -0
- package/lib/support/cli.js +13 -0
- package/lib/support/engineUtils.js +16 -5
- package/lib/support/getViewPort.js +5 -0
- package/lib/support/har/index.js +41 -3
- package/lib/support/usbPower.js +2 -3
- package/lib/video/postprocessing/finetune/addTextToVideo.js +4 -0
- package/lib/video/postprocessing/finetune/getFont.js +3 -3
- package/lib/video/postprocessing/finetune/getTimingMetrics.js +7 -0
- package/lib/video/postprocessing/finetune/index.js +26 -9
- package/lib/video/screenRecording/ios/iosDeviceRecorder.js +109 -0
- package/lib/video/screenRecording/recorder.js +2 -2
- package/lib/video/screenRecording/setOrangeBackground.js +3 -0
- package/package.json +11 -11
- package/types/core/engine/command/addText.d.ts +35 -6
- package/types/core/engine/command/addText.d.ts.map +1 -1
- package/types/core/engine/command/click.d.ts +87 -16
- package/types/core/engine/command/click.d.ts.map +1 -1
- package/types/core/engine/command/commandHelper.d.ts +16 -0
- package/types/core/engine/command/commandHelper.d.ts.map +1 -0
- package/types/core/engine/command/cookie.d.ts +65 -0
- package/types/core/engine/command/cookie.d.ts.map +1 -0
- package/types/core/engine/command/element.d.ts +19 -1
- package/types/core/engine/command/element.d.ts.map +1 -1
- package/types/core/engine/command/javaScript.d.ts +4 -0
- package/types/core/engine/command/javaScript.d.ts.map +1 -1
- package/types/core/engine/command/measure/screenshots.d.ts +15 -0
- package/types/core/engine/command/measure/screenshots.d.ts.map +1 -0
- package/types/core/engine/command/measure/video.d.ts +26 -0
- package/types/core/engine/command/measure/video.d.ts.map +1 -0
- package/types/core/engine/command/measure.d.ts +59 -33
- package/types/core/engine/command/measure.d.ts.map +1 -1
- package/types/core/engine/command/mouse/clickAndHold.d.ts.map +1 -1
- package/types/core/engine/command/mouse/contextClick.d.ts +28 -4
- package/types/core/engine/command/mouse/contextClick.d.ts.map +1 -1
- package/types/core/engine/command/mouse/doubleClick.d.ts +24 -4
- package/types/core/engine/command/mouse/doubleClick.d.ts.map +1 -1
- package/types/core/engine/command/mouse/mouseMove.d.ts +30 -5
- package/types/core/engine/command/mouse/mouseMove.d.ts.map +1 -1
- package/types/core/engine/command/mouse/singleClick.d.ts +50 -13
- package/types/core/engine/command/mouse/singleClick.d.ts.map +1 -1
- package/types/core/engine/command/navigation.d.ts +3 -3
- package/types/core/engine/command/select.d.ts +45 -8
- package/types/core/engine/command/select.d.ts.map +1 -1
- package/types/core/engine/command/selectorParser.d.ts +20 -0
- package/types/core/engine/command/selectorParser.d.ts.map +1 -0
- package/types/core/engine/command/set.d.ts +34 -7
- package/types/core/engine/command/set.d.ts.map +1 -1
- package/types/core/engine/command/wait.d.ts +29 -6
- package/types/core/engine/command/wait.d.ts.map +1 -1
- package/types/core/engine/commands.d.ts +296 -28
- package/types/core/engine/commands.d.ts.map +1 -1
- package/types/support/engineUtils.d.ts.map +1 -1
- package/types/support/getViewPort.d.ts.map +1 -1
- package/types/support/usbPower.d.ts.map +1 -1
- package/types/video/postprocessing/finetune/addTextToVideo.d.ts.map +1 -1
- package/types/video/postprocessing/finetune/getTimingMetrics.d.ts.map +1 -1
- package/types/video/postprocessing/finetune/index.d.ts.map +1 -1
- package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts +28 -0
- package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts.map +1 -0
- package/types/video/screenRecording/recorder.d.ts +2 -2
- package/types/video/screenRecording/recorder.d.ts.map +1 -1
- package/types/video/screenRecording/setOrangeBackground.d.ts.map +1 -1
- package/types/video/video.d.ts +1 -1
- package/types/video/video.d.ts.map +1 -1
- package/visualmetrics/visualmetrics-portable.py +136 -136
- package/lib/video/screenRecording/ios/convertToMp4.js +0 -22
- package/lib/video/screenRecording/ios/iosRecorder.js +0 -68
|
@@ -36,12 +36,12 @@ import gzip
|
|
|
36
36
|
import json
|
|
37
37
|
import logging
|
|
38
38
|
import math
|
|
39
|
-
import os
|
|
40
39
|
import re
|
|
41
40
|
import sys
|
|
42
41
|
import shutil
|
|
43
42
|
import subprocess
|
|
44
43
|
import tempfile
|
|
44
|
+
from pathlib import Path
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
GZIP_TEXT = "wt"
|
|
@@ -254,8 +254,9 @@ def build_edge_video(video_path, viewport):
|
|
|
254
254
|
"""
|
|
255
255
|
logging.debug("Creating edge video for {0}".format(video_path))
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
_video_path = Path(video_path)
|
|
258
|
+
output_dir = str(_video_path.parent)
|
|
259
|
+
video_name = _video_path.stem
|
|
259
260
|
|
|
260
261
|
try:
|
|
261
262
|
import cv2
|
|
@@ -287,14 +288,14 @@ def build_edge_video(video_path, viewport):
|
|
|
287
288
|
|
|
288
289
|
out_size = edge_video[-1].shape
|
|
289
290
|
out_edges = cv2.VideoWriter(
|
|
290
|
-
|
|
291
|
+
str(Path(output_dir) / (video_name + "-edges.mp4")),
|
|
291
292
|
cv2.VideoWriter_fourcc(*"MP4V"),
|
|
292
293
|
frame_count,
|
|
293
294
|
(out_size[1], out_size[0]),
|
|
294
295
|
1,
|
|
295
296
|
)
|
|
296
297
|
out_edges_overlay = cv2.VideoWriter(
|
|
297
|
-
|
|
298
|
+
str(Path(output_dir) / (video_name + "-edges-overlay.mp4")),
|
|
298
299
|
cv2.VideoWriter_fourcc(*"MP4V"),
|
|
299
300
|
frame_count,
|
|
300
301
|
(out_size[1], out_size[0]),
|
|
@@ -376,21 +377,21 @@ def video_to_frames(
|
|
|
376
377
|
):
|
|
377
378
|
"""Extract the video frames"""
|
|
378
379
|
global client_viewport
|
|
379
|
-
first_frame =
|
|
380
|
+
first_frame = str(Path(directory) / "ms_000000")
|
|
380
381
|
if (
|
|
381
|
-
not
|
|
382
|
-
and not
|
|
382
|
+
not Path(first_frame + ".png").is_file()
|
|
383
|
+
and not Path(first_frame + ".jpg").is_file()
|
|
383
384
|
) or force:
|
|
384
|
-
if
|
|
385
|
-
video =
|
|
385
|
+
if Path(video).is_file():
|
|
386
|
+
video = str(Path(video).resolve())
|
|
386
387
|
logging.info("Processing frames from video " + video + " to " + directory)
|
|
387
388
|
is_mobile = find_recording_platform(video)
|
|
388
|
-
if
|
|
389
|
+
if Path(directory).is_dir():
|
|
389
390
|
shutil.rmtree(directory, True)
|
|
390
|
-
if not
|
|
391
|
-
|
|
392
|
-
if
|
|
393
|
-
directory =
|
|
391
|
+
if not Path(directory).is_dir():
|
|
392
|
+
Path(directory).mkdir(mode=0o755)
|
|
393
|
+
if Path(directory).is_dir():
|
|
394
|
+
directory = str(Path(directory).resolve())
|
|
394
395
|
viewport, cropped = find_video_viewport(
|
|
395
396
|
video,
|
|
396
397
|
directory,
|
|
@@ -457,14 +458,17 @@ def extract_frames(video, directory, full_resolution, viewport):
|
|
|
457
458
|
"0",
|
|
458
459
|
"-vf",
|
|
459
460
|
crop + scale + decimate + "=0:64:640:0.001",
|
|
460
|
-
|
|
461
|
+
str(Path(dir_escaped) / "img-%d.png"),
|
|
461
462
|
]
|
|
462
463
|
logging.debug(" ".join(command))
|
|
463
|
-
lines = []
|
|
464
464
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
465
|
+
# Use subprocess.run so the full stderr stream is read after the
|
|
466
|
+
# process exits. The previous Popen + poll loop stopped reading once
|
|
467
|
+
# poll() returned, dropping any buffered tail; with `-v debug` ffmpeg
|
|
468
|
+
# also writes enough to fill the OS pipe buffer and could deadlock
|
|
469
|
+
# blocked on write while we waited on poll().
|
|
470
|
+
result = subprocess.run(command, stderr=subprocess.PIPE, encoding="UTF-8")
|
|
471
|
+
lines = result.stderr.splitlines()
|
|
468
472
|
|
|
469
473
|
pattern = re.compile(r"keep pts:[0-9]+ pts_time:(?P<timecode>[0-9\.]+)")
|
|
470
474
|
frame_count = 0
|
|
@@ -475,10 +479,10 @@ def extract_frames(video, directory, full_resolution, viewport):
|
|
|
475
479
|
frame_time = int(
|
|
476
480
|
math.floor(float(match.groupdict().get("timecode")) * 1000)
|
|
477
481
|
)
|
|
478
|
-
src =
|
|
479
|
-
dest =
|
|
482
|
+
src = str(Path(directory) / "img-{0:d}.png".format(frame_count))
|
|
483
|
+
dest = str(Path(directory) / "video-{0:06d}.png".format(frame_time))
|
|
480
484
|
logging.debug("Renaming " + src + " to " + dest)
|
|
481
|
-
|
|
485
|
+
Path(src).rename(dest)
|
|
482
486
|
ret = True
|
|
483
487
|
return ret
|
|
484
488
|
|
|
@@ -492,11 +496,8 @@ def find_recording_platform(video):
|
|
|
492
496
|
command = ["ffprobe", video]
|
|
493
497
|
logging.debug(command)
|
|
494
498
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
while proc.poll() is None:
|
|
499
|
-
lines.extend(iter(proc.stderr.readline, ""))
|
|
499
|
+
result = subprocess.run(command, stderr=subprocess.PIPE, encoding="UTF-8")
|
|
500
|
+
lines = result.stderr.splitlines()
|
|
500
501
|
|
|
501
502
|
is_mobile = False
|
|
502
503
|
matcher = re.compile(".*com\.android\.version.*")
|
|
@@ -509,7 +510,7 @@ def find_recording_platform(video):
|
|
|
509
510
|
|
|
510
511
|
def remove_frames_before_orange(directory, orange_file):
|
|
511
512
|
"""Remove stray frames from the start of the video"""
|
|
512
|
-
frames = sorted(glob.glob(
|
|
513
|
+
frames = sorted(glob.glob(str(Path(directory) / "video-*.png")))
|
|
513
514
|
if len(frames):
|
|
514
515
|
# go through the first 20 frames and remove any that come before the first orange frame.
|
|
515
516
|
# iOS video capture starts with a blank white frame and then flips to
|
|
@@ -530,24 +531,24 @@ def remove_frames_before_orange(directory, orange_file):
|
|
|
530
531
|
if found_orange and len(remove_frames):
|
|
531
532
|
for frame in remove_frames:
|
|
532
533
|
logging.debug("Removing pre-orange frame %s", frame)
|
|
533
|
-
|
|
534
|
+
Path(frame).unlink()
|
|
534
535
|
|
|
535
536
|
|
|
536
537
|
def remove_orange_frames(directory, orange_file):
|
|
537
538
|
"""Remove orange frames from the beginning of the video"""
|
|
538
|
-
frames = sorted(glob.glob(
|
|
539
|
+
frames = sorted(glob.glob(str(Path(directory) / "video-*.png")))
|
|
539
540
|
if len(frames):
|
|
540
541
|
logging.debug("Scanning for orange frames...")
|
|
541
542
|
for frame in frames:
|
|
542
543
|
if is_color_frame(frame, orange_file):
|
|
543
544
|
logging.debug("Removing Orange frame: %s", frame)
|
|
544
|
-
|
|
545
|
+
Path(frame).unlink()
|
|
545
546
|
else:
|
|
546
547
|
break
|
|
547
548
|
for frame in reversed(frames):
|
|
548
549
|
if is_color_frame(frame, orange_file):
|
|
549
550
|
logging.debug("Removing orange frame %s from the end", frame)
|
|
550
|
-
|
|
551
|
+
Path(frame).unlink()
|
|
551
552
|
else:
|
|
552
553
|
break
|
|
553
554
|
|
|
@@ -672,9 +673,9 @@ def find_video_viewport(
|
|
|
672
673
|
|
|
673
674
|
logging.debug("Using frame " + str(retries))
|
|
674
675
|
|
|
675
|
-
frame =
|
|
676
|
-
if
|
|
677
|
-
|
|
676
|
+
frame = str(Path(directory) / "viewport.png")
|
|
677
|
+
if Path(frame).is_file():
|
|
678
|
+
Path(frame).unlink()
|
|
678
679
|
|
|
679
680
|
command = ["ffmpeg", "-i", video]
|
|
680
681
|
|
|
@@ -684,7 +685,7 @@ def find_video_viewport(
|
|
|
684
685
|
command.extend(["-frames:v", "1", frame])
|
|
685
686
|
subprocess.check_output(command)
|
|
686
687
|
|
|
687
|
-
if
|
|
688
|
+
if Path(frame).is_file():
|
|
688
689
|
with Image.open(frame) as im:
|
|
689
690
|
width, height = im.size
|
|
690
691
|
logging.debug("%s is %dx%d", frame, width, height)
|
|
@@ -693,7 +694,7 @@ def find_video_viewport(
|
|
|
693
694
|
else:
|
|
694
695
|
viewport = {"x": 0, "y": 0, "width": width, "height": height}
|
|
695
696
|
|
|
696
|
-
|
|
697
|
+
Path(frame).unlink()
|
|
697
698
|
|
|
698
699
|
if viewport is not None and viewport != {
|
|
699
700
|
"x": 0,
|
|
@@ -711,7 +712,7 @@ def find_video_viewport(
|
|
|
711
712
|
|
|
712
713
|
def adjust_frame_times(directory):
|
|
713
714
|
offset = None
|
|
714
|
-
frames = sorted(glob.glob(
|
|
715
|
+
frames = sorted(glob.glob(str(Path(directory) / "video-*.png")))
|
|
715
716
|
# Special hack to the the video start
|
|
716
717
|
# Let us tune this in the future to skip using a global
|
|
717
718
|
global videoRecordingStart
|
|
@@ -726,8 +727,8 @@ def adjust_frame_times(directory):
|
|
|
726
727
|
videoRecordingStart = frame_time
|
|
727
728
|
offset = frame_time
|
|
728
729
|
new_time = frame_time - offset
|
|
729
|
-
dest =
|
|
730
|
-
|
|
730
|
+
dest = str(Path(directory) / "ms_{0:06d}.png".format(new_time))
|
|
731
|
+
Path(frame).rename(dest)
|
|
731
732
|
|
|
732
733
|
|
|
733
734
|
def find_render_start(directory, orange_file, cropped, is_mobile):
|
|
@@ -738,7 +739,7 @@ def find_render_start(directory, orange_file, cropped, is_mobile):
|
|
|
738
739
|
or options.viewport is not None
|
|
739
740
|
or (options.renderignore > 0 and options.renderignore <= 100)
|
|
740
741
|
):
|
|
741
|
-
files = sorted(glob.glob(
|
|
742
|
+
files = sorted(glob.glob(str(Path(directory) / "video-*.png")))
|
|
742
743
|
count = len(files)
|
|
743
744
|
if count > 1:
|
|
744
745
|
from PIL import Image
|
|
@@ -797,12 +798,12 @@ def find_render_start(directory, orange_file, cropped, is_mobile):
|
|
|
797
798
|
for i in range(1, count):
|
|
798
799
|
if frames_match(first, files[i], 10, 0, crop, mask):
|
|
799
800
|
logging.debug("Removing pre-render frame %s", files[i])
|
|
800
|
-
|
|
801
|
+
Path(files[i]).unlink()
|
|
801
802
|
elif orange_file is not None and is_color_frame(
|
|
802
803
|
files[i], orange_file
|
|
803
804
|
):
|
|
804
805
|
logging.debug("Removing orange frame %s", files[i])
|
|
805
|
-
|
|
806
|
+
Path(files[i]).unlink()
|
|
806
807
|
else:
|
|
807
808
|
break
|
|
808
809
|
except BaseException:
|
|
@@ -813,7 +814,7 @@ def eliminate_duplicate_frames(directory, cropped, is_mobile):
|
|
|
813
814
|
logging.debug("Eliminating Duplicate Frames...")
|
|
814
815
|
global client_viewport
|
|
815
816
|
try:
|
|
816
|
-
files = sorted(glob.glob(
|
|
817
|
+
files = sorted(glob.glob(str(Path(directory) / "ms_*.png")))
|
|
817
818
|
if len(files) > 1:
|
|
818
819
|
from PIL import Image
|
|
819
820
|
|
|
@@ -867,14 +868,14 @@ def eliminate_duplicate_frames(directory, cropped, is_mobile):
|
|
|
867
868
|
files[i]
|
|
868
869
|
)
|
|
869
870
|
)
|
|
870
|
-
|
|
871
|
+
Path(files[i]).unlink()
|
|
871
872
|
else:
|
|
872
873
|
break
|
|
873
874
|
|
|
874
875
|
# Do another pass looking for the last frame but with an allowance for up
|
|
875
876
|
# to a 15% difference in individual pixels to deal with noise
|
|
876
877
|
# around text.
|
|
877
|
-
files = sorted(glob.glob(
|
|
878
|
+
files = sorted(glob.glob(str(Path(directory) / "ms_*.png")))
|
|
878
879
|
count = len(files)
|
|
879
880
|
duplicates = []
|
|
880
881
|
if count > 2:
|
|
@@ -891,7 +892,7 @@ def eliminate_duplicate_frames(directory, cropped, is_mobile):
|
|
|
891
892
|
previous_frame
|
|
892
893
|
)
|
|
893
894
|
)
|
|
894
|
-
|
|
895
|
+
Path(previous_frame).unlink()
|
|
895
896
|
previous_frame = files[i]
|
|
896
897
|
else:
|
|
897
898
|
break
|
|
@@ -899,7 +900,7 @@ def eliminate_duplicate_frames(directory, cropped, is_mobile):
|
|
|
899
900
|
logging.debug(
|
|
900
901
|
"Removing duplicate frame {0} from the end".format(duplicate)
|
|
901
902
|
)
|
|
902
|
-
|
|
903
|
+
Path(duplicate).unlink()
|
|
903
904
|
|
|
904
905
|
except BaseException:
|
|
905
906
|
logging.exception("Error processing frames for duplicates")
|
|
@@ -910,7 +911,7 @@ def crop_viewport(directory):
|
|
|
910
911
|
try:
|
|
911
912
|
from PIL import Image
|
|
912
913
|
|
|
913
|
-
files = sorted(glob.glob(
|
|
914
|
+
files = sorted(glob.glob(str(Path(directory) / "ms_*.png")))
|
|
914
915
|
count = len(files)
|
|
915
916
|
if count > 0:
|
|
916
917
|
for i in range(count):
|
|
@@ -950,15 +951,15 @@ def get_decimate_filter():
|
|
|
950
951
|
|
|
951
952
|
|
|
952
953
|
def clean_directory(directory):
|
|
953
|
-
files = glob.glob(
|
|
954
|
+
files = glob.glob(str(Path(directory) / "*.png"))
|
|
954
955
|
for file in files:
|
|
955
|
-
|
|
956
|
-
files = glob.glob(
|
|
956
|
+
Path(file).unlink()
|
|
957
|
+
files = glob.glob(str(Path(directory) / "*.jpg"))
|
|
957
958
|
for file in files:
|
|
958
|
-
|
|
959
|
-
files = glob.glob(
|
|
959
|
+
Path(file).unlink()
|
|
960
|
+
files = glob.glob(str(Path(directory) / "*.json"))
|
|
960
961
|
for file in files:
|
|
961
|
-
|
|
962
|
+
Path(file).unlink()
|
|
962
963
|
|
|
963
964
|
|
|
964
965
|
def is_color_frame(file, color_file):
|
|
@@ -967,7 +968,7 @@ def is_color_frame(file, color_file):
|
|
|
967
968
|
if file in frame_cache and color_file in frame_cache[file]:
|
|
968
969
|
return bool(frame_cache[file][color_file])
|
|
969
970
|
match = False
|
|
970
|
-
if
|
|
971
|
+
if Path(color_file).is_file():
|
|
971
972
|
try:
|
|
972
973
|
from PIL import Image
|
|
973
974
|
|
|
@@ -1087,18 +1088,18 @@ def generate_orange_png(orange_file):
|
|
|
1087
1088
|
|
|
1088
1089
|
def calculate_histograms(directory, histograms_file, force):
|
|
1089
1090
|
logging.debug("Calculating image histograms")
|
|
1090
|
-
if not
|
|
1091
|
+
if not Path(histograms_file).is_file() or force:
|
|
1091
1092
|
try:
|
|
1092
1093
|
extension = None
|
|
1093
|
-
directory =
|
|
1094
|
-
first_frame =
|
|
1095
|
-
if
|
|
1094
|
+
directory = str(Path(directory).resolve())
|
|
1095
|
+
first_frame = str(Path(directory) / "ms_000000")
|
|
1096
|
+
if Path(first_frame + ".png").is_file():
|
|
1096
1097
|
extension = ".png"
|
|
1097
|
-
elif
|
|
1098
|
+
elif Path(first_frame + ".jpg").is_file():
|
|
1098
1099
|
extension = ".jpg"
|
|
1099
1100
|
if extension is not None:
|
|
1100
1101
|
histograms = []
|
|
1101
|
-
frames = sorted(glob.glob(
|
|
1102
|
+
frames = sorted(glob.glob(str(Path(directory) / ("ms_*" + extension))))
|
|
1102
1103
|
match = re.compile(r"ms_(?P<ms>[0-9]+)\.")
|
|
1103
1104
|
for frame in frames:
|
|
1104
1105
|
m = re.search(match, frame)
|
|
@@ -1110,14 +1111,14 @@ def calculate_histograms(directory, histograms_file, force):
|
|
|
1110
1111
|
histograms.append(
|
|
1111
1112
|
{
|
|
1112
1113
|
"time": frame_time,
|
|
1113
|
-
"file":
|
|
1114
|
+
"file": Path(frame).name,
|
|
1114
1115
|
"histogram": histogram,
|
|
1115
1116
|
"total_pixels": total,
|
|
1116
1117
|
"dropped_pixels": dropped,
|
|
1117
1118
|
}
|
|
1118
1119
|
)
|
|
1119
|
-
if
|
|
1120
|
-
|
|
1120
|
+
if Path(histograms_file).is_file():
|
|
1121
|
+
Path(histograms_file).unlink()
|
|
1121
1122
|
f = gzip.open(histograms_file, GZIP_TEXT)
|
|
1122
1123
|
json.dump(histograms, f)
|
|
1123
1124
|
f.close()
|
|
@@ -1173,8 +1174,8 @@ def calculate_image_histogram(file):
|
|
|
1173
1174
|
|
|
1174
1175
|
|
|
1175
1176
|
def save_screenshot(directory, dest, quality):
|
|
1176
|
-
directory =
|
|
1177
|
-
files = sorted(glob.glob(
|
|
1177
|
+
directory = str(Path(directory).resolve())
|
|
1178
|
+
files = sorted(glob.glob(str(Path(directory) / "ms_*.png")))
|
|
1178
1179
|
if files is not None and len(files) >= 1:
|
|
1179
1180
|
src = files[-1]
|
|
1180
1181
|
if dest[-4:] == ".jpg":
|
|
@@ -1190,17 +1191,16 @@ def save_screenshot(directory, dest, quality):
|
|
|
1190
1191
|
|
|
1191
1192
|
def convert_to_jpeg(directory, quality):
|
|
1192
1193
|
logging.debug("Converting video frames to JPEG")
|
|
1193
|
-
directory =
|
|
1194
|
-
pattern =
|
|
1194
|
+
directory = str(Path(directory).resolve())
|
|
1195
|
+
pattern = str(Path(directory) / "ms_*.png")
|
|
1195
1196
|
|
|
1196
1197
|
files = sorted(glob.glob(pattern))
|
|
1197
1198
|
for file in files:
|
|
1198
|
-
|
|
1199
|
-
filen, ext = os.path.splitext(filename)
|
|
1199
|
+
filen = Path(file).stem
|
|
1200
1200
|
convert_img_to_jpeg(
|
|
1201
|
-
file,
|
|
1201
|
+
file, str(Path(directory) / (filen + ".jpg")), quality=quality
|
|
1202
1202
|
)
|
|
1203
|
-
|
|
1203
|
+
Path(file).unlink()
|
|
1204
1204
|
|
|
1205
1205
|
logging.debug("Done converting video frames to JPEG")
|
|
1206
1206
|
|
|
@@ -1226,8 +1226,7 @@ def calculate_visual_metrics(
|
|
|
1226
1226
|
if histograms is not None and len(histograms) > 0:
|
|
1227
1227
|
progress = calculate_visual_progress(histograms)
|
|
1228
1228
|
if progress and progress_file is not None:
|
|
1229
|
-
|
|
1230
|
-
if ext.lower() == ".gz":
|
|
1229
|
+
if Path(progress_file).suffix.lower() == ".gz":
|
|
1231
1230
|
f = gzip.open(progress_file, GZIP_TEXT, 7)
|
|
1232
1231
|
else:
|
|
1233
1232
|
f = open(progress_file, "w")
|
|
@@ -1263,7 +1262,7 @@ def calculate_visual_metrics(
|
|
|
1263
1262
|
},
|
|
1264
1263
|
)
|
|
1265
1264
|
)
|
|
1266
|
-
if hero_elements_file is not None and
|
|
1265
|
+
if hero_elements_file is not None and Path(hero_elements_file).is_file():
|
|
1267
1266
|
logging.debug("Calculating hero element times")
|
|
1268
1267
|
hero_data = None
|
|
1269
1268
|
hero_f_in = gzip.open(hero_elements_file, GZIP_READ_TEXT)
|
|
@@ -1350,7 +1349,7 @@ def calculate_visual_metrics(
|
|
|
1350
1349
|
|
|
1351
1350
|
def load_histograms(histograms_file, start, end):
|
|
1352
1351
|
histograms = None
|
|
1353
|
-
if
|
|
1352
|
+
if Path(histograms_file).is_file():
|
|
1354
1353
|
f = gzip.open(histograms_file)
|
|
1355
1354
|
original = json.load(f)
|
|
1356
1355
|
f.close()
|
|
@@ -1448,7 +1447,7 @@ def calculate_visual_progress(histograms):
|
|
|
1448
1447
|
last = histograms[-1]["histogram"]
|
|
1449
1448
|
for index, histogram in enumerate(histograms):
|
|
1450
1449
|
p = calculate_frame_progress(histogram["histogram"], first, last)
|
|
1451
|
-
file_name
|
|
1450
|
+
file_name = Path(histogram["file"]).stem
|
|
1452
1451
|
progress.append({"time": histogram["time"], "file": file_name, "progress": p})
|
|
1453
1452
|
logging.debug("{0:d}ms - {1:d}% Complete".format(histogram["time"], int(p)))
|
|
1454
1453
|
return progress
|
|
@@ -1519,12 +1518,12 @@ def calculate_contentful_speed_index(progress, directory):
|
|
|
1519
1518
|
try:
|
|
1520
1519
|
from PIL import Image
|
|
1521
1520
|
|
|
1522
|
-
dir =
|
|
1521
|
+
dir = str(Path(__file__).resolve().parent / directory)
|
|
1523
1522
|
content = []
|
|
1524
1523
|
maxContent = 0
|
|
1525
1524
|
for p in progress[1:]:
|
|
1526
1525
|
# Full Path of the Current Frame
|
|
1527
|
-
current_frame =
|
|
1526
|
+
current_frame = str(Path(dir) / "ms_{0:06d}.png".format(p["time"]))
|
|
1528
1527
|
logging.debug("contentfulSpeedIndex: Current frame is %s" % current_frame)
|
|
1529
1528
|
|
|
1530
1529
|
value = 0
|
|
@@ -1565,51 +1564,59 @@ def calculate_contentful_speed_index(progress, directory):
|
|
|
1565
1564
|
|
|
1566
1565
|
|
|
1567
1566
|
def calculate_perceptual_speed_index(progress, directory):
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1567
|
+
# Match the behaviour of calculate_contentful_speed_index: any failure
|
|
1568
|
+
# here (missing frame, SSIM library error, etc.) must not propagate up
|
|
1569
|
+
# to calculate_visual_metrics, where it would discard SpeedIndex / FCP /
|
|
1570
|
+
# the rest of the metrics for this iteration.
|
|
1571
|
+
try:
|
|
1572
|
+
from ssim import compute_ssim
|
|
1573
|
+
|
|
1574
|
+
x = len(progress)
|
|
1575
|
+
dir = str(Path(__file__).resolve().parent / directory)
|
|
1576
|
+
first_paint_frame = str(Path(dir) / "ms_{0:06d}.png".format(progress[1]["time"]))
|
|
1577
|
+
target_frame = str(Path(dir) / "ms_{0:06d}.png".format(progress[x - 1]["time"]))
|
|
1578
|
+
ssim_1 = compute_ssim(first_paint_frame, target_frame)
|
|
1579
|
+
per_si = float(progress[1]["time"])
|
|
1580
|
+
last_ms = progress[1]["time"]
|
|
1581
|
+
# Full Path of the Target Frame
|
|
1582
|
+
logging.debug("Target image for perSI is %s" % target_frame)
|
|
1583
|
+
ssim = ssim_1
|
|
1584
|
+
completeness_value = []
|
|
1585
|
+
for p in progress[1:]:
|
|
1586
|
+
elapsed = p["time"] - last_ms
|
|
1587
|
+
# print '*******elapsed %f'%elapsed
|
|
1588
|
+
# Full Path of the Current Frame
|
|
1589
|
+
current_frame = str(Path(dir) / "ms_{0:06d}.png".format(p["time"]))
|
|
1590
|
+
logging.debug("Current Image is %s" % current_frame)
|
|
1591
|
+
# Takes full path of PNG frames to compute SSIM value
|
|
1592
|
+
per_si += elapsed * (1.0 - ssim)
|
|
1593
|
+
ssim = compute_ssim(current_frame, target_frame)
|
|
1594
|
+
gc.collect()
|
|
1595
|
+
last_ms = p["time"]
|
|
1596
|
+
completeness_value.append((p["time"], int(per_si)))
|
|
1597
|
+
|
|
1598
|
+
per_si = int(per_si)
|
|
1599
|
+
raw_progress_value = ["0=0"]
|
|
1600
|
+
for timestamp, percent in completeness_value:
|
|
1601
|
+
p = int(100 * float(percent) / float(per_si))
|
|
1602
|
+
raw_progress_value.append("%d=%d" % (timestamp, p))
|
|
1599
1603
|
|
|
1600
|
-
|
|
1604
|
+
return per_si, ", ".join(raw_progress_value)
|
|
1605
|
+
except Exception as e:
|
|
1606
|
+
logging.exception(e)
|
|
1607
|
+
return None, None
|
|
1601
1608
|
|
|
1602
1609
|
|
|
1603
1610
|
def calculate_hero_time(progress, directory, hero, viewport):
|
|
1604
1611
|
try:
|
|
1605
|
-
dir =
|
|
1612
|
+
dir = str(Path(__file__).resolve().parent / directory)
|
|
1606
1613
|
n = len(progress)
|
|
1607
|
-
target_frame =
|
|
1614
|
+
target_frame = str(Path(dir) / "ms_{0:06d}".format(progress[n - 1]["time"]))
|
|
1608
1615
|
|
|
1609
1616
|
extension = None
|
|
1610
|
-
if
|
|
1617
|
+
if Path(target_frame + ".png").is_file():
|
|
1611
1618
|
extension = ".png"
|
|
1612
|
-
elif
|
|
1619
|
+
elif Path(target_frame + ".jpg").is_file():
|
|
1613
1620
|
extension = ".jpg"
|
|
1614
1621
|
if extension is not None:
|
|
1615
1622
|
hero_width = int(hero["width"])
|
|
@@ -1642,10 +1649,7 @@ def calculate_hero_time(progress, directory, hero, viewport):
|
|
|
1642
1649
|
)
|
|
1643
1650
|
|
|
1644
1651
|
# Apply the mask to the target frame to create the reference frame
|
|
1645
|
-
target_mask_path =
|
|
1646
|
-
dir,
|
|
1647
|
-
"hero_{0}_ms_{1:06d}.png".format(hero["name"], progress[n - 1]["time"]),
|
|
1648
|
-
)
|
|
1652
|
+
target_mask_path = str(Path(dir) / "hero_{0}_ms_{1:06d}.png".format(hero["name"], progress[n - 1]["time"]))
|
|
1649
1653
|
|
|
1650
1654
|
def __apply_hero_mask(cur_frame):
|
|
1651
1655
|
"""Helper method for re-applying the same mask."""
|
|
@@ -1658,8 +1662,8 @@ def calculate_hero_time(progress, directory, hero, viewport):
|
|
|
1658
1662
|
target_mask.save(target_mask_path)
|
|
1659
1663
|
|
|
1660
1664
|
def cleanup():
|
|
1661
|
-
if
|
|
1662
|
-
|
|
1665
|
+
if Path(target_mask_path).is_file():
|
|
1666
|
+
Path(target_mask_path).unlink()
|
|
1663
1667
|
|
|
1664
1668
|
# Allow for small differences like scrollbars and overlaid UI elements
|
|
1665
1669
|
# by applying a 10% fuzz and allowing for up to 2% of the pixels to be
|
|
@@ -1668,16 +1672,14 @@ def calculate_hero_time(progress, directory, hero, viewport):
|
|
|
1668
1672
|
max_pixel_diff = math.ceil(hero_width * hero_height * 0.02)
|
|
1669
1673
|
|
|
1670
1674
|
for p in progress:
|
|
1671
|
-
current_frame =
|
|
1675
|
+
current_frame = str(Path(dir) / "ms_{0:06d}".format(p["time"]))
|
|
1672
1676
|
extension = None
|
|
1673
|
-
if
|
|
1677
|
+
if Path(current_frame + ".png").is_file():
|
|
1674
1678
|
extension = ".png"
|
|
1675
|
-
elif
|
|
1679
|
+
elif Path(current_frame + ".jpg").is_file():
|
|
1676
1680
|
extension = ".jpg"
|
|
1677
1681
|
if extension is not None:
|
|
1678
|
-
current_mask_path =
|
|
1679
|
-
dir, "hero_{0}_ms_{1:06d}.png".format(hero["name"], p["time"])
|
|
1680
|
-
)
|
|
1682
|
+
current_mask_path = str(Path(dir) / "hero_{0}_ms_{1:06d}.png".format(hero["name"], p["time"]))
|
|
1681
1683
|
|
|
1682
1684
|
current_mask = __apply_hero_mask(current_frame + extension)
|
|
1683
1685
|
current_mask.save(current_mask_path)
|
|
@@ -1692,7 +1694,7 @@ def calculate_hero_time(progress, directory, hero, viewport):
|
|
|
1692
1694
|
)
|
|
1693
1695
|
|
|
1694
1696
|
# Remove each mask after using it
|
|
1695
|
-
|
|
1697
|
+
Path(current_mask_path).unlink()
|
|
1696
1698
|
|
|
1697
1699
|
if match:
|
|
1698
1700
|
# Clean up masks as soon as a match is found
|
|
@@ -1975,7 +1977,7 @@ def main():
|
|
|
1975
1977
|
directory = temp_dir
|
|
1976
1978
|
if options.dir is not None:
|
|
1977
1979
|
directory = options.dir
|
|
1978
|
-
histogram_file =
|
|
1980
|
+
histogram_file = str(Path(temp_dir) / "histograms.json.gz")
|
|
1979
1981
|
|
|
1980
1982
|
# Set up logging
|
|
1981
1983
|
log_level = logging.CRITICAL
|
|
@@ -2012,11 +2014,9 @@ def main():
|
|
|
2012
2014
|
if options.video:
|
|
2013
2015
|
orange_file = None
|
|
2014
2016
|
if options.orange:
|
|
2015
|
-
orange_file =
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
if not os.path.isfile(orange_file):
|
|
2019
|
-
orange_file = os.path.join(colors_temp_dir, "orange.png")
|
|
2017
|
+
orange_file = str(Path(__file__).resolve().parent / "orange.png")
|
|
2018
|
+
if not Path(orange_file).is_file():
|
|
2019
|
+
orange_file = str(Path(colors_temp_dir) / "orange.png")
|
|
2020
2020
|
generate_orange_png(orange_file)
|
|
2021
2021
|
video_to_frames(
|
|
2022
2022
|
options.video,
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { execa } from 'execa';
|
|
2
|
-
import { getLogger } from '@sitespeed.io/log';
|
|
3
|
-
const log = getLogger('browsertime.video');
|
|
4
|
-
|
|
5
|
-
export async function convert(source, destination) {
|
|
6
|
-
const scriptArguments = [
|
|
7
|
-
'-framerate',
|
|
8
|
-
60,
|
|
9
|
-
'-i',
|
|
10
|
-
source,
|
|
11
|
-
'-c:v',
|
|
12
|
-
'copy',
|
|
13
|
-
'-f',
|
|
14
|
-
'mp4',
|
|
15
|
-
destination,
|
|
16
|
-
'-y'
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
log.debug('Converting video from h264 to mp4 %j', scriptArguments);
|
|
20
|
-
|
|
21
|
-
return execa('ffmpeg', scriptArguments);
|
|
22
|
-
}
|