299 lines
11 KiB
Python
299 lines
11 KiB
Python
# 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.AUDIO_TEST_EXTENSION,
|
|
constants.DISPLAY_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)
|