Home | History | Annotate | Download | only in video_PlaybackQuality
      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