1# Copyright 2014 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import subprocess 6 7from catapult_base import cloud_storage # pylint: disable=import-error 8 9from telemetry.core import platform 10from telemetry.util import image_util 11from telemetry.util import rgba_color 12 13HIGHLIGHT_ORANGE_FRAME = rgba_color.WEB_PAGE_TEST_ORANGE 14 15class BoundingBoxNotFoundException(Exception): 16 pass 17 18 19class Video(object): 20 """Utilities for storing and interacting with the video capture.""" 21 22 def __init__(self, video_file_obj): 23 assert video_file_obj.delete 24 assert not video_file_obj.close_called 25 self._video_file_obj = video_file_obj 26 self._tab_contents_bounding_box = None 27 28 def UploadToCloudStorage(self, bucket, target_path): 29 """Uploads video file to cloud storage. 30 31 Args: 32 target_path: Path indicating where to store the file in cloud storage. 33 """ 34 cloud_storage.Insert(bucket, target_path, self._video_file_obj.name) 35 36 def GetVideoFrameIter(self): 37 """Returns the iteration for processing the video capture. 38 39 This looks for the initial color flash in the first frame to establish the 40 tab content boundaries and then omits all frames displaying the flash. 41 42 Yields: 43 (time_ms, image) tuples representing each video keyframe. Only the first 44 frame is a run of sequential duplicate bitmaps is typically included. 45 time_ms is milliseconds since navigationStart. 46 image may be a telemetry.core.Bitmap, or a numpy array depending on 47 whether numpy is installed. 48 """ 49 frame_generator = self._FramesFromMp4(self._video_file_obj.name) 50 51 # Flip through frames until we find the initial tab contents flash. 52 content_box = None 53 for _, bmp in frame_generator: 54 content_box = self._FindHighlightBoundingBox( 55 bmp, HIGHLIGHT_ORANGE_FRAME) 56 if content_box: 57 break 58 59 if not content_box: 60 raise BoundingBoxNotFoundException( 61 'Failed to identify tab contents in video capture.') 62 63 # Flip through frames until the flash goes away and emit that as frame 0. 64 timestamp = 0 65 for timestamp, bmp in frame_generator: 66 if not self._FindHighlightBoundingBox(bmp, HIGHLIGHT_ORANGE_FRAME): 67 yield 0, image_util.Crop(bmp, *content_box) 68 break 69 70 start_time = timestamp 71 for timestamp, bmp in frame_generator: 72 yield timestamp - start_time, image_util.Crop(bmp, *content_box) 73 74 def _FindHighlightBoundingBox(self, bmp, color, bounds_tolerance=8, 75 color_tolerance=8): 76 """Returns the bounding box of the content highlight of the given color. 77 78 Raises: 79 BoundingBoxNotFoundException if the hightlight could not be found. 80 """ 81 content_box, pixel_count = image_util.GetBoundingBox(bmp, color, 82 tolerance=color_tolerance) 83 84 if not content_box: 85 return None 86 87 # We assume arbitrarily that tabs are all larger than 200x200. If this 88 # fails it either means that assumption has changed or something is 89 # awry with our bounding box calculation. 90 if content_box[2] < 200 or content_box[3] < 200: 91 raise BoundingBoxNotFoundException('Unexpectedly small tab contents.') 92 93 # TODO(tonyg): Can this threshold be increased? 94 if pixel_count < 0.9 * content_box[2] * content_box[3]: 95 raise BoundingBoxNotFoundException( 96 'Low count of pixels in tab contents matching expected color.') 97 98 # Since we allow some fuzziness in bounding box finding, we want to make 99 # sure that the bounds are always stable across a run. So we cache the 100 # first box, whatever it may be. 101 # 102 # This relies on the assumption that since Telemetry doesn't know how to 103 # resize the window, we should always get the same content box for a tab. 104 # If this assumption changes, this caching needs to be reworked. 105 if not self._tab_contents_bounding_box: 106 self._tab_contents_bounding_box = content_box 107 108 # Verify that there is only minor variation in the bounding box. If it's 109 # just a few pixels, we can assume it's due to compression artifacts. 110 for x, y in zip(self._tab_contents_bounding_box, content_box): 111 if abs(x - y) > bounds_tolerance: 112 # If this fails, it means either that either the above assumption has 113 # changed or something is awry with our bounding box calculation. 114 raise BoundingBoxNotFoundException( 115 'Unexpected change in tab contents box.') 116 117 return self._tab_contents_bounding_box 118 119 def _FramesFromMp4(self, mp4_file): 120 host_platform = platform.GetHostPlatform() 121 if not host_platform.CanLaunchApplication('avconv'): 122 host_platform.InstallApplication('avconv') 123 124 def GetDimensions(video): 125 proc = subprocess.Popen(['avconv', '-i', video], stderr=subprocess.PIPE) 126 dimensions = None 127 output = '' 128 for line in proc.stderr.readlines(): 129 output += line 130 if 'Video:' in line: 131 dimensions = line.split(',')[2] 132 dimensions = map(int, dimensions.split()[0].split('x')) 133 break 134 proc.communicate() 135 assert dimensions, ('Failed to determine video dimensions. output=%s' % 136 output) 137 return dimensions 138 139 def GetFrameTimestampMs(stderr): 140 """Returns the frame timestamp in integer milliseconds from the dump log. 141 142 The expected line format is: 143 ' dts=1.715 pts=1.715\n' 144 145 We have to be careful to only read a single timestamp per call to avoid 146 deadlock because avconv interleaves its writes to stdout and stderr. 147 """ 148 while True: 149 line = '' 150 next_char = '' 151 while next_char != '\n': 152 next_char = stderr.read(1) 153 line += next_char 154 if 'pts=' in line: 155 return int(1000 * float(line.split('=')[-1])) 156 157 dimensions = GetDimensions(mp4_file) 158 frame_length = dimensions[0] * dimensions[1] * 3 159 frame_data = bytearray(frame_length) 160 161 # Use rawvideo so that we don't need any external library to parse frames. 162 proc = subprocess.Popen(['avconv', '-i', mp4_file, '-vcodec', 163 'rawvideo', '-pix_fmt', 'rgb24', '-dump', 164 '-loglevel', 'debug', '-f', 'rawvideo', '-'], 165 stderr=subprocess.PIPE, stdout=subprocess.PIPE) 166 while True: 167 num_read = proc.stdout.readinto(frame_data) 168 if not num_read: 169 raise StopIteration 170 assert num_read == len(frame_data), 'Unexpected frame size: %d' % num_read 171 yield (GetFrameTimestampMs(proc.stderr), 172 image_util.FromRGBPixels(dimensions[0], dimensions[1], frame_data)) 173