1 # Copyright 2016 The Chromium OS 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 5 import logging 6 import os 7 import tempfile 8 from PIL import Image 9 10 from autotest_lib.client.bin import utils 11 from autotest_lib.client.common_lib import error 12 from autotest_lib.client.common_lib import file_utils 13 from autotest_lib.client.cros.chameleon import chameleon_port_finder 14 from autotest_lib.client.cros.chameleon import chameleon_stream_server 15 from autotest_lib.client.cros.chameleon import edid 16 from autotest_lib.server import test 17 from autotest_lib.server.cros.multimedia import remote_facade_factory 18 19 20 class video_PlaybackQuality(test.test): 21 """Server side video playback quality measurement. 22 23 This test measures the video playback quality by chameleon. 24 It will output 2 performance data. Number of Corrupted Frames and Number of 25 Dropped Frames. 26 27 """ 28 version = 1 29 30 # treat 0~0x30 as 0 31 COLOR_MARGIN_0 = 0x30 32 # treat (0xFF-0x60)~0xFF as 0xFF. 33 COLOR_MARGIN_255 = 0xFF - 0x60 34 35 # If we can't find the expected frame after TIMEOUT_FRAMES, raise exception. 36 TIMEOUT_FRAMES = 120 37 38 # RGB for black. Used for preamble and postamble. 39 RGB_BLACK = [0, 0, 0] 40 41 # Expected color bar rgb. The color order in the array is the same order in 42 # the video frames. 43 EXPECTED_RGB = [('Blue', [0, 0, 255]), ('Green', [0, 255, 0]), 44 ('Cyan', [0, 255, 255]), ('Red', [255, 0, 0]), 45 ('Magenta', [255, 0, 255]), ('Yellow', [255, 255, 0]), 46 ('White', [255, 255, 255])] 47 48 def _save_frame_to_file(self, resolution, frame, filename): 49 """Save video frame to file under results directory. 50 51 This function will append .png filename extension. 52 53 @param resolution: A tuple (width, height) of resolution. 54 @param frame: The video frame data. 55 @param filename: File name. 56 57 """ 58 image = Image.fromstring('RGB', resolution, frame) 59 image.save('%s/%s.png' % (self.resultsdir, filename)) 60 61 def _check_rgb_value(self, value, expected_value): 62 """Check value of the RGB. 63 64 This function will check if the value is in the range of expected value 65 and its margin. 66 67 @param value: The value for checking. 68 @param expected_value: Expected value. It's ether 0 or 0xFF. 69 @returns: True if the value is in range. False otherwise. 70 71 """ 72 if expected_value <= value <= self.COLOR_MARGIN_0: 73 return True 74 75 if expected_value >= value >= self.COLOR_MARGIN_255: 76 return True 77 78 return False 79 80 def _check_rgb(self, frame, expected_rgb): 81 """Check the RGB raw data of all pixels in a video frame. 82 83 Because checking all pixels may take more than one video frame time. If 84 we want to analyze the video frame on the fly, we need to skip pixels 85 for saving the checking time. 86 The parameter of how many pixels to skip is self._skip_check_pixels. 87 88 @param frame: Array of all pixels of video frame. 89 @param expected_rgb: Expected values for RGB. 90 @returns: number of error pixels. 91 92 """ 93 error_number = 0 94 95 for i in xrange(0, len(frame), 3 * (self._skip_check_pixels + 1)): 96 if not self._check_rgb_value(ord(frame[i]), expected_rgb[0]): 97 error_number += 1 98 continue 99 100 if not self._check_rgb_value(ord(frame[i + 1]), expected_rgb[1]): 101 error_number += 1 102 continue 103 104 if not self._check_rgb_value(ord(frame[i + 2]), expected_rgb[2]): 105 error_number += 1 106 107 return error_number 108 109 def _find_and_skip_preamble(self, description): 110 """Find and skip the preamble video frames. 111 112 @param description: Description of the log and file name. 113 114 """ 115 # find preamble which is the first black frame. 116 number_of_frames = 0 117 while True: 118 video_frame = self._stream_server.receive_realtime_video_frame() 119 (frame_number, width, height, _, frame) = video_frame 120 if self._check_rgb(frame, self.RGB_BLACK) == 0: 121 logging.info('Find preamble at frame %d', frame_number) 122 break 123 if number_of_frames > self.TIMEOUT_FRAMES: 124 raise error.TestFail('%s found no preamble' % description) 125 number_of_frames += 1 126 self._save_frame_to_file((width, height), frame, 127 '%s_pre_%d' % (description, frame_number)) 128 # skip preamble. 129 # After finding preamble, find the first frame that is not black. 130 number_of_frames = 0 131 while True: 132 video_frame = self._stream_server.receive_realtime_video_frame() 133 (frame_number, _, _, _, frame) = video_frame 134 if self._check_rgb(frame, self.RGB_BLACK) != 0: 135 logging.info('End preamble at frame %d', frame_number) 136 self._save_frame_to_file((width, height), frame, 137 '%s_end_preamble' % description) 138 break 139 if number_of_frames > self.TIMEOUT_FRAMES: 140 raise error.TestFail('%s found no color bar' % description) 141 number_of_frames += 1 142 143 def _store_wrong_frames(self, frame_number, resolution, frames): 144 """Store wrong frames for debugging. 145 146 @param frame_number: latest frame number. 147 @param resolution: A tuple (width, height) of resolution. 148 @param frames: Array of video frames. The latest video frame is in the 149 front. 150 151 """ 152 for index, frame in enumerate(frames): 153 if not frame: 154 continue 155 element = ((frame_number - index), resolution, frame) 156 self._wrong_frames.append(element) 157 158 def _check_color_bars(self, description): 159 """Check color bars for video playback quality. 160 161 This function will read video frame from stream server and check if the 162 color is right by self._check_rgb until read postamble. 163 If only some pixels are wrong, the frame will be counted to corrupted 164 frame. If all pixels are wrong, the frame will be counted to wrong 165 frame. 166 167 @param description: Description of log and file name. 168 @return A tuple (corrupted_frame_count, wrong_frame_count) for quality 169 data. 170 171 """ 172 # store the recent 2 video frames for debugging. 173 # Put the latest frame in the front. 174 frame_history = [None, None] 175 # Check index for color bars. 176 check_index = 0 177 corrupted_frame_count = 0 178 wrong_frame_count = 0 179 while True: 180 # Because the first color bar is skipped in _find_and_skip_preamble, 181 # we start from the 2nd color. 182 check_index = (check_index + 1) % len(self.EXPECTED_RGB) 183 video_frame = self._stream_server.receive_realtime_video_frame() 184 (frame_number, width, height, _, frame) = video_frame 185 # drop old video frame and store new one 186 frame_history.pop(-1) 187 frame_history.insert(0, frame) 188 color_name = self.EXPECTED_RGB[check_index][0] 189 expected_rgb = self.EXPECTED_RGB[check_index][1] 190 error_number = self._check_rgb(frame, expected_rgb) 191 192 # The video frame is correct, go to next video frame. 193 if not error_number: 194 continue 195 196 # Total pixels need to be adjusted by the _skip_check_pixels. 197 total_pixels = width * height / (self._skip_check_pixels + 1) 198 log_string = ('[%s] Number of error pixels %d on frame %d, ' 199 'expected color %s, RGB %r' % 200 (description, error_number, frame_number, color_name, 201 expected_rgb)) 202 203 self._store_wrong_frames(frame_number, (width, height), 204 frame_history) 205 # clean history after they are stored. 206 frame_history = [None, None] 207 208 # Some pixels are wrong. 209 if error_number != total_pixels: 210 corrupted_frame_count += 1 211 logging.warn('[Corrupted]%s', log_string) 212 continue 213 214 # All pixels are wrong. 215 # Check if we get postamble where all pixels are black. 216 if self._check_rgb(frame, self.RGB_BLACK) == 0: 217 logging.info('Find postamble at frame %d', frame_number) 218 break 219 220 wrong_frame_count += 1 221 logging.info('[Wrong]%s', log_string) 222 # Adjust the check index due to frame drop. 223 # The screen should keep the old frame or go to next video frame 224 # due to frame drop. 225 # Check if color is the same as the previous frame. 226 # If it is not the same as previous frame, we assign the color of 227 # next frame without checking. 228 previous_index = ((check_index + len(self.EXPECTED_RGB) - 1) 229 % len(self.EXPECTED_RGB)) 230 if not self._check_rgb(frame, self.EXPECTED_RGB[previous_index][1]): 231 check_index = previous_index 232 else: 233 check_index = (check_index + 1) % len(self.EXPECTED_RGB) 234 235 return (corrupted_frame_count, wrong_frame_count) 236 237 def _dump_wrong_frames(self, description): 238 """Dump wrong frames to files. 239 240 @param description: Description of the file name. 241 242 """ 243 for frame_number, resolution, frame in self._wrong_frames: 244 self._save_frame_to_file(resolution, frame, 245 '%s_%d' % (description, frame_number)) 246 self._wrong_frames = [] 247 248 def _prepare_playback(self): 249 """Prepare playback video.""" 250 # Workaround for white bar on rightmost and bottommost on samus when we 251 # set fullscreen from fullscreen. 252 self._display_facade.set_fullscreen(False) 253 self._video_facade.prepare_playback(self._video_tempfile.name) 254 255 def _get_playback_quality(self, description, capture_dimension): 256 """Get the playback quality. 257 258 This function will playback the video and analysis each video frames. 259 It will output performance data too. 260 261 @param description: Description of the log, file name and performance 262 data. 263 @param capture_dimension: A tuple (width, height) of the captured video 264 frame. 265 """ 266 logging.info('Start to get %s playback quality', description) 267 self._prepare_playback() 268 self._chameleon_port.start_capturing_video(capture_dimension) 269 self._stream_server.reset_video_session() 270 self._stream_server.dump_realtime_video_frame( 271 False, chameleon_stream_server.RealtimeMode.BestEffort) 272 273 self._video_facade.start_playback() 274 self._find_and_skip_preamble(description) 275 276 (corrupted_frame_count, wrong_frame_count) = ( 277 self._check_color_bars(description)) 278 279 self._stream_server.stop_dump_realtime_video_frame() 280 self._chameleon_port.stop_capturing_video() 281 self._video_facade.pause_playback() 282 self._dump_wrong_frames(description) 283 284 dropped_frame_count = self._video_facade.dropped_frame_count() 285 286 graph_name = '%s_%s' % (self._video_description, description) 287 self.output_perf_value(description='Corrupted frames', 288 value=corrupted_frame_count, units='frame', 289 higher_is_better=False, graph=graph_name) 290 self.output_perf_value(description='Wrong frames', 291 value=wrong_frame_count, units='frame', 292 higher_is_better=False, graph=graph_name) 293 self.output_perf_value(description='Dropped frames', 294 value=dropped_frame_count, units='frame', 295 higher_is_better=False, graph=graph_name) 296 297 def run_once(self, host, video_url, video_description, test_regions, 298 skip_check_pixels=5): 299 """Runs video playback quality measurement. 300 301 @param host: A host object representing the DUT. 302 @param video_url: The ULR of the test video. 303 @param video_description: a string describes the video to play which 304 will be part of entry name in dashboard. 305 @param test_regions: An array of tuples (description, capture_dimension) 306 for the testing region of video. capture_dimension is a tuple 307 (width, height). 308 @param skip_check_pixels: We will check one pixel and skip number of 309 pixels. 0 means no skip. 1 means check 1 pixel and skip 1 pixel. 310 Because we may take more than 1 video frame time for checking 311 all pixels. Skip some pixles for saving time. 312 313 """ 314 # Store wrong video frames for dumping and debugging. 315 self._video_url = video_url 316 self._video_description = video_description 317 self._wrong_frames = [] 318 self._skip_check_pixels = skip_check_pixels 319 320 factory = remote_facade_factory.RemoteFacadeFactory( 321 host, results_dir=self.resultsdir, no_chrome=True) 322 chameleon_board = host.chameleon 323 browser_facade = factory.create_browser_facade() 324 display_facade = factory.create_display_facade() 325 self._display_facade = display_facade 326 self._video_facade = factory.create_video_facade() 327 self._stream_server = chameleon_stream_server.ChameleonStreamServer( 328 chameleon_board.host.hostname) 329 330 chameleon_board.setup_and_reset(self.outputdir) 331 self._stream_server.connect() 332 333 # Download the video to self._video_tempfile.name 334 _, ext = os.path.splitext(video_url) 335 self._video_tempfile = tempfile.NamedTemporaryFile(suffix=ext) 336 # The default permission is 0o600. 337 os.chmod(self._video_tempfile.name, 0o644) 338 file_utils.download_file(video_url, self._video_tempfile.name) 339 340 browser_facade.start_default_chrome() 341 display_facade.set_mirrored(False) 342 343 edid_path = os.path.join(self.bindir, 'test_data', 'edids', 344 'EDIDv2_1920x1080') 345 finder = chameleon_port_finder.ChameleonVideoInputFinder( 346 chameleon_board, display_facade) 347 for chameleon_port in finder.iterate_all_ports(): 348 self._chameleon_port = chameleon_port 349 350 connector_type = chameleon_port.get_connector_type() 351 logging.info('See the display on Chameleon: port %d (%s)', 352 chameleon_port.get_connector_id(), 353 connector_type) 354 355 with chameleon_port.use_edid( 356 edid.Edid.from_file(edid_path, skip_verify=True)): 357 resolution = utils.wait_for_value_changed( 358 display_facade.get_external_resolution, 359 old_value=None) 360 if resolution is None: 361 raise error.TestFail('No external display detected on DUT') 362 363 display_facade.move_to_display( 364 display_facade.get_first_external_display_id()) 365 366 for description, capture_dimension in test_regions: 367 self._get_playback_quality('%s_%s' % (connector_type, 368 description), 369 capture_dimension) 370