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 5import json 6import logging 7import os 8import struct 9import tempfile 10import time 11 12from autotest_lib.client.bin import utils 13from autotest_lib.client.common_lib import file_utils 14from autotest_lib.client.common_lib.cros import arc_common 15from autotest_lib.client.cros import constants 16from autotest_lib.client.cros.chameleon import audio_test_utils 17from autotest_lib.client.cros.chameleon import chameleon_port_finder 18from autotest_lib.client.cros.multimedia import arc_resource_common 19from autotest_lib.server import autotest 20from autotest_lib.server import test 21from autotest_lib.server.cros.multimedia import remote_facade_factory 22 23 24class 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