• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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