Home | History | Annotate | Download | only in audiovideo_AVSync
      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 json
      6 import logging
      7 import os
      8 import struct
      9 import tempfile
     10 import time
     11 
     12 from autotest_lib.client.bin import utils
     13 from autotest_lib.client.common_lib import file_utils
     14 from autotest_lib.client.common_lib.cros import arc_common
     15 from autotest_lib.client.cros import constants
     16 from autotest_lib.client.cros.chameleon import audio_test_utils
     17 from autotest_lib.client.cros.chameleon import chameleon_port_finder
     18 from autotest_lib.client.cros.multimedia import arc_resource_common
     19 from autotest_lib.server import autotest
     20 from autotest_lib.server import test
     21 from autotest_lib.server.cros.multimedia import remote_facade_factory
     22 
     23 
     24 class audiovideo_AVSync(test.test):
     25     """ Server side HDMI audio/video sync quality measurement
     26 
     27     This test talks to a Chameleon board and a Cros device to measure the
     28     audio/video sync quality under playing a 1080p 60fps video.
     29     """
     30     version = 1
     31 
     32     AUDIO_CAPTURE_RATE = 48000
     33     VIDEO_CAPTURE_RATE = 60
     34 
     35     BEEP_THRESHOLD = 10 ** 9
     36 
     37     DELAY_BEFORE_CAPTURING = 2
     38     DELAY_BEFORE_PLAYBACK = 2
     39     DELAY_AFTER_PLAYBACK = 2
     40 
     41     DEFAULT_VIDEO_URL = ('http://commondatastorage.googleapis.com/'
     42                          'chromiumos-test-assets-public/chameleon/'
     43                          'audiovideo_AVSync/1080p_60fps.mp4')
     44 
     45     WAIT_CLIENT_READY_TIMEOUT_SECS = 120
     46 
     47     def compute_audio_keypoint(self, data):
     48         """Compute audio keypoints. Audio keypoints are the starting times of
     49         beeps.
     50 
     51         @param data: Raw captured audio data in S32LE, 8 channels, 48000 Hz.
     52 
     53         @returns: Key points of captured data put in a list.
     54         """
     55         keypoints = []
     56         sample_no = 0
     57         last_beep_no = -100
     58         for i in xrange(0, len(data), 32):
     59             values = struct.unpack('<8i', data[i:i+32])
     60             if values[0] > self.BEEP_THRESHOLD:
     61                 if sample_no - last_beep_no >= 100:
     62                     keypoints.append(sample_no / float(self.AUDIO_CAPTURE_RATE))
     63                 last_beep_no = sample_no
     64             sample_no += 1
     65         return keypoints
     66 
     67 
     68     def compute_video_keypoint(self, checksum):
     69         """Compute video keypoints. Video keypoints are the times when the
     70         checksum changes.
     71 
     72         @param checksum: Checksums of frames put in a list.
     73 
     74         @returns: Key points of captured video data put in a list.
     75         """
     76         return [i / float(self.VIDEO_CAPTURE_RATE)
     77                 for i in xrange(1, len(checksum))
     78                 if checksum[i] != checksum[i - 1]]
     79 
     80 
     81     def log_result(self, prefix, key_audio, key_video, dropped_frame_count):
     82         """Log the test result to result.json and the dashboard.
     83 
     84         @param prefix: A string distinguishes between subtests.
     85         @param key_audio: Key points of captured audio data put in a list.
     86         @param key_video: Key points of captured video data put in a list.
     87         @param dropped_frame_count: Number of dropped frames.
     88         """
     89         log_path = os.path.join(self.resultsdir, 'result.json')
     90         diff = map(lambda x: x[0] - x[1], zip(key_audio, key_video))
     91         diff_range = max(diff) - min(diff)
     92         result = dict(
     93             key_audio=key_audio,
     94             key_video=key_video,
     95             av_diff=diff,
     96             diff_range=diff_range
     97         )
     98         if dropped_frame_count is not None:
     99             result['dropped_frame_count'] = dropped_frame_count
    100 
    101         result = json.dumps(result, indent=2)
    102         with open(log_path, 'w') as f:
    103             f.write(result)
    104         logging.info(str(result))
    105 
    106         dashboard_result = dict(
    107             diff_range=[diff_range, 'seconds'],
    108             max_diff=[max(diff), 'seconds'],
    109             min_diff=[min(diff), 'seconds'],
    110             average_diff=[sum(diff) / len(diff), 'seconds']
    111         )
    112         if dropped_frame_count is not None:
    113             dashboard_result['dropped_frame_count'] = [
    114                     dropped_frame_count, 'frames']
    115 
    116         for key, value in dashboard_result.iteritems():
    117             self.output_perf_value(description=prefix+key, value=value[0],
    118                                    units=value[1], higher_is_better=False)
    119 
    120 
    121     def run_once(self, host, video_hardware_acceleration=True,
    122                  video_url=DEFAULT_VIDEO_URL, arc=False):
    123         """Running audio/video synchronization quality measurement
    124 
    125         @param host: A host object representing the DUT.
    126         @param video_hardware_acceleration: Enables the hardware acceleration
    127                                             for video decoding.
    128         @param video_url: The ULR of the test video.
    129         @param arc: Tests on ARC with an Android Video Player App.
    130         """
    131         self.host = host
    132 
    133         factory = remote_facade_factory.RemoteFacadeFactory(
    134                 host, results_dir=self.resultsdir, no_chrome=True)
    135 
    136         chrome_args = {
    137             'extension_paths': [constants.AUDIO_TEST_EXTENSION,
    138                                 constants.DISPLAY_TEST_EXTENSION],
    139             'extra_browser_args': [],
    140             'arc_mode': arc_common.ARC_MODE_DISABLED,
    141             'autotest_ext': True
    142         }
    143         if not video_hardware_acceleration:
    144             chrome_args['extra_browser_args'].append(
    145                     '--disable-accelerated-video-decode')
    146         if arc:
    147             chrome_args['arc_mode'] = arc_common.ARC_MODE_ENABLED
    148         browser_facade = factory.create_browser_facade()
    149         browser_facade.start_custom_chrome(chrome_args)
    150         logging.info("created chrome")
    151         if arc:
    152             self.setup_video_app()
    153 
    154         chameleon_board = host.chameleon
    155         audio_facade = factory.create_audio_facade()
    156         display_facade = factory.create_display_facade()
    157         video_facade = factory.create_video_facade()
    158 
    159         audio_port_finder = chameleon_port_finder.ChameleonAudioInputFinder(
    160                 chameleon_board)
    161         video_port_finder = chameleon_port_finder.ChameleonVideoInputFinder(
    162                 chameleon_board, display_facade)
    163         audio_port = audio_port_finder.find_port('HDMI')
    164         video_port = video_port_finder.find_port('HDMI')
    165 
    166         chameleon_board.setup_and_reset(self.outputdir)
    167 
    168         _, ext = os.path.splitext(video_url)
    169         with tempfile.NamedTemporaryFile(prefix='playback_', suffix=ext) as f:
    170             # The default permission is 0o600.
    171             os.chmod(f.name, 0o644)
    172 
    173             file_utils.download_file(video_url, f.name)
    174             if arc:
    175                 video_facade.prepare_arc_playback(f.name)
    176             else:
    177                 video_facade.prepare_playback(f.name)
    178 
    179         edid_path = os.path.join(
    180                 self.bindir, 'test_data/edids/HDMI_DELL_U2410.txt')
    181 
    182         video_port.plug()
    183         with video_port.use_edid_file(edid_path):
    184             audio_facade.set_chrome_active_node_type('HDMI', None)
    185             audio_facade.set_chrome_active_volume(100)
    186             audio_test_utils.check_audio_nodes(
    187                     audio_facade, (['HDMI'], None))
    188             display_facade.set_mirrored(True)
    189             video_port.start_monitoring_audio_video_capturing_delay()
    190 
    191             time.sleep(self.DELAY_BEFORE_CAPTURING)
    192             video_port.start_capturing_video((64, 64, 16, 16))
    193             audio_port.start_capturing_audio()
    194 
    195             time.sleep(self.DELAY_BEFORE_PLAYBACK)
    196             if arc:
    197                 video_facade.start_arc_playback(blocking_secs=20)
    198             else:
    199                 video_facade.start_playback(blocking=True)
    200             time.sleep(self.DELAY_AFTER_PLAYBACK)
    201 
    202             remote_path, _ = audio_port.stop_capturing_audio()
    203             video_port.stop_capturing_video()
    204             start_delay = video_port.get_audio_video_capturing_delay()
    205 
    206         local_path = os.path.join(self.resultsdir, 'recorded.raw')
    207         chameleon_board.host.get_file(remote_path, local_path)
    208 
    209         audio_data = open(local_path).read()
    210         video_data = video_port.get_captured_checksums()
    211 
    212         logging.info("audio capture %d bytes, %f seconds", len(audio_data),
    213                      len(audio_data) / float(self.AUDIO_CAPTURE_RATE) / 32)
    214         logging.info("video capture %d frames, %f seconds", len(video_data),
    215                      len(video_data) / float(self.VIDEO_CAPTURE_RATE))
    216 
    217         key_audio = self.compute_audio_keypoint(audio_data)
    218         key_video = self.compute_video_keypoint(video_data)
    219         # Use the capturing delay to align A/V
    220         key_video = map(lambda x: x + start_delay, key_video)
    221 
    222         dropped_frame_count = None
    223         if not arc:
    224             video_facade.dropped_frame_count()
    225 
    226         prefix = ''
    227         if arc:
    228             prefix = 'arc_'
    229         elif video_hardware_acceleration:
    230             prefix = 'hw_'
    231         else:
    232             prefix = 'sw_'
    233 
    234         self.log_result(prefix, key_audio, key_video, dropped_frame_count)
    235 
    236 
    237     def run_client_side_test(self):
    238         """Runs a client side test on Cros device in background."""
    239         self.client_at = autotest.Autotest(self.host)
    240         logging.info('Start running client side test %s',
    241                      arc_resource_common.PlayVideoProps.TEST_NAME)
    242         self.client_at.run_test(
    243                 arc_resource_common.PlayVideoProps.TEST_NAME,
    244                 background=True)
    245 
    246 
    247     def setup_video_app(self):
    248         """Setups Play Video app on Cros device.
    249 
    250         Runs a client side test on Cros device to start Chrome and ARC and
    251         install Play Video app.
    252         Wait for it to be ready.
    253 
    254         """
    255         # Removes ready tag that server side test should wait for later.
    256         self.remove_ready_tag()
    257 
    258         # Runs the client side test.
    259         self.run_client_side_test()
    260 
    261         logging.info('Waiting for client side Play Video app to be ready')
    262 
    263         # Waits for ready tag to be posted by client side test.
    264         utils.poll_for_condition(condition=self.ready_tag_exists,
    265                                  timeout=self.WAIT_CLIENT_READY_TIMEOUT_SECS,
    266                                  desc='Wait for client side test being ready',
    267                                  sleep_interval=1)
    268 
    269         logging.info('Client side Play Video app is ready')
    270 
    271 
    272     def cleanup(self):
    273         """Cleanup of the test."""
    274         self.touch_exit_tag()
    275         super(audiovideo_AVSync, self).cleanup()
    276 
    277 
    278     def remove_ready_tag(self):
    279         """Removes ready tag on Cros device."""
    280         if self.ready_tag_exists():
    281             self.host.run(command='rm %s' % (
    282                     arc_resource_common.PlayVideoProps.READY_TAG_FILE))
    283 
    284 
    285     def touch_exit_tag(self):
    286         """Touches exit tag on Cros device to stop client side test."""
    287         self.host.run(command='touch %s' % (
    288                 arc_resource_common.PlayVideoProps.EXIT_TAG_FILE))
    289 
    290 
    291     def ready_tag_exists(self):
    292         """Checks if ready tag exists.
    293 
    294         @returns: True if the tag file exists. False otherwise.
    295 
    296         """
    297         return self.host.path_exists(
    298                 arc_resource_common.PlayVideoProps.READY_TAG_FILE)
    299