browsertime 26.3.2 → 27.0.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +1 -1
  3. package/browserscripts/timings/softNavigations.js +94 -0
  4. package/lib/chrome/har.js +36 -5
  5. package/lib/chrome/settings/chromeAndroidOptions.js +2 -1
  6. package/lib/chrome/webdriver/chromium.js +39 -0
  7. package/lib/chrome/webdriver/setupChromiumOptions.js +86 -31
  8. package/lib/core/engine/collector.js +340 -317
  9. package/lib/core/engine/command/addText.js +93 -12
  10. package/lib/core/engine/command/click.js +239 -139
  11. package/lib/core/engine/command/commandHelper.js +45 -0
  12. package/lib/core/engine/command/cookie.js +85 -0
  13. package/lib/core/engine/command/element.js +37 -2
  14. package/lib/core/engine/command/javaScript.js +14 -24
  15. package/lib/core/engine/command/measure/screenshots.js +114 -0
  16. package/lib/core/engine/command/measure/video.js +85 -0
  17. package/lib/core/engine/command/measure.js +104 -180
  18. package/lib/core/engine/command/mouse/clickAndHold.js +6 -3
  19. package/lib/core/engine/command/mouse/contextClick.js +56 -4
  20. package/lib/core/engine/command/mouse/doubleClick.js +49 -1
  21. package/lib/core/engine/command/mouse/mouseMove.js +50 -1
  22. package/lib/core/engine/command/mouse/singleClick.js +165 -129
  23. package/lib/core/engine/command/navigation.js +6 -6
  24. package/lib/core/engine/command/select.js +95 -3
  25. package/lib/core/engine/command/selectorParser.js +58 -0
  26. package/lib/core/engine/command/set.js +55 -1
  27. package/lib/core/engine/command/wait.js +41 -2
  28. package/lib/core/engine/commands.js +550 -42
  29. package/lib/core/engine/index.js +206 -150
  30. package/lib/core/engine/iteration.js +269 -137
  31. package/lib/firefox/networkManager.js +22 -30
  32. package/lib/firefox/webdriver/builder.js +14 -0
  33. package/lib/safari/har.js +131 -0
  34. package/lib/safari/native/ios-capture +0 -0
  35. package/lib/safari/native/ios-capture.m +340 -0
  36. package/lib/safari/safariInspectorClient.js +322 -0
  37. package/lib/safari/webdriver/safari.js +303 -13
  38. package/lib/safari/webkitToCdpAdapter.js +241 -0
  39. package/lib/support/cli.js +13 -0
  40. package/lib/support/engineUtils.js +16 -5
  41. package/lib/support/getViewPort.js +5 -0
  42. package/lib/support/har/index.js +41 -3
  43. package/lib/support/usbPower.js +2 -3
  44. package/lib/video/postprocessing/finetune/addTextToVideo.js +4 -0
  45. package/lib/video/postprocessing/finetune/getFont.js +3 -3
  46. package/lib/video/postprocessing/finetune/getTimingMetrics.js +7 -0
  47. package/lib/video/postprocessing/finetune/index.js +26 -9
  48. package/lib/video/screenRecording/ios/iosDeviceRecorder.js +109 -0
  49. package/lib/video/screenRecording/recorder.js +2 -2
  50. package/lib/video/screenRecording/setOrangeBackground.js +3 -0
  51. package/package.json +11 -11
  52. package/types/core/engine/command/addText.d.ts +35 -6
  53. package/types/core/engine/command/addText.d.ts.map +1 -1
  54. package/types/core/engine/command/click.d.ts +87 -16
  55. package/types/core/engine/command/click.d.ts.map +1 -1
  56. package/types/core/engine/command/commandHelper.d.ts +16 -0
  57. package/types/core/engine/command/commandHelper.d.ts.map +1 -0
  58. package/types/core/engine/command/cookie.d.ts +65 -0
  59. package/types/core/engine/command/cookie.d.ts.map +1 -0
  60. package/types/core/engine/command/element.d.ts +19 -1
  61. package/types/core/engine/command/element.d.ts.map +1 -1
  62. package/types/core/engine/command/javaScript.d.ts +4 -0
  63. package/types/core/engine/command/javaScript.d.ts.map +1 -1
  64. package/types/core/engine/command/measure/screenshots.d.ts +15 -0
  65. package/types/core/engine/command/measure/screenshots.d.ts.map +1 -0
  66. package/types/core/engine/command/measure/video.d.ts +26 -0
  67. package/types/core/engine/command/measure/video.d.ts.map +1 -0
  68. package/types/core/engine/command/measure.d.ts +59 -33
  69. package/types/core/engine/command/measure.d.ts.map +1 -1
  70. package/types/core/engine/command/mouse/clickAndHold.d.ts.map +1 -1
  71. package/types/core/engine/command/mouse/contextClick.d.ts +28 -4
  72. package/types/core/engine/command/mouse/contextClick.d.ts.map +1 -1
  73. package/types/core/engine/command/mouse/doubleClick.d.ts +24 -4
  74. package/types/core/engine/command/mouse/doubleClick.d.ts.map +1 -1
  75. package/types/core/engine/command/mouse/mouseMove.d.ts +30 -5
  76. package/types/core/engine/command/mouse/mouseMove.d.ts.map +1 -1
  77. package/types/core/engine/command/mouse/singleClick.d.ts +50 -13
  78. package/types/core/engine/command/mouse/singleClick.d.ts.map +1 -1
  79. package/types/core/engine/command/navigation.d.ts +3 -3
  80. package/types/core/engine/command/select.d.ts +45 -8
  81. package/types/core/engine/command/select.d.ts.map +1 -1
  82. package/types/core/engine/command/selectorParser.d.ts +20 -0
  83. package/types/core/engine/command/selectorParser.d.ts.map +1 -0
  84. package/types/core/engine/command/set.d.ts +34 -7
  85. package/types/core/engine/command/set.d.ts.map +1 -1
  86. package/types/core/engine/command/wait.d.ts +29 -6
  87. package/types/core/engine/command/wait.d.ts.map +1 -1
  88. package/types/core/engine/commands.d.ts +296 -28
  89. package/types/core/engine/commands.d.ts.map +1 -1
  90. package/types/support/engineUtils.d.ts.map +1 -1
  91. package/types/support/getViewPort.d.ts.map +1 -1
  92. package/types/support/usbPower.d.ts.map +1 -1
  93. package/types/video/postprocessing/finetune/addTextToVideo.d.ts.map +1 -1
  94. package/types/video/postprocessing/finetune/getTimingMetrics.d.ts.map +1 -1
  95. package/types/video/postprocessing/finetune/index.d.ts.map +1 -1
  96. package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts +28 -0
  97. package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts.map +1 -0
  98. package/types/video/screenRecording/recorder.d.ts +2 -2
  99. package/types/video/screenRecording/recorder.d.ts.map +1 -1
  100. package/types/video/screenRecording/setOrangeBackground.d.ts.map +1 -1
  101. package/types/video/video.d.ts +1 -1
  102. package/types/video/video.d.ts.map +1 -1
  103. package/visualmetrics/visualmetrics-portable.py +136 -136
  104. package/lib/video/screenRecording/ios/convertToMp4.js +0 -22
  105. 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
- output_dir, video_name = os.path.split(video_path)
258
- video_name, _ = os.path.splitext(video_name)
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
- os.path.join(output_dir, video_name + "-edges.mp4"),
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
- os.path.join(output_dir, video_name + "-edges-overlay.mp4"),
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 = os.path.join(directory, "ms_000000")
380
+ first_frame = str(Path(directory) / "ms_000000")
380
381
  if (
381
- not os.path.isfile(first_frame + ".png")
382
- and not os.path.isfile(first_frame + ".jpg")
382
+ not Path(first_frame + ".png").is_file()
383
+ and not Path(first_frame + ".jpg").is_file()
383
384
  ) or force:
384
- if os.path.isfile(video):
385
- video = os.path.realpath(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 os.path.isdir(directory):
389
+ if Path(directory).is_dir():
389
390
  shutil.rmtree(directory, True)
390
- if not os.path.isdir(directory):
391
- os.mkdir(directory, 0o755)
392
- if os.path.isdir(directory):
393
- directory = os.path.realpath(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
- os.path.join(dir_escaped, "img-%d.png"),
461
+ str(Path(dir_escaped) / "img-%d.png"),
461
462
  ]
462
463
  logging.debug(" ".join(command))
463
- lines = []
464
464
 
465
- proc = subprocess.Popen(command, stderr=subprocess.PIPE, encoding="UTF-8")
466
- while proc.poll() is None:
467
- lines.extend(iter(proc.stderr.readline, ""))
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 = os.path.join(directory, "img-{0:d}.png".format(frame_count))
479
- dest = os.path.join(directory, "video-{0:06d}.png".format(frame_time))
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
- os.rename(src, dest)
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
- lines = []
496
- proc = subprocess.Popen(command, stderr=subprocess.PIPE, encoding="UTF-8")
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(os.path.join(directory, "video-*.png")))
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
- os.remove(frame)
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(os.path.join(directory, "video-*.png")))
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
- os.remove(frame)
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
- os.remove(frame)
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 = os.path.join(directory, "viewport.png")
676
- if os.path.isfile(frame):
677
- os.remove(frame)
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 os.path.isfile(frame):
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
- os.remove(frame)
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(os.path.join(directory, "video-*.png")))
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 = os.path.join(directory, "ms_{0:06d}.png".format(new_time))
730
- os.rename(frame, dest)
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(os.path.join(directory, "video-*.png")))
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
- os.remove(files[i])
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
- os.remove(files[i])
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(os.path.join(directory, "ms_*.png")))
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
- os.remove(files[i])
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(os.path.join(directory, "ms_*.png")))
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
- os.remove(previous_frame)
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
- os.remove(duplicate)
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(os.path.join(directory, "ms_*.png")))
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(os.path.join(directory, "*.png"))
954
+ files = glob.glob(str(Path(directory) / "*.png"))
954
955
  for file in files:
955
- os.remove(file)
956
- files = glob.glob(os.path.join(directory, "*.jpg"))
956
+ Path(file).unlink()
957
+ files = glob.glob(str(Path(directory) / "*.jpg"))
957
958
  for file in files:
958
- os.remove(file)
959
- files = glob.glob(os.path.join(directory, "*.json"))
959
+ Path(file).unlink()
960
+ files = glob.glob(str(Path(directory) / "*.json"))
960
961
  for file in files:
961
- os.remove(file)
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 os.path.isfile(color_file):
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 os.path.isfile(histograms_file) or force:
1091
+ if not Path(histograms_file).is_file() or force:
1091
1092
  try:
1092
1093
  extension = None
1093
- directory = os.path.realpath(directory)
1094
- first_frame = os.path.join(directory, "ms_000000")
1095
- if os.path.isfile(first_frame + ".png"):
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 os.path.isfile(first_frame + ".jpg"):
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(os.path.join(directory, "ms_*" + extension)))
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": os.path.basename(frame),
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 os.path.isfile(histograms_file):
1120
- os.remove(histograms_file)
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 = os.path.realpath(directory)
1177
- files = sorted(glob.glob(os.path.join(directory, "ms_*.png")))
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 = os.path.realpath(directory)
1194
- pattern = os.path.join(directory, "ms_*.png")
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
- _, filename = os.path.split(file)
1199
- filen, ext = os.path.splitext(filename)
1199
+ filen = Path(file).stem
1200
1200
  convert_img_to_jpeg(
1201
- file, os.path.join(directory, filen + ".jpg"), quality=quality
1201
+ file, str(Path(directory) / (filen + ".jpg")), quality=quality
1202
1202
  )
1203
- os.remove(file)
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
- file_name, ext = os.path.splitext(progress_file)
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 os.path.isfile(hero_elements_file):
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 os.path.isfile(histograms_file):
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, ext = os.path.splitext(histogram["file"])
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 = os.path.join(os.path.dirname(os.path.abspath(__file__)), directory)
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 = os.path.join(dir, "ms_{0:06d}.png".format(p["time"]))
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
- from ssim import compute_ssim
1569
-
1570
- x = len(progress)
1571
- dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), directory)
1572
- first_paint_frame = os.path.join(dir, "ms_{0:06d}.png".format(progress[1]["time"]))
1573
- target_frame = os.path.join(dir, "ms_{0:06d}.png".format(progress[x - 1]["time"]))
1574
- ssim_1 = compute_ssim(first_paint_frame, target_frame)
1575
- per_si = float(progress[1]["time"])
1576
- last_ms = progress[1]["time"]
1577
- # Full Path of the Target Frame
1578
- logging.debug("Target image for perSI is %s" % target_frame)
1579
- ssim = ssim_1
1580
- completeness_value = []
1581
- for p in progress[1:]:
1582
- elapsed = p["time"] - last_ms
1583
- # print '*******elapsed %f'%elapsed
1584
- # Full Path of the Current Frame
1585
- current_frame = os.path.join(dir, "ms_{0:06d}.png".format(p["time"]))
1586
- logging.debug("Current Image is %s" % current_frame)
1587
- # Takes full path of PNG frames to compute SSIM value
1588
- per_si += elapsed * (1.0 - ssim)
1589
- ssim = compute_ssim(current_frame, target_frame)
1590
- gc.collect()
1591
- last_ms = p["time"]
1592
- completeness_value.append((p["time"], int(per_si)))
1593
-
1594
- per_si = int(per_si)
1595
- raw_progress_value = ["0=0"]
1596
- for timestamp, percent in completeness_value:
1597
- p = int(100 * float(percent) / float(per_si))
1598
- raw_progress_value.append("%d=%d" % (timestamp, p))
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
- return per_si, ", ".join(raw_progress_value)
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 = os.path.join(os.path.dirname(os.path.abspath(__file__)), directory)
1612
+ dir = str(Path(__file__).resolve().parent / directory)
1606
1613
  n = len(progress)
1607
- target_frame = os.path.join(dir, "ms_{0:06d}".format(progress[n - 1]["time"]))
1614
+ target_frame = str(Path(dir) / "ms_{0:06d}".format(progress[n - 1]["time"]))
1608
1615
 
1609
1616
  extension = None
1610
- if os.path.isfile(target_frame + ".png"):
1617
+ if Path(target_frame + ".png").is_file():
1611
1618
  extension = ".png"
1612
- elif os.path.isfile(target_frame + ".jpg"):
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 = os.path.join(
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 os.path.isfile(target_mask_path):
1662
- os.remove(target_mask_path)
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 = os.path.join(dir, "ms_{0:06d}".format(p["time"]))
1675
+ current_frame = str(Path(dir) / "ms_{0:06d}".format(p["time"]))
1672
1676
  extension = None
1673
- if os.path.isfile(current_frame + ".png"):
1677
+ if Path(current_frame + ".png").is_file():
1674
1678
  extension = ".png"
1675
- elif os.path.isfile(current_frame + ".jpg"):
1679
+ elif Path(current_frame + ".jpg").is_file():
1676
1680
  extension = ".jpg"
1677
1681
  if extension is not None:
1678
- current_mask_path = os.path.join(
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
- os.remove(current_mask_path)
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 = os.path.join(temp_dir, "histograms.json.gz")
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 = os.path.join(
2016
- os.path.dirname(os.path.realpath(__file__)), "orange.png"
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
- }