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 logging
6import os
7import tempfile
8from PIL import Image
9
10from autotest_lib.client.bin import utils
11from autotest_lib.client.common_lib import error
12from autotest_lib.client.common_lib import file_utils
13from autotest_lib.client.cros.chameleon import chameleon_port_finder
14from autotest_lib.client.cros.chameleon import chameleon_stream_server
15from autotest_lib.client.cros.chameleon import edid
16from autotest_lib.server import test
17from autotest_lib.server.cros.multimedia import remote_facade_factory
18
19
20class video_PlaybackQuality(test.test):
21    """Server side video playback quality measurement.
22
23    This test measures the video playback quality by chameleon.
24    It will output 2 performance data. Number of Corrupted Frames and Number of
25    Dropped Frames.
26
27    """
28    version = 1
29
30    # treat 0~0x30 as 0
31    COLOR_MARGIN_0 = 0x30
32    # treat (0xFF-0x60)~0xFF as 0xFF.
33    COLOR_MARGIN_255 = 0xFF - 0x60
34
35    # If we can't find the expected frame after TIMEOUT_FRAMES, raise exception.
36    TIMEOUT_FRAMES = 120
37
38    # RGB for black. Used for preamble and postamble.
39    RGB_BLACK = [0, 0, 0]
40
41    # Expected color bar rgb. The color order in the array is the same order in
42    # the video frames.
43    EXPECTED_RGB = [('Blue', [0, 0, 255]), ('Green', [0, 255, 0]),
44                    ('Cyan', [0, 255, 255]), ('Red', [255, 0, 0]),
45                    ('Magenta', [255, 0, 255]), ('Yellow', [255, 255, 0]),
46                    ('White', [255, 255, 255])]
47
48    def _save_frame_to_file(self, resolution, frame, filename):
49        """Save video frame to file under results directory.
50
51        This function will append .png filename extension.
52
53        @param resolution: A tuple (width, height) of resolution.
54        @param frame: The video frame data.
55        @param filename: File name.
56
57        """
58        image = Image.fromstring('RGB', resolution, frame)
59        image.save('%s/%s.png' % (self.resultsdir, filename))
60
61    def _check_rgb_value(self, value, expected_value):
62        """Check value of the RGB.
63
64        This function will check if the value is in the range of expected value
65        and its margin.
66
67        @param value: The value for checking.
68        @param expected_value: Expected value. It's ether 0 or 0xFF.
69        @returns: True if the value is in range. False otherwise.
70
71        """
72        if expected_value <= value <= self.COLOR_MARGIN_0:
73            return True
74
75        if expected_value >= value >= self.COLOR_MARGIN_255:
76            return True
77
78        return False
79
80    def _check_rgb(self, frame, expected_rgb):
81        """Check the RGB raw data of all pixels in a video frame.
82
83        Because checking all pixels may take more than one video frame time. If
84        we want to analyze the video frame on the fly, we need to skip pixels
85        for saving the checking time.
86        The parameter of how many pixels to skip is self._skip_check_pixels.
87
88        @param frame: Array of all pixels of video frame.
89        @param expected_rgb: Expected values for RGB.
90        @returns: number of error pixels.
91
92        """
93        error_number = 0
94
95        for i in xrange(0, len(frame), 3 * (self._skip_check_pixels + 1)):
96            if not self._check_rgb_value(ord(frame[i]), expected_rgb[0]):
97                error_number += 1
98                continue
99
100            if not self._check_rgb_value(ord(frame[i + 1]), expected_rgb[1]):
101                error_number += 1
102                continue
103
104            if not self._check_rgb_value(ord(frame[i + 2]), expected_rgb[2]):
105                error_number += 1
106
107        return error_number
108
109    def _find_and_skip_preamble(self, description):
110        """Find and skip the preamble video frames.
111
112        @param description: Description of the log and file name.
113
114        """
115        # find preamble which is the first black frame.
116        number_of_frames = 0
117        while True:
118            video_frame = self._stream_server.receive_realtime_video_frame()
119            (frame_number, width, height, _, frame) = video_frame
120            if self._check_rgb(frame, self.RGB_BLACK) == 0:
121                logging.info('Find preamble at frame %d', frame_number)
122                break
123            if number_of_frames > self.TIMEOUT_FRAMES:
124                raise error.TestFail('%s found no preamble' % description)
125            number_of_frames += 1
126            self._save_frame_to_file((width, height), frame,
127                                     '%s_pre_%d' % (description, frame_number))
128        # skip preamble.
129        # After finding preamble, find the first frame that is not black.
130        number_of_frames = 0
131        while True:
132            video_frame = self._stream_server.receive_realtime_video_frame()
133            (frame_number, _, _, _, frame) = video_frame
134            if self._check_rgb(frame, self.RGB_BLACK) != 0:
135                logging.info('End preamble at frame %d', frame_number)
136                self._save_frame_to_file((width, height), frame,
137                                         '%s_end_preamble' % description)
138                break
139            if number_of_frames > self.TIMEOUT_FRAMES:
140                raise error.TestFail('%s found no color bar' % description)
141            number_of_frames += 1
142
143    def _store_wrong_frames(self, frame_number, resolution, frames):
144        """Store wrong frames for debugging.
145
146        @param frame_number: latest frame number.
147        @param resolution: A tuple (width, height) of resolution.
148        @param frames: Array of video frames. The latest video frame is in the
149                front.
150
151        """
152        for index, frame in enumerate(frames):
153            if not frame:
154                continue
155            element = ((frame_number - index), resolution, frame)
156            self._wrong_frames.append(element)
157
158    def _check_color_bars(self, description):
159        """Check color bars for video playback quality.
160
161        This function will read video frame from stream server and check if the
162        color is right by self._check_rgb until read postamble.
163        If only some pixels are wrong, the frame will be counted to corrupted
164        frame. If all pixels are wrong, the frame will be counted to wrong
165        frame.
166
167        @param description: Description of log and file name.
168        @return A tuple (corrupted_frame_count, wrong_frame_count) for quality
169                data.
170
171        """
172        # store the recent 2 video frames for debugging.
173        # Put the latest frame in the front.
174        frame_history = [None, None]
175        # Check index for color bars.
176        check_index = 0
177        corrupted_frame_count = 0
178        wrong_frame_count = 0
179        while True:
180            # Because the first color bar is skipped in _find_and_skip_preamble,
181            # we start from the 2nd color.
182            check_index = (check_index + 1) % len(self.EXPECTED_RGB)
183            video_frame = self._stream_server.receive_realtime_video_frame()
184            (frame_number, width, height, _, frame) = video_frame
185            # drop old video frame and store new one
186            frame_history.pop(-1)
187            frame_history.insert(0, frame)
188            color_name = self.EXPECTED_RGB[check_index][0]
189            expected_rgb = self.EXPECTED_RGB[check_index][1]
190            error_number = self._check_rgb(frame, expected_rgb)
191
192            # The video frame is correct, go to next video frame.
193            if not error_number:
194                continue
195
196            # Total pixels need to be adjusted by the _skip_check_pixels.
197            total_pixels = width * height / (self._skip_check_pixels + 1)
198            log_string = ('[%s] Number of error pixels %d on frame %d, '
199                          'expected color %s, RGB %r' %
200                          (description, error_number, frame_number, color_name,
201                           expected_rgb))
202
203            self._store_wrong_frames(frame_number, (width, height),
204                                     frame_history)
205            # clean history after they are stored.
206            frame_history = [None, None]
207
208            # Some pixels are wrong.
209            if error_number != total_pixels:
210                corrupted_frame_count += 1
211                logging.warn('[Corrupted]%s', log_string)
212                continue
213
214            # All pixels are wrong.
215            # Check if we get postamble where all pixels are black.
216            if self._check_rgb(frame, self.RGB_BLACK) == 0:
217                logging.info('Find postamble at frame %d', frame_number)
218                break
219
220            wrong_frame_count += 1
221            logging.info('[Wrong]%s', log_string)
222            # Adjust the check index due to frame drop.
223            # The screen should keep the old frame or go to next video frame
224            # due to frame drop.
225            # Check if color is the same as the previous frame.
226            # If it is not the same as previous frame, we assign the color of
227            # next frame without checking.
228            previous_index = ((check_index + len(self.EXPECTED_RGB) - 1)
229                              % len(self.EXPECTED_RGB))
230            if not self._check_rgb(frame, self.EXPECTED_RGB[previous_index][1]):
231                check_index = previous_index
232            else:
233                check_index = (check_index + 1) % len(self.EXPECTED_RGB)
234
235        return (corrupted_frame_count, wrong_frame_count)
236
237    def _dump_wrong_frames(self, description):
238        """Dump wrong frames to files.
239
240        @param description: Description of the file name.
241
242        """
243        for frame_number, resolution, frame in self._wrong_frames:
244            self._save_frame_to_file(resolution, frame,
245                                     '%s_%d' % (description, frame_number))
246        self._wrong_frames = []
247
248    def _prepare_playback(self):
249        """Prepare playback video."""
250        # Workaround for white bar on rightmost and bottommost on samus when we
251        # set fullscreen from fullscreen.
252        self._display_facade.set_fullscreen(False)
253        self._video_facade.prepare_playback(self._video_tempfile.name)
254
255    def _get_playback_quality(self, description, capture_dimension):
256        """Get the playback quality.
257
258        This function will playback the video and analysis each video frames.
259        It will output performance data too.
260
261        @param description: Description of the log, file name and performance
262                data.
263        @param capture_dimension: A tuple (width, height) of the captured video
264                frame.
265        """
266        logging.info('Start to get %s playback quality', description)
267        self._prepare_playback()
268        self._chameleon_port.start_capturing_video(capture_dimension)
269        self._stream_server.reset_video_session()
270        self._stream_server.dump_realtime_video_frame(
271            False, chameleon_stream_server.RealtimeMode.BestEffort)
272
273        self._video_facade.start_playback()
274        self._find_and_skip_preamble(description)
275
276        (corrupted_frame_count, wrong_frame_count) = (
277            self._check_color_bars(description))
278
279        self._stream_server.stop_dump_realtime_video_frame()
280        self._chameleon_port.stop_capturing_video()
281        self._video_facade.pause_playback()
282        self._dump_wrong_frames(description)
283
284        dropped_frame_count = self._video_facade.dropped_frame_count()
285
286        graph_name = '%s_%s' % (self._video_description, description)
287        self.output_perf_value(description='Corrupted frames',
288                               value=corrupted_frame_count, units='frame',
289                               higher_is_better=False, graph=graph_name)
290        self.output_perf_value(description='Wrong frames',
291                               value=wrong_frame_count, units='frame',
292                               higher_is_better=False, graph=graph_name)
293        self.output_perf_value(description='Dropped frames',
294                               value=dropped_frame_count, units='frame',
295                               higher_is_better=False, graph=graph_name)
296
297    def run_once(self, host, video_url, video_description, test_regions,
298                 skip_check_pixels=5):
299        """Runs video playback quality measurement.
300
301        @param host: A host object representing the DUT.
302        @param video_url: The ULR of the test video.
303        @param video_description: a string describes the video to play which
304                will be part of entry name in dashboard.
305        @param test_regions: An array of tuples (description, capture_dimension)
306                for the testing region of video. capture_dimension is a tuple
307                (width, height).
308        @param skip_check_pixels: We will check one pixel and skip number of
309                pixels. 0 means no skip. 1 means check 1 pixel and skip 1 pixel.
310                Because we may take more than 1 video frame time for checking
311                all pixels. Skip some pixles for saving time.
312
313        """
314        # Store wrong video frames for dumping and debugging.
315        self._video_url = video_url
316        self._video_description = video_description
317        self._wrong_frames = []
318        self._skip_check_pixels = skip_check_pixels
319
320        factory = remote_facade_factory.RemoteFacadeFactory(
321                host, results_dir=self.resultsdir, no_chrome=True)
322        chameleon_board = host.chameleon
323        browser_facade = factory.create_browser_facade()
324        display_facade = factory.create_display_facade()
325        self._display_facade = display_facade
326        self._video_facade = factory.create_video_facade()
327        self._stream_server = chameleon_stream_server.ChameleonStreamServer(
328            chameleon_board.host.hostname)
329
330        chameleon_board.setup_and_reset(self.outputdir)
331        self._stream_server.connect()
332
333        # Download the video to self._video_tempfile.name
334        _, ext = os.path.splitext(video_url)
335        self._video_tempfile = tempfile.NamedTemporaryFile(suffix=ext)
336        # The default permission is 0o600.
337        os.chmod(self._video_tempfile.name, 0o644)
338        file_utils.download_file(video_url, self._video_tempfile.name)
339
340        browser_facade.start_default_chrome()
341        display_facade.set_mirrored(False)
342
343        edid_path = os.path.join(self.bindir, 'test_data', 'edids',
344                                 'EDIDv2_1920x1080')
345        finder = chameleon_port_finder.ChameleonVideoInputFinder(
346                chameleon_board, display_facade)
347        for chameleon_port in finder.iterate_all_ports():
348            self._chameleon_port = chameleon_port
349
350            connector_type = chameleon_port.get_connector_type()
351            logging.info('See the display on Chameleon: port %d (%s)',
352                         chameleon_port.get_connector_id(),
353                         connector_type)
354
355            with chameleon_port.use_edid(
356                    edid.Edid.from_file(edid_path, skip_verify=True)):
357                resolution = utils.wait_for_value_changed(
358                    display_facade.get_external_resolution,
359                    old_value=None)
360                if resolution is None:
361                    raise error.TestFail('No external display detected on DUT')
362
363            display_facade.move_to_display(
364                display_facade.get_first_external_display_id())
365
366            for description, capture_dimension in test_regions:
367                self._get_playback_quality('%s_%s' % (connector_type,
368                                                      description),
369                                           capture_dimension)
370