# Copyright 2016 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import json import logging import os import struct import tempfile import time from autotest_lib.client.bin import utils from autotest_lib.client.common_lib import file_utils from autotest_lib.client.common_lib.cros import arc_common from autotest_lib.client.cros import constants from autotest_lib.client.cros.chameleon import audio_test_utils from autotest_lib.client.cros.chameleon import chameleon_port_finder from autotest_lib.client.cros.multimedia import arc_resource_common from autotest_lib.server import autotest from autotest_lib.server import test from autotest_lib.server.cros.multimedia import remote_facade_factory class audiovideo_AVSync(test.test): """ Server side HDMI audio/video sync quality measurement This test talks to a Chameleon board and a Cros device to measure the audio/video sync quality under playing a 1080p 60fps video. """ version = 1 AUDIO_CAPTURE_RATE = 48000 VIDEO_CAPTURE_RATE = 60 BEEP_THRESHOLD = 10 ** 9 DELAY_BEFORE_CAPTURING = 2 DELAY_BEFORE_PLAYBACK = 2 DELAY_AFTER_PLAYBACK = 2 DEFAULT_VIDEO_URL = ('http://commondatastorage.googleapis.com/' 'chromiumos-test-assets-public/chameleon/' 'audiovideo_AVSync/1080p_60fps.mp4') WAIT_CLIENT_READY_TIMEOUT_SECS = 120 def compute_audio_keypoint(self, data): """Compute audio keypoints. Audio keypoints are the starting times of beeps. @param data: Raw captured audio data in S32LE, 8 channels, 48000 Hz. @returns: Key points of captured data put in a list. """ keypoints = [] sample_no = 0 last_beep_no = -100 for i in xrange(0, len(data), 32): values = struct.unpack('<8i', data[i:i+32]) if values[0] > self.BEEP_THRESHOLD: if sample_no - last_beep_no >= 100: keypoints.append(sample_no / float(self.AUDIO_CAPTURE_RATE)) last_beep_no = sample_no sample_no += 1 return keypoints def compute_video_keypoint(self, checksum): """Compute video keypoints. Video keypoints are the times when the checksum changes. @param checksum: Checksums of frames put in a list. @returns: Key points of captured video data put in a list. """ return [i / float(self.VIDEO_CAPTURE_RATE) for i in xrange(1, len(checksum)) if checksum[i] != checksum[i - 1]] def log_result(self, prefix, key_audio, key_video, dropped_frame_count): """Log the test result to result.json and the dashboard. @param prefix: A string distinguishes between subtests. @param key_audio: Key points of captured audio data put in a list. @param key_video: Key points of captured video data put in a list. @param dropped_frame_count: Number of dropped frames. """ log_path = os.path.join(self.resultsdir, 'result.json') diff = map(lambda x: x[0] - x[1], zip(key_audio, key_video)) diff_range = max(diff) - min(diff) result = dict( key_audio=key_audio, key_video=key_video, av_diff=diff, diff_range=diff_range ) if dropped_frame_count is not None: result['dropped_frame_count'] = dropped_frame_count result = json.dumps(result, indent=2) with open(log_path, 'w') as f: f.write(result) logging.info(str(result)) dashboard_result = dict( diff_range=[diff_range, 'seconds'], max_diff=[max(diff), 'seconds'], min_diff=[min(diff), 'seconds'], average_diff=[sum(diff) / len(diff), 'seconds'] ) if dropped_frame_count is not None: dashboard_result['dropped_frame_count'] = [ dropped_frame_count, 'frames'] for key, value in dashboard_result.iteritems(): self.output_perf_value(description=prefix+key, value=value[0], units=value[1], higher_is_better=False) def run_once(self, host, video_hardware_acceleration=True, video_url=DEFAULT_VIDEO_URL, arc=False): """Running audio/video synchronization quality measurement @param host: A host object representing the DUT. @param video_hardware_acceleration: Enables the hardware acceleration for video decoding. @param video_url: The ULR of the test video. @param arc: Tests on ARC with an Android Video Player App. """ self.host = host factory = remote_facade_factory.RemoteFacadeFactory( host, results_dir=self.resultsdir, no_chrome=True) chrome_args = { 'extension_paths': [constants.MULTIMEDIA_TEST_EXTENSION], 'extra_browser_args': [], 'arc_mode': arc_common.ARC_MODE_DISABLED, 'autotest_ext': True } if not video_hardware_acceleration: chrome_args['extra_browser_args'].append( '--disable-accelerated-video-decode') if arc: chrome_args['arc_mode'] = arc_common.ARC_MODE_ENABLED browser_facade = factory.create_browser_facade() browser_facade.start_custom_chrome(chrome_args) logging.info("created chrome") if arc: self.setup_video_app() chameleon_board = host.chameleon audio_facade = factory.create_audio_facade() display_facade = factory.create_display_facade() video_facade = factory.create_video_facade() audio_port_finder = chameleon_port_finder.ChameleonAudioInputFinder( chameleon_board) video_port_finder = chameleon_port_finder.ChameleonVideoInputFinder( chameleon_board, display_facade) audio_port = audio_port_finder.find_port('HDMI') video_port = video_port_finder.find_port('HDMI') chameleon_board.setup_and_reset(self.outputdir) _, ext = os.path.splitext(video_url) with tempfile.NamedTemporaryFile(prefix='playback_', suffix=ext) as f: # The default permission is 0o600. os.chmod(f.name, 0o644) file_utils.download_file(video_url, f.name) if arc: video_facade.prepare_arc_playback(f.name) else: video_facade.prepare_playback(f.name) edid_path = os.path.join( self.bindir, 'test_data/edids/HDMI_DELL_U2410.txt') video_port.plug() with video_port.use_edid_file(edid_path): audio_facade.set_chrome_active_node_type('HDMI', None) audio_facade.set_chrome_active_volume(100) audio_test_utils.check_audio_nodes( audio_facade, (['HDMI'], None)) display_facade.set_mirrored(True) video_port.start_monitoring_audio_video_capturing_delay() time.sleep(self.DELAY_BEFORE_CAPTURING) video_port.start_capturing_video((64, 64, 16, 16)) audio_port.start_capturing_audio() time.sleep(self.DELAY_BEFORE_PLAYBACK) if arc: video_facade.start_arc_playback(blocking_secs=20) else: video_facade.start_playback(blocking=True) time.sleep(self.DELAY_AFTER_PLAYBACK) remote_path, _ = audio_port.stop_capturing_audio() video_port.stop_capturing_video() start_delay = video_port.get_audio_video_capturing_delay() local_path = os.path.join(self.resultsdir, 'recorded.raw') chameleon_board.host.get_file(remote_path, local_path) audio_data = open(local_path).read() video_data = video_port.get_captured_checksums() logging.info("audio capture %d bytes, %f seconds", len(audio_data), len(audio_data) / float(self.AUDIO_CAPTURE_RATE) / 32) logging.info("video capture %d frames, %f seconds", len(video_data), len(video_data) / float(self.VIDEO_CAPTURE_RATE)) key_audio = self.compute_audio_keypoint(audio_data) key_video = self.compute_video_keypoint(video_data) # Use the capturing delay to align A/V key_video = map(lambda x: x + start_delay, key_video) dropped_frame_count = None if not arc: video_facade.dropped_frame_count() prefix = '' if arc: prefix = 'arc_' elif video_hardware_acceleration: prefix = 'hw_' else: prefix = 'sw_' self.log_result(prefix, key_audio, key_video, dropped_frame_count) def run_client_side_test(self): """Runs a client side test on Cros device in background.""" self.client_at = autotest.Autotest(self.host) logging.info('Start running client side test %s', arc_resource_common.PlayVideoProps.TEST_NAME) self.client_at.run_test( arc_resource_common.PlayVideoProps.TEST_NAME, background=True) def setup_video_app(self): """Setups Play Video app on Cros device. Runs a client side test on Cros device to start Chrome and ARC and install Play Video app. Wait for it to be ready. """ # Removes ready tag that server side test should wait for later. self.remove_ready_tag() # Runs the client side test. self.run_client_side_test() logging.info('Waiting for client side Play Video app to be ready') # Waits for ready tag to be posted by client side test. utils.poll_for_condition(condition=self.ready_tag_exists, timeout=self.WAIT_CLIENT_READY_TIMEOUT_SECS, desc='Wait for client side test being ready', sleep_interval=1) logging.info('Client side Play Video app is ready') def cleanup(self): """Cleanup of the test.""" self.touch_exit_tag() super(audiovideo_AVSync, self).cleanup() def remove_ready_tag(self): """Removes ready tag on Cros device.""" if self.ready_tag_exists(): self.host.run(command='rm %s' % ( arc_resource_common.PlayVideoProps.READY_TAG_FILE)) def touch_exit_tag(self): """Touches exit tag on Cros device to stop client side test.""" self.host.run(command='touch %s' % ( arc_resource_common.PlayVideoProps.EXIT_TAG_FILE)) def ready_tag_exists(self): """Checks if ready tag exists. @returns: True if the tag file exists. False otherwise. """ return self.host.path_exists( arc_resource_common.PlayVideoProps.READY_TAG_FILE)