browsertime 16.7.0 → 16.9.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.
@@ -37,19 +37,15 @@ import json
37
37
  import logging
38
38
  import math
39
39
  import os
40
- import platform
41
40
  import re
42
41
  import sys
43
42
  import shutil
44
43
  import subprocess
45
44
  import tempfile
46
45
 
47
- if sys.version_info > (3, 0):
48
- GZIP_TEXT = "wt"
49
- GZIP_READ_TEXT = "rt"
50
- else:
51
- GZIP_TEXT = "w"
52
- GZIP_READ_TEXT = "r"
46
+
47
+ GZIP_TEXT = "wt"
48
+ GZIP_READ_TEXT = "rt"
53
49
 
54
50
  # Globals
55
51
  options = None
@@ -372,17 +368,11 @@ def video_to_frames(
372
368
  directory,
373
369
  force,
374
370
  orange_file,
375
- white_file,
376
- gray_file,
377
- multiple,
378
371
  find_viewport,
379
- viewport_time,
380
372
  viewport_retries,
381
373
  viewport_min_height,
382
374
  viewport_min_width,
383
- full_resolution,
384
- timeline_file,
385
- trim_end,
375
+ full_resolution
386
376
  ):
387
377
  """Extract the video frames"""
388
378
  global client_viewport
@@ -405,7 +395,6 @@ def video_to_frames(
405
395
  video,
406
396
  directory,
407
397
  find_viewport,
408
- viewport_time,
409
398
  viewport_retries,
410
399
  viewport_min_height,
411
400
  viewport_min_width,
@@ -419,34 +408,16 @@ def video_to_frames(
419
408
  gc.collect()
420
409
  if extract_frames(video, directory, full_resolution, viewport):
421
410
  client_viewport = None
422
- if find_viewport and options.notification:
423
- client_viewport = find_image_viewport(
424
- os.path.join(directory, "video-000000.png"), is_mobile
425
- )
426
- if multiple and orange_file is not None:
427
- directories = split_videos(directory, orange_file)
428
- else:
429
- directories = [directory]
411
+ directories = [directory]
430
412
  for dir in directories:
431
- trim_video_end(dir, trim_end)
432
413
  if orange_file is not None:
433
414
  remove_frames_before_orange(dir, orange_file)
434
415
  remove_orange_frames(dir, orange_file)
435
- find_first_frame(dir, white_file)
436
- blank_first_frame(dir)
437
416
  find_render_start(
438
- dir, orange_file, gray_file, cropped, is_mobile
417
+ dir, orange_file, cropped, is_mobile
439
418
  )
440
- find_last_frame(dir, white_file)
441
419
  adjust_frame_times(dir)
442
- if timeline_file is not None and not multiple:
443
- synchronize_to_timeline(dir, timeline_file)
444
420
  eliminate_duplicate_frames(dir, cropped, is_mobile)
445
- eliminate_similar_frames(dir)
446
- # See if we are limiting the number of frames to keep
447
- # (before processing them to save processing time)
448
- if options.maxframes > 0:
449
- cap_frame_count(dir, options.maxframes)
450
421
  crop_viewport(dir)
451
422
  gc.collect()
452
423
  else:
@@ -492,10 +463,8 @@ def extract_frames(video, directory, full_resolution, viewport):
492
463
  ]
493
464
  logging.debug(" ".join(command))
494
465
  lines = []
495
- if sys.version_info > (3, 0):
496
- proc = subprocess.Popen(command, stderr=subprocess.PIPE, encoding="UTF-8")
497
- else:
498
- proc = subprocess.Popen(command, stderr=subprocess.PIPE)
466
+
467
+ proc = subprocess.Popen(command, stderr=subprocess.PIPE, encoding="UTF-8")
499
468
  while proc.poll() is None:
500
469
  lines.extend(iter(proc.stderr.readline, ""))
501
470
 
@@ -526,10 +495,7 @@ def find_recording_platform(video):
526
495
  logging.debug(command)
527
496
 
528
497
  lines = []
529
- if sys.version_info > (3, 0):
530
- proc = subprocess.Popen(command, stderr=subprocess.PIPE, encoding="UTF-8")
531
- else:
532
- proc = subprocess.Popen(command, stderr=subprocess.PIPE)
498
+ proc = subprocess.Popen(command, stderr=subprocess.PIPE, encoding="UTF-8")
533
499
 
534
500
  while proc.poll() is None:
535
501
  lines.extend(iter(proc.stderr.readline, ""))
@@ -542,51 +508,6 @@ def find_recording_platform(video):
542
508
 
543
509
  return is_mobile
544
510
 
545
-
546
- def split_videos(directory, orange_file):
547
- """Split multiple videos on orange frame separators"""
548
- logging.debug("Splitting video on orange frames (this may take a while)...")
549
- directories = []
550
- current = 0
551
- found_orange = False
552
- video_dir = None
553
- frames = sorted(glob.glob(os.path.join(directory, "video-*.png")))
554
- if len(frames):
555
- for frame in frames:
556
- if is_color_frame(frame, orange_file):
557
- if not found_orange:
558
- found_orange = True
559
- # Make a copy of the orange frame for the end of the
560
- # current video
561
- if video_dir is not None:
562
- dest = os.path.join(video_dir, os.path.basename(frame))
563
- shutil.copyfile(frame, dest)
564
- current += 1
565
- video_dir = os.path.join(directory, str(current))
566
- logging.debug(
567
- "Orange frame found: %s, starting video directory %s",
568
- frame,
569
- video_dir,
570
- )
571
- if not os.path.isdir(video_dir):
572
- os.mkdir(video_dir, 0o755)
573
- if os.path.isdir(video_dir):
574
- video_dir = os.path.realpath(video_dir)
575
- clean_directory(video_dir)
576
- directories.append(video_dir)
577
- else:
578
- video_dir = None
579
- else:
580
- found_orange = False
581
- if video_dir is not None:
582
- dest = os.path.join(video_dir, os.path.basename(frame))
583
- os.rename(frame, dest)
584
- else:
585
- logging.debug("Removing spurious frame %s at the beginning", frame)
586
- os.remove(frame)
587
- return directories
588
-
589
-
590
511
  def remove_frames_before_orange(directory, orange_file):
591
512
  """Remove stray frames from the start of the video"""
592
513
  frames = sorted(glob.glob(os.path.join(directory, "video-*.png")))
@@ -612,7 +533,6 @@ def remove_frames_before_orange(directory, orange_file):
612
533
  logging.debug("Removing pre-orange frame %s", frame)
613
534
  os.remove(frame)
614
535
 
615
-
616
536
  def remove_orange_frames(directory, orange_file):
617
537
  """Remove orange frames from the beginning of the video"""
618
538
  frames = sorted(glob.glob(os.path.join(directory, "video-*.png")))
@@ -714,7 +634,6 @@ def find_video_viewport(
714
634
  video,
715
635
  directory,
716
636
  find_viewport,
717
- viewport_time,
718
637
  viewport_retries,
719
638
  viewport_min_height,
720
639
  viewport_min_width,
@@ -758,8 +677,6 @@ def find_video_viewport(
758
677
  os.remove(frame)
759
678
 
760
679
  command = ["ffmpeg", "-i", video]
761
- if viewport_time:
762
- command.extend(["-ss", viewport_time])
763
680
 
764
681
  # Pull one frame from the video starting with the frame at
765
682
  # the `retries` index
@@ -771,46 +688,7 @@ def find_video_viewport(
771
688
  with Image.open(frame) as im:
772
689
  width, height = im.size
773
690
  logging.debug("%s is %dx%d", frame, width, height)
774
- if options.notification:
775
- im = Image.open(frame)
776
- pixels = im.load()
777
- middle = int(math.floor(height / 2))
778
- # Find the top edge (at ~40% in to deal with browsers that
779
- # color the notification area)
780
- x = int(width * 0.4)
781
- y = 0
782
- background = pixels[x, y]
783
- top = None
784
- while top is None and y < middle:
785
- if not colors_are_similar(background, pixels[x, y]):
786
- top = y
787
- else:
788
- y += 1
789
- if top is None:
790
- top = 0
791
- logging.debug("Window top edge is {0:d}".format(top))
792
-
793
- # Find the bottom edge
794
- x = 0
795
- y = height - 1
796
- bottom = None
797
- while bottom is None and y > middle:
798
- if not colors_are_similar(background, pixels[x, y]):
799
- bottom = y
800
- else:
801
- y -= 1
802
- if bottom is None:
803
- bottom = height - 1
804
- logging.debug("Window bottom edge is {0:d}".format(bottom))
805
-
806
- viewport = {
807
- "x": 0,
808
- "y": top,
809
- "width": width,
810
- "height": (bottom - top),
811
- }
812
-
813
- elif find_viewport:
691
+ if find_viewport:
814
692
  viewport = find_image_viewport(frame, is_mobile)
815
693
  else:
816
694
  viewport = {"x": 0, "y": 0, "width": width, "height": height}
@@ -830,32 +708,6 @@ def find_video_viewport(
830
708
 
831
709
  return viewport, cropped
832
710
 
833
-
834
- def trim_video_end(directory, trim_time):
835
- if trim_time > 0:
836
- logging.debug(
837
- "Trimming "
838
- + str(trim_time)
839
- + "ms from the end of the video in "
840
- + directory
841
- )
842
- frames = sorted(glob.glob(os.path.join(directory, "video-*.png")))
843
- if len(frames):
844
- match = re.compile(r"video-(?P<ms>[0-9]+)\.png")
845
- m = re.search(match, frames[-1])
846
- if m is not None:
847
- frame_time = int(m.groupdict().get("ms"))
848
- end_time = frame_time - trim_time
849
- logging.debug("Trimming frames before " + str(end_time) + "ms")
850
- for frame in frames:
851
- m = re.search(match, frame)
852
- if m is not None:
853
- frame_time = int(m.groupdict().get("ms"))
854
- if frame_time > end_time:
855
- logging.debug("Trimming frame " + frame)
856
- os.remove(frame)
857
-
858
-
859
711
  def adjust_frame_times(directory):
860
712
  offset = None
861
713
  frames = sorted(glob.glob(os.path.join(directory, "video-*.png")))
@@ -876,108 +728,7 @@ def adjust_frame_times(directory):
876
728
  dest = os.path.join(directory, "ms_{0:06d}.png".format(new_time))
877
729
  os.rename(frame, dest)
878
730
 
879
-
880
- def find_first_frame(directory, white_file):
881
- logging.debug("Finding First Frame...")
882
- try:
883
- if options.startwhite:
884
- files = sorted(glob.glob(os.path.join(directory, "video-*.png")))
885
- count = len(files)
886
- if count > 1:
887
- from PIL import Image
888
-
889
- for i in range(count):
890
- if is_white_frame(files[i], white_file):
891
- break
892
- else:
893
- logging.debug(
894
- "Removing non-white frame {0} from the beginning".format(
895
- files[i]
896
- )
897
- )
898
- os.remove(files[i])
899
- elif options.findstart > 0 and options.findstart <= 100:
900
- files = sorted(glob.glob(os.path.join(directory, "video-*.png")))
901
- count = len(files)
902
- if count > 1:
903
- from PIL import Image
904
-
905
- blank = files[0]
906
- with Image.open(blank) as im:
907
- width, height = im.size
908
- match_height = int(math.ceil(height * options.findstart / 100.0))
909
- crop = (width, match_height, 0, 0)
910
- found_first_change = False
911
- found_white_frame = False
912
- found_non_white_frame = False
913
- first_frame = None
914
- if white_file is None:
915
- found_white_frame = True
916
- for i in range(count):
917
- if not found_first_change:
918
- different = not frames_match(
919
- files[i], files[i + 1], 5, 100, crop, None
920
- )
921
- logging.debug(
922
- "Removing early frame %s from the beginning", files[i]
923
- )
924
- os.remove(files[i])
925
- if different:
926
- first_frame = files[i + 1]
927
- found_first_change = True
928
- elif not found_white_frame:
929
- if files[i] != first_frame:
930
- if found_non_white_frame:
931
- found_white_frame = is_white_frame(files[i], white_file)
932
- if not found_white_frame:
933
- logging.debug(
934
- "Removing early non-white frame {0} from the beginning".format(
935
- files[i]
936
- )
937
- )
938
- os.remove(files[i])
939
- else:
940
- found_non_white_frame = not is_white_frame(
941
- files[i], white_file
942
- )
943
- logging.debug(
944
- "Removing early pre-non-white frame {0} from the beginning".format(
945
- files[i]
946
- )
947
- )
948
- os.remove(files[i])
949
- if found_first_change and found_white_frame:
950
- break
951
- except BaseException:
952
- logging.exception("Error finding first frame")
953
-
954
-
955
- def find_last_frame(directory, white_file):
956
- logging.debug("Finding Last Frame...")
957
- try:
958
- if options.endwhite:
959
- files = sorted(glob.glob(os.path.join(directory, "video-*.png")))
960
- count = len(files)
961
- if count > 2:
962
- found_end = False
963
-
964
- for i in range(2, count):
965
- if found_end:
966
- logging.debug(
967
- "Removing frame {0} from the end".format(files[i])
968
- )
969
- os.remove(files[i])
970
- if is_white_frame(files[i], white_file):
971
- found_end = True
972
- logging.debug(
973
- "Removing ending white frame {0}".format(files[i])
974
- )
975
- os.remove(files[i])
976
- except BaseException:
977
- logging.exception("Error finding last frame")
978
-
979
-
980
- def find_render_start(directory, orange_file, gray_file, cropped, is_mobile):
731
+ def find_render_start(directory, orange_file, cropped, is_mobile):
981
732
  logging.debug("Finding Render Start...")
982
733
  try:
983
734
  if (
@@ -1050,9 +801,6 @@ def find_render_start(directory, orange_file, gray_file, cropped, is_mobile):
1050
801
  ):
1051
802
  logging.debug("Removing orange frame %s", files[i])
1052
803
  os.remove(files[i])
1053
- elif gray_file is not None and is_color_frame(files[i], gray_file):
1054
- logging.debug("Removing gray frame %s", files[i])
1055
- os.remove(files[i])
1056
804
  else:
1057
805
  break
1058
806
  except BaseException:
@@ -1070,13 +818,6 @@ def eliminate_duplicate_frames(directory, cropped, is_mobile):
1070
818
  blank = files[0]
1071
819
  with Image.open(blank) as im:
1072
820
  width, height = im.size
1073
- if options.viewport and options.notification:
1074
- if (
1075
- client_viewport["width"] == width
1076
- and client_viewport["height"] == height
1077
- ):
1078
- client_viewport = None
1079
-
1080
821
  im_width = width
1081
822
  im_height = height
1082
823
 
@@ -1161,47 +902,6 @@ def eliminate_duplicate_frames(directory, cropped, is_mobile):
1161
902
  except BaseException:
1162
903
  logging.exception("Error processing frames for duplicates")
1163
904
 
1164
-
1165
- def eliminate_similar_frames(directory):
1166
- logging.debug("Removing Similar Frames...")
1167
- try:
1168
- # only do this when decimate couldn't be used to eliminate similar
1169
- # frames
1170
- if options.notification:
1171
- files = sorted(glob.glob(os.path.join(directory, "ms_*.png")))
1172
- count = len(files)
1173
- if count > 3:
1174
- crop = None
1175
- if client_viewport is not None:
1176
- crop = (
1177
- client_viewport["width"],
1178
- client_viewport["height"],
1179
- client_viewport["x"],
1180
- client_viewport["y"],
1181
- )
1182
- baseline = files[1]
1183
- for i in range(2, count - 1):
1184
- if frames_match(baseline, files[i], 1, 0, crop, None):
1185
- logging.debug("Removing similar frame {0}".format(files[i]))
1186
- os.remove(files[i])
1187
- else:
1188
- baseline = files[i]
1189
- except BaseException:
1190
- logging.exception("Error removing similar frames")
1191
-
1192
-
1193
- def blank_first_frame(directory):
1194
- try:
1195
- if options.forceblank:
1196
- files = sorted(glob.glob(os.path.join(directory, "video-*.png")))
1197
- count = len(files)
1198
- if count > 1:
1199
- blank = blank_frame(files[0])
1200
- blank.save(files[0])
1201
- except BaseException:
1202
- logging.exception("Error blanking first frame")
1203
-
1204
-
1205
905
  def crop_viewport(directory):
1206
906
  if client_viewport is not None:
1207
907
  try:
@@ -1228,14 +928,7 @@ def crop_viewport(directory):
1228
928
  def get_decimate_filter():
1229
929
  decimate = None
1230
930
  try:
1231
- if sys.version_info > (3, 0):
1232
- filters = subprocess.check_output(
1233
- ["ffmpeg", "-filters"], stderr=subprocess.STDOUT, encoding="UTF-8"
1234
- )
1235
- else:
1236
- filters = subprocess.check_output(
1237
- ["ffmpeg", "-filters"], stderr=subprocess.STDOUT
1238
- )
931
+ filters = subprocess.check_output(["ffmpeg", "-filters"], stderr=subprocess.STDOUT, encoding="UTF-8")
1239
932
  lines = filters.split("\n")
1240
933
  match = re.compile(
1241
934
  r"(?P<filter>[\w]*decimate).*V->V.*Remove near-duplicate frames"
@@ -1312,50 +1005,6 @@ def is_color_frame(file, color_file):
1312
1005
  frame_cache[file][color_file] = bool(match)
1313
1006
  return match
1314
1007
 
1315
-
1316
- def is_white_frame(file, white_file):
1317
- white = False
1318
- if os.path.isfile(white_file):
1319
- try:
1320
- from PIL import Image
1321
-
1322
- fmt_img = None
1323
- white_img = Image.open(white_file)
1324
-
1325
- if options.viewport:
1326
- with Image.open(file) as im:
1327
- fmt_img = resize(im, 200, 200)
1328
-
1329
- else:
1330
- with Image.open(file) as im:
1331
- width, height, _ = im.shape
1332
- fmt_img = crop_im(
1333
- im, 0.5 * width, 0.33 * height, 0, 0, gravity="center"
1334
- )
1335
- fmt_img = resize(fmt_img, 200, 200)
1336
-
1337
- if client_viewport is not None:
1338
- with Image.open(file) as im:
1339
- width, height, _ = im.shape
1340
- fmt_img = crop_im(
1341
- im,
1342
- client_viewport["width"],
1343
- client_viewport["height"],
1344
- client_viewport["x"],
1345
- client_viewport["y"],
1346
- )
1347
- fmt_img = resize(fmt_img, 200, 200)
1348
- except BaseException as e:
1349
- logging.exception(e)
1350
- return None
1351
-
1352
- different_pixels = compare(white_img, fmt_img, fuzz=0.1)
1353
- if different_pixels < 500:
1354
- white = True
1355
-
1356
- return white
1357
-
1358
-
1359
1008
  def colors_are_similar(a, b, threshold=15):
1360
1009
  similar = True
1361
1010
  sum = 0
@@ -1424,179 +1073,6 @@ def generate_orange_png(orange_file):
1424
1073
  except BaseException:
1425
1074
  logging.exception("Error generating orange png " + orange_file)
1426
1075
 
1427
-
1428
- def generate_gray_png(gray_file):
1429
- try:
1430
- from PIL import Image, ImageDraw
1431
-
1432
- im = Image.new("RGB", (200, 200))
1433
- draw = ImageDraw.Draw(im)
1434
- draw.rectangle([0, 0, 200, 200], fill=(128, 128, 128))
1435
- del draw
1436
- im.save(gray_file, "PNG")
1437
- except BaseException:
1438
- logging.exception("Error generating gray png " + gray_file)
1439
-
1440
-
1441
- def generate_white_png(white_file):
1442
- try:
1443
- from PIL import Image, ImageDraw
1444
-
1445
- im = Image.new("RGB", (200, 200))
1446
- draw = ImageDraw.Draw(im)
1447
- draw.rectangle([0, 0, 200, 200], fill=(255, 255, 255))
1448
- del draw
1449
- im.save(white_file, "PNG")
1450
- except BaseException:
1451
- logging.exception("Error generating white png " + white_file)
1452
-
1453
-
1454
- def synchronize_to_timeline(directory, timeline_file):
1455
- offset = get_timeline_offset(timeline_file)
1456
- if offset > 0:
1457
- frames = sorted(glob.glob(os.path.join(directory, "ms_*.png")))
1458
- match = re.compile(r"ms_(?P<ms>[0-9]+)\.png")
1459
- for frame in frames:
1460
- m = re.search(match, frame)
1461
- if m is not None:
1462
- frame_time = int(m.groupdict().get("ms"))
1463
- new_time = max(frame_time - offset, 0)
1464
- dest = os.path.join(directory, "ms_{0:06d}.png".format(new_time))
1465
- if frame != dest:
1466
- if os.path.isfile(dest):
1467
- os.remove(dest)
1468
- os.rename(frame, dest)
1469
-
1470
-
1471
- def get_timeline_offset(timeline_file):
1472
- offset = 0
1473
- try:
1474
- file_name, ext = os.path.splitext(timeline_file)
1475
- if ext.lower() == ".gz":
1476
- f = gzip.open(timeline_file, GZIP_READ_TEXT)
1477
- else:
1478
- f = open(timeline_file, "r")
1479
- timeline = json.load(f)
1480
- f.close()
1481
- last_paint = None
1482
- first_navigate = None
1483
-
1484
- # In the case of a trace instead of a timeline we want the list of
1485
- # events
1486
- if "traceEvents" in timeline:
1487
- timeline = timeline["traceEvents"]
1488
-
1489
- for timeline_event in timeline:
1490
- paint_time = get_timeline_event_paint_time(timeline_event)
1491
- if paint_time is not None:
1492
- last_paint = paint_time
1493
- first_navigate = get_timeline_event_navigate_time(timeline_event)
1494
- if first_navigate is not None:
1495
- break
1496
-
1497
- if (
1498
- last_paint is not None
1499
- and first_navigate is not None
1500
- and first_navigate > last_paint
1501
- ):
1502
- offset = int(round(first_navigate - last_paint))
1503
- logging.info(
1504
- "Trimming {0:d}ms from the start of the video based on timeline synchronization".format(
1505
- offset
1506
- )
1507
- )
1508
- except BaseException:
1509
- logging.critical("Error processing timeline file " + timeline_file)
1510
-
1511
- return offset
1512
-
1513
-
1514
- def get_timeline_event_paint_time(timeline_event):
1515
- paint_time = None
1516
- if "cat" in timeline_event:
1517
- if (
1518
- timeline_event["cat"].find("devtools.timeline") >= 0
1519
- and "ts" in timeline_event
1520
- and "name" in timeline_event
1521
- and (
1522
- timeline_event["name"].find("Paint") >= 0
1523
- or timeline_event["name"].find("CompositeLayers") >= 0
1524
- )
1525
- ):
1526
- paint_time = float(timeline_event["ts"]) / 1000.0
1527
- if "dur" in timeline_event:
1528
- paint_time += float(timeline_event["dur"]) / 1000.0
1529
- elif "method" in timeline_event:
1530
- if (
1531
- timeline_event["method"] == "Timeline.eventRecorded"
1532
- and "params" in timeline_event
1533
- and "record" in timeline_event["params"]
1534
- ):
1535
- paint_time = get_timeline_event_paint_time(
1536
- timeline_event["params"]["record"]
1537
- )
1538
- else:
1539
- if "type" in timeline_event and (
1540
- timeline_event["type"] == "Rasterize"
1541
- or timeline_event["type"] == "CompositeLayers"
1542
- or timeline_event["type"] == "Paint"
1543
- ):
1544
- if "endTime" in timeline_event:
1545
- paint_time = timeline_event["endTime"]
1546
- elif "startTime" in timeline_event:
1547
- paint_time = timeline_event["startTime"]
1548
-
1549
- # Check for any child paint events
1550
- if "children" in timeline_event:
1551
- for child in timeline_event["children"]:
1552
- child_paint_time = get_timeline_event_paint_time(child)
1553
- if child_paint_time is not None and (
1554
- paint_time is None or child_paint_time > paint_time
1555
- ):
1556
- paint_time = child_paint_time
1557
-
1558
- return paint_time
1559
-
1560
-
1561
- def get_timeline_event_navigate_time(timeline_event):
1562
- navigate_time = None
1563
- if "cat" in timeline_event:
1564
- if (
1565
- timeline_event["cat"].find("devtools.timeline") >= 0
1566
- and "ts" in timeline_event
1567
- and "name" in timeline_event
1568
- and timeline_event["name"] == "ResourceSendRequest"
1569
- ):
1570
- navigate_time = float(timeline_event["ts"]) / 1000.0
1571
- elif "method" in timeline_event:
1572
- if (
1573
- timeline_event["method"] == "Timeline.eventRecorded"
1574
- and "params" in timeline_event
1575
- and "record" in timeline_event["params"]
1576
- ):
1577
- navigate_time = get_timeline_event_navigate_time(
1578
- timeline_event["params"]["record"]
1579
- )
1580
- else:
1581
- if (
1582
- "type" in timeline_event
1583
- and timeline_event["type"] == "ResourceSendRequest"
1584
- and "startTime" in timeline_event
1585
- ):
1586
- navigate_time = timeline_event["startTime"]
1587
-
1588
- # Check for any child paint events
1589
- if "children" in timeline_event:
1590
- for child in timeline_event["children"]:
1591
- child_navigate_time = get_timeline_event_navigate_time(child)
1592
- if child_navigate_time is not None and (
1593
- navigate_time is None or child_navigate_time < navigate_time
1594
- ):
1595
- navigate_time = child_navigate_time
1596
-
1597
- return navigate_time
1598
-
1599
-
1600
1076
  ##########################################################################
1601
1077
  # Histogram calculations
1602
1078
  ##########################################################################
@@ -1714,172 +1190,6 @@ def convert_to_jpeg(directory, quality):
1714
1190
 
1715
1191
  logging.debug("Done converting video frames to JPEG")
1716
1192
 
1717
-
1718
- ##########################################################################
1719
- # Video rendering
1720
- ##########################################################################
1721
-
1722
-
1723
- def render_video(directory, video_file):
1724
- """Render the frames to the given mp4 file"""
1725
- directory = os.path.realpath(directory)
1726
- files = sorted(glob.glob(os.path.join(directory, "ms_*.png")))
1727
- if len(files) > 1:
1728
- current_image = None
1729
- with open(os.path.join(directory, files[0]), "rb") as f_in:
1730
- current_image = f_in.read()
1731
- if current_image is not None:
1732
- command = [
1733
- "ffmpeg",
1734
- "-f",
1735
- "image2pipe",
1736
- "-vcodec",
1737
- "png",
1738
- "-r",
1739
- "30",
1740
- "-i",
1741
- "-",
1742
- "-vcodec",
1743
- "libx264",
1744
- "-r",
1745
- "30",
1746
- "-crf",
1747
- "24",
1748
- "-g",
1749
- "15",
1750
- "-preset",
1751
- "superfast",
1752
- "-y",
1753
- video_file,
1754
- ]
1755
- try:
1756
- proc = subprocess.Popen(command, stdin=subprocess.PIPE)
1757
- if proc:
1758
- match = re.compile(r"ms_([0-9]+)\.")
1759
- m = re.search(match, files[1])
1760
- file_index = 0
1761
- last_index = len(files) - 1
1762
- if m is not None:
1763
- next_image_time = int(m.group(1))
1764
- done = False
1765
- current_frame = 0
1766
- while not done:
1767
- current_frame_time = int(
1768
- round(float(current_frame) * 1000.0 / 30.0)
1769
- )
1770
- if current_frame_time >= next_image_time:
1771
- file_index += 1
1772
- with open(
1773
- os.path.join(directory, files[file_index]), "rb"
1774
- ) as f_in:
1775
- current_image = f_in.read()
1776
- if file_index < last_index:
1777
- m = re.search(match, files[file_index + 1])
1778
- if m:
1779
- next_image_time = int(m.group(1))
1780
- else:
1781
- done = True
1782
- proc.stdin.write(current_image)
1783
- current_frame += 1
1784
- # hold the end frame for one second so it's actually
1785
- # visible
1786
- for i in range(30):
1787
- proc.stdin.write(current_image)
1788
- proc.stdin.close()
1789
- proc.communicate()
1790
- except Exception:
1791
- pass
1792
-
1793
-
1794
- ##########################################################################
1795
- # Reduce the number of saved video frames if necessary
1796
- ##########################################################################
1797
- def cap_frame_count(directory, maxframes):
1798
- directory = os.path.realpath(directory)
1799
- frames = sorted(glob.glob(os.path.join(directory, "ms_*.png")))
1800
- frame_count = len(frames)
1801
- if frame_count > maxframes:
1802
- # First pass, sample all video frames at 10fps instead of 60fps,
1803
- # keeping the first 20% of the target
1804
- logging.debug(
1805
- "Sampling 10fps: Reducing {0:d} frames to target of {1:d}...".format(
1806
- frame_count, maxframes
1807
- )
1808
- )
1809
- skip_frames = int(maxframes * 0.2)
1810
- sample_frames(frames, 100, 0, skip_frames)
1811
-
1812
- frames = sorted(glob.glob(os.path.join(directory, "ms_*.png")))
1813
- frame_count = len(frames)
1814
- if frame_count > maxframes:
1815
- # Second pass, sample all video frames after the first 5 seconds at
1816
- # 2fps, keeping the first 40% of the target
1817
- logging.debug(
1818
- "Sampling 2fps: Reducing {0:d} frames to target of {1:d}...".format(
1819
- frame_count, maxframes
1820
- )
1821
- )
1822
- skip_frames = int(maxframes * 0.4)
1823
- sample_frames(frames, 500, 5000, skip_frames)
1824
-
1825
- frames = sorted(glob.glob(os.path.join(directory, "ms_*.png")))
1826
- frame_count = len(frames)
1827
- if frame_count > maxframes:
1828
- # Third pass, sample all video frames after the first 10
1829
- # seconds at 1fps, keeping the first 60% of the target
1830
- logging.debug(
1831
- "Sampling 1fps: Reducing {0:d} frames to target of {1:d}...".format(
1832
- frame_count, maxframes
1833
- )
1834
- )
1835
- skip_frames = int(maxframes * 0.6)
1836
- sample_frames(frames, 1000, 10000, skip_frames)
1837
-
1838
- logging.debug(
1839
- "{0:d} frames final count with a target max of {1:d} frames...".format(
1840
- frame_count, maxframes
1841
- )
1842
- )
1843
-
1844
-
1845
- def sample_frames(frames, interval, start_ms, skip_frames):
1846
- frame_count = len(frames)
1847
- if frame_count > 3:
1848
- # Always keep the first and last frames, only sample in the middle
1849
- first_frame = frames[0]
1850
- first_change = frames[1]
1851
- last_frame = frames[-1]
1852
- match = re.compile(r"ms_(?P<ms>[0-9]+)\.")
1853
- m = re.search(match, first_change)
1854
- first_change_time = 0
1855
- if m is not None:
1856
- first_change_time = int(m.groupdict().get("ms"))
1857
- last_bucket = None
1858
- logging.debug(
1859
- "Sapling frames in {0:d}ms intervals after {1:d} ms, skipping {2:d} frames...".format(
1860
- interval, first_change_time + start_ms, skip_frames
1861
- )
1862
- )
1863
- frame_count = 0
1864
- for frame in frames:
1865
- m = re.search(match, frame)
1866
- if m is not None:
1867
- frame_count += 1
1868
- frame_time = int(m.groupdict().get("ms"))
1869
- frame_bucket = int(math.floor(frame_time / interval))
1870
- if (
1871
- frame_time > first_change_time + start_ms
1872
- and frame_bucket == last_bucket
1873
- and frame != first_frame
1874
- and frame != first_change
1875
- and frame != last_frame
1876
- and frame_count > skip_frames
1877
- ):
1878
- logging.debug("Removing sampled frame " + frame)
1879
- os.remove(frame)
1880
- last_bucket = frame_bucket
1881
-
1882
-
1883
1193
  ##########################################################################
1884
1194
  # Visual Metrics
1885
1195
  ##########################################################################
@@ -2350,13 +1660,8 @@ def check_config():
2350
1660
 
2351
1661
  def check_process(command, output):
2352
1662
  ok = False
2353
- try:
2354
- if sys.version_info > (3, 0):
2355
- out = subprocess.check_output(
2356
- command, stderr=subprocess.STDOUT, shell=True, encoding="UTF-8"
2357
- )
2358
- else:
2359
- out = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True)
1663
+ try:
1664
+ out = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True, encoding="UTF-8")
2360
1665
  if out.find(output) > -1:
2361
1666
  ok = True
2362
1667
  except BaseException:
@@ -2414,19 +1719,6 @@ def main():
2414
1719
  "--screenshot",
2415
1720
  help="Save the last frame of video as an image to the path provided.",
2416
1721
  )
2417
- parser.add_argument(
2418
- "-g",
2419
- "--histogram",
2420
- help="Histogram file (as input if exists or as output if "
2421
- "histograms need to be calculated).",
2422
- )
2423
- parser.add_argument(
2424
- "-m",
2425
- "--timeline",
2426
- help="Timeline capture from Chrome dev tools. Used to synchronize the video"
2427
- " start time and only applies when orange frames are removed "
2428
- "(see --orange). The timeline file can be gzipped if it ends in .gz",
2429
- )
2430
1722
  parser.add_argument(
2431
1723
  "-q",
2432
1724
  "--quality",
@@ -2457,37 +1749,6 @@ def main():
2457
1749
  default=False,
2458
1750
  help="Remove orange-colored frames from the beginning of the video.",
2459
1751
  )
2460
- parser.add_argument(
2461
- "--gray",
2462
- action="store_true",
2463
- default=False,
2464
- help="Remove gray-colored frames from the beginning of the video.",
2465
- )
2466
- parser.add_argument(
2467
- "-w",
2468
- "--white",
2469
- action="store_true",
2470
- default=False,
2471
- help="Wait for a full white frame after a non-white frame "
2472
- "at the beginning of the video.",
2473
- )
2474
- parser.add_argument(
2475
- "--multiple",
2476
- action="store_true",
2477
- default=False,
2478
- help="Multiple videos are combined, separated by orange frames."
2479
- "In this mode only the extraction is done and analysis "
2480
- "needs to be run separetely on each directory. Numbered "
2481
- "directories will be created for each video under the output "
2482
- "directory.",
2483
- )
2484
- parser.add_argument(
2485
- "-n",
2486
- "--notification",
2487
- action="store_true",
2488
- default=False,
2489
- help="Trim the notification and home bars from the window.",
2490
- )
2491
1752
  parser.add_argument(
2492
1753
  "-p",
2493
1754
  "--viewport",
@@ -2495,12 +1756,6 @@ def main():
2495
1756
  default=False,
2496
1757
  help="Locate and use the viewport from the first video frame.",
2497
1758
  )
2498
- parser.add_argument(
2499
- "-t",
2500
- "--viewporttime",
2501
- help="Time of the video frame to use for identifying the viewport "
2502
- "(in HH:MM:SS.xx format).",
2503
- )
2504
1759
  parser.add_argument(
2505
1760
  "--viewportretries",
2506
1761
  type=int,
@@ -2537,13 +1792,6 @@ def main():
2537
1792
  default=0,
2538
1793
  help="End time (in milliseconds) for calculating visual metrics.",
2539
1794
  )
2540
- parser.add_argument(
2541
- "--findstart",
2542
- type=int,
2543
- default=0,
2544
- help="Find the start of activity by looking at the top X%% "
2545
- "of the video (like a browser address bar).",
2546
- )
2547
1795
  parser.add_argument(
2548
1796
  "--renderignore",
2549
1797
  type=int,
@@ -2551,38 +1799,6 @@ def main():
2551
1799
  help="Ignore the center X%% of the frame when looking for "
2552
1800
  "the first rendered frame (useful for Opera mini).",
2553
1801
  )
2554
- parser.add_argument(
2555
- "--startwhite",
2556
- action="store_true",
2557
- default=False,
2558
- help="Find the first fully white frame as the start of the video.",
2559
- )
2560
- parser.add_argument(
2561
- "--endwhite",
2562
- action="store_true",
2563
- default=False,
2564
- help="Find the first fully white frame after render start as the "
2565
- "end of the video.",
2566
- )
2567
- parser.add_argument(
2568
- "--forceblank",
2569
- action="store_true",
2570
- default=False,
2571
- help="Force the first frame to be blank white.",
2572
- )
2573
- parser.add_argument(
2574
- "--trimend",
2575
- type=int,
2576
- default=0,
2577
- help="Time to trim from the end of the video (in milliseconds).",
2578
- )
2579
- parser.add_argument(
2580
- "--maxframes",
2581
- type=int,
2582
- default=0,
2583
- help="Maximum number of video frames before reducing by "
2584
- "sampling (to 10fps, 1fps, etc).",
2585
- )
2586
1802
  parser.add_argument(
2587
1803
  "-k",
2588
1804
  "--perceptual",
@@ -2620,7 +1836,6 @@ def main():
2620
1836
  not options.check
2621
1837
  and not options.dir
2622
1838
  and not options.video
2623
- and not options.histogram
2624
1839
  ):
2625
1840
  parser.error(
2626
1841
  "A video, Directory of images or histograms file needs to be provided.\n\n"
@@ -2639,10 +1854,7 @@ def main():
2639
1854
  directory = temp_dir
2640
1855
  if options.dir is not None:
2641
1856
  directory = options.dir
2642
- if options.histogram is not None:
2643
- histogram_file = options.histogram
2644
- else:
2645
- histogram_file = os.path.join(temp_dir, "histograms.json.gz")
1857
+ histogram_file = os.path.join(temp_dir, "histograms.json.gz")
2646
1858
 
2647
1859
  # Set up logging
2648
1860
  log_level = logging.CRITICAL
@@ -2666,9 +1878,6 @@ def main():
2666
1878
  level=log_level, format=options.logformat, datefmt="%H:%M:%S"
2667
1879
  )
2668
1880
 
2669
- if options.multiple:
2670
- options.orange = True
2671
-
2672
1881
  ok = False
2673
1882
  try:
2674
1883
  if not options.check:
@@ -2688,77 +1897,52 @@ def main():
2688
1897
  if not os.path.isfile(orange_file):
2689
1898
  orange_file = os.path.join(colors_temp_dir, "orange.png")
2690
1899
  generate_orange_png(orange_file)
2691
- white_file = None
2692
- if options.white or options.startwhite or options.endwhite:
2693
- white_file = os.path.join(
2694
- os.path.dirname(os.path.realpath(__file__)), "white.png"
2695
- )
2696
- if not os.path.isfile(white_file):
2697
- white_file = os.path.join(colors_temp_dir, "white.png")
2698
- generate_white_png(white_file)
2699
- gray_file = None
2700
- if options.gray:
2701
- gray_file = os.path.join(
2702
- os.path.dirname(os.path.realpath(__file__)), "gray.png"
2703
- )
2704
- if not os.path.isfile(gray_file):
2705
- gray_file = os.path.join(colors_temp_dir, "gray.png")
2706
- generate_gray_png(gray_file)
2707
1900
  video_to_frames(
2708
1901
  options.video,
2709
1902
  directory,
2710
1903
  options.force,
2711
1904
  orange_file,
2712
- white_file,
2713
- gray_file,
2714
- options.multiple,
2715
1905
  options.viewport,
2716
- options.viewporttime,
2717
1906
  options.viewportretries,
2718
1907
  options.viewportminheight,
2719
1908
  options.viewportminwidth,
2720
1909
  options.full,
2721
- options.timeline,
2722
- options.trimend,
2723
- )
2724
- if not options.multiple:
2725
- if options.render is not None:
2726
- render_video(directory, options.render)
2727
-
2728
- # Calculate the histograms and visual metrics
2729
- calculate_histograms(directory, histogram_file, options.force)
2730
- metrics = calculate_visual_metrics(
2731
- histogram_file,
2732
- options.start,
2733
- options.end,
2734
- options.perceptual,
2735
- options.contentful,
2736
- directory,
2737
- options.progress,
2738
- options.herodata,
2739
1910
  )
1911
+
1912
+ # Calculate the histograms and visual metrics
1913
+ calculate_histograms(directory, histogram_file, options.force)
1914
+ metrics = calculate_visual_metrics(
1915
+ histogram_file,
1916
+ options.start,
1917
+ options.end,
1918
+ options.perceptual,
1919
+ options.contentful,
1920
+ directory,
1921
+ options.progress,
1922
+ options.herodata,
1923
+ )
2740
1924
 
2741
- if options.screenshot is not None:
2742
- quality = 30
2743
- if options.quality is not None:
2744
- quality = options.quality
2745
- save_screenshot(directory, options.screenshot, quality)
2746
- # JPEG conversion
2747
- if options.dir is not None and options.quality is not None:
2748
- convert_to_jpeg(directory, options.quality)
2749
-
2750
- if metrics is not None:
2751
- ok = True
2752
- if options.json:
2753
- data = dict()
2754
- for metric in metrics:
2755
- data[metric["name"].replace(" ", "")] = metric["value"]
2756
- if "videoRecordingStart" in globals():
2757
- data["videoRecordingStart"] = videoRecordingStart
2758
- print(json.dumps(data))
2759
- else:
2760
- for metric in metrics:
2761
- print("{0}: {1}".format(metric["name"], metric["value"]))
1925
+ if options.screenshot is not None:
1926
+ quality = 30
1927
+ if options.quality is not None:
1928
+ quality = options.quality
1929
+ save_screenshot(directory, options.screenshot, quality)
1930
+ # JPEG conversion
1931
+ if options.dir is not None and options.quality is not None:
1932
+ convert_to_jpeg(directory, options.quality)
1933
+
1934
+ if metrics is not None:
1935
+ ok = True
1936
+ if options.json:
1937
+ data = dict()
1938
+ for metric in metrics:
1939
+ data[metric["name"].replace(" ", "")] = metric["value"]
1940
+ if "videoRecordingStart" in globals():
1941
+ data["videoRecordingStart"] = videoRecordingStart
1942
+ print(json.dumps(data))
1943
+ else:
1944
+ for metric in metrics:
1945
+ print("{0}: {1}".format(metric["name"], metric["value"]))
2762
1946
  else:
2763
1947
  ok = check_config()
2764
1948
  except Exception as e: