370 lines
15 KiB
Python
370 lines
15 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 logging
|
|
import os
|
|
import tempfile
|
|
from PIL import Image
|
|
|
|
from autotest_lib.client.bin import utils
|
|
from autotest_lib.client.common_lib import error
|
|
from autotest_lib.client.common_lib import file_utils
|
|
from autotest_lib.client.cros.chameleon import chameleon_port_finder
|
|
from autotest_lib.client.cros.chameleon import chameleon_stream_server
|
|
from autotest_lib.client.cros.chameleon import edid
|
|
from autotest_lib.server import test
|
|
from autotest_lib.server.cros.multimedia import remote_facade_factory
|
|
|
|
|
|
class video_PlaybackQuality(test.test):
|
|
"""Server side video playback quality measurement.
|
|
|
|
This test measures the video playback quality by chameleon.
|
|
It will output 2 performance data. Number of Corrupted Frames and Number of
|
|
Dropped Frames.
|
|
|
|
"""
|
|
version = 1
|
|
|
|
# treat 0~0x30 as 0
|
|
COLOR_MARGIN_0 = 0x30
|
|
# treat (0xFF-0x60)~0xFF as 0xFF.
|
|
COLOR_MARGIN_255 = 0xFF - 0x60
|
|
|
|
# If we can't find the expected frame after TIMEOUT_FRAMES, raise exception.
|
|
TIMEOUT_FRAMES = 120
|
|
|
|
# RGB for black. Used for preamble and postamble.
|
|
RGB_BLACK = [0, 0, 0]
|
|
|
|
# Expected color bar rgb. The color order in the array is the same order in
|
|
# the video frames.
|
|
EXPECTED_RGB = [('Blue', [0, 0, 255]), ('Green', [0, 255, 0]),
|
|
('Cyan', [0, 255, 255]), ('Red', [255, 0, 0]),
|
|
('Magenta', [255, 0, 255]), ('Yellow', [255, 255, 0]),
|
|
('White', [255, 255, 255])]
|
|
|
|
def _save_frame_to_file(self, resolution, frame, filename):
|
|
"""Save video frame to file under results directory.
|
|
|
|
This function will append .png filename extension.
|
|
|
|
@param resolution: A tuple (width, height) of resolution.
|
|
@param frame: The video frame data.
|
|
@param filename: File name.
|
|
|
|
"""
|
|
image = Image.fromstring('RGB', resolution, frame)
|
|
image.save('%s/%s.png' % (self.resultsdir, filename))
|
|
|
|
def _check_rgb_value(self, value, expected_value):
|
|
"""Check value of the RGB.
|
|
|
|
This function will check if the value is in the range of expected value
|
|
and its margin.
|
|
|
|
@param value: The value for checking.
|
|
@param expected_value: Expected value. It's ether 0 or 0xFF.
|
|
@returns: True if the value is in range. False otherwise.
|
|
|
|
"""
|
|
if expected_value <= value <= self.COLOR_MARGIN_0:
|
|
return True
|
|
|
|
if expected_value >= value >= self.COLOR_MARGIN_255:
|
|
return True
|
|
|
|
return False
|
|
|
|
def _check_rgb(self, frame, expected_rgb):
|
|
"""Check the RGB raw data of all pixels in a video frame.
|
|
|
|
Because checking all pixels may take more than one video frame time. If
|
|
we want to analyze the video frame on the fly, we need to skip pixels
|
|
for saving the checking time.
|
|
The parameter of how many pixels to skip is self._skip_check_pixels.
|
|
|
|
@param frame: Array of all pixels of video frame.
|
|
@param expected_rgb: Expected values for RGB.
|
|
@returns: number of error pixels.
|
|
|
|
"""
|
|
error_number = 0
|
|
|
|
for i in xrange(0, len(frame), 3 * (self._skip_check_pixels + 1)):
|
|
if not self._check_rgb_value(ord(frame[i]), expected_rgb[0]):
|
|
error_number += 1
|
|
continue
|
|
|
|
if not self._check_rgb_value(ord(frame[i + 1]), expected_rgb[1]):
|
|
error_number += 1
|
|
continue
|
|
|
|
if not self._check_rgb_value(ord(frame[i + 2]), expected_rgb[2]):
|
|
error_number += 1
|
|
|
|
return error_number
|
|
|
|
def _find_and_skip_preamble(self, description):
|
|
"""Find and skip the preamble video frames.
|
|
|
|
@param description: Description of the log and file name.
|
|
|
|
"""
|
|
# find preamble which is the first black frame.
|
|
number_of_frames = 0
|
|
while True:
|
|
video_frame = self._stream_server.receive_realtime_video_frame()
|
|
(frame_number, width, height, _, frame) = video_frame
|
|
if self._check_rgb(frame, self.RGB_BLACK) == 0:
|
|
logging.info('Find preamble at frame %d', frame_number)
|
|
break
|
|
if number_of_frames > self.TIMEOUT_FRAMES:
|
|
raise error.TestFail('%s found no preamble' % description)
|
|
number_of_frames += 1
|
|
self._save_frame_to_file((width, height), frame,
|
|
'%s_pre_%d' % (description, frame_number))
|
|
# skip preamble.
|
|
# After finding preamble, find the first frame that is not black.
|
|
number_of_frames = 0
|
|
while True:
|
|
video_frame = self._stream_server.receive_realtime_video_frame()
|
|
(frame_number, _, _, _, frame) = video_frame
|
|
if self._check_rgb(frame, self.RGB_BLACK) != 0:
|
|
logging.info('End preamble at frame %d', frame_number)
|
|
self._save_frame_to_file((width, height), frame,
|
|
'%s_end_preamble' % description)
|
|
break
|
|
if number_of_frames > self.TIMEOUT_FRAMES:
|
|
raise error.TestFail('%s found no color bar' % description)
|
|
number_of_frames += 1
|
|
|
|
def _store_wrong_frames(self, frame_number, resolution, frames):
|
|
"""Store wrong frames for debugging.
|
|
|
|
@param frame_number: latest frame number.
|
|
@param resolution: A tuple (width, height) of resolution.
|
|
@param frames: Array of video frames. The latest video frame is in the
|
|
front.
|
|
|
|
"""
|
|
for index, frame in enumerate(frames):
|
|
if not frame:
|
|
continue
|
|
element = ((frame_number - index), resolution, frame)
|
|
self._wrong_frames.append(element)
|
|
|
|
def _check_color_bars(self, description):
|
|
"""Check color bars for video playback quality.
|
|
|
|
This function will read video frame from stream server and check if the
|
|
color is right by self._check_rgb until read postamble.
|
|
If only some pixels are wrong, the frame will be counted to corrupted
|
|
frame. If all pixels are wrong, the frame will be counted to wrong
|
|
frame.
|
|
|
|
@param description: Description of log and file name.
|
|
@return A tuple (corrupted_frame_count, wrong_frame_count) for quality
|
|
data.
|
|
|
|
"""
|
|
# store the recent 2 video frames for debugging.
|
|
# Put the latest frame in the front.
|
|
frame_history = [None, None]
|
|
# Check index for color bars.
|
|
check_index = 0
|
|
corrupted_frame_count = 0
|
|
wrong_frame_count = 0
|
|
while True:
|
|
# Because the first color bar is skipped in _find_and_skip_preamble,
|
|
# we start from the 2nd color.
|
|
check_index = (check_index + 1) % len(self.EXPECTED_RGB)
|
|
video_frame = self._stream_server.receive_realtime_video_frame()
|
|
(frame_number, width, height, _, frame) = video_frame
|
|
# drop old video frame and store new one
|
|
frame_history.pop(-1)
|
|
frame_history.insert(0, frame)
|
|
color_name = self.EXPECTED_RGB[check_index][0]
|
|
expected_rgb = self.EXPECTED_RGB[check_index][1]
|
|
error_number = self._check_rgb(frame, expected_rgb)
|
|
|
|
# The video frame is correct, go to next video frame.
|
|
if not error_number:
|
|
continue
|
|
|
|
# Total pixels need to be adjusted by the _skip_check_pixels.
|
|
total_pixels = width * height / (self._skip_check_pixels + 1)
|
|
log_string = ('[%s] Number of error pixels %d on frame %d, '
|
|
'expected color %s, RGB %r' %
|
|
(description, error_number, frame_number, color_name,
|
|
expected_rgb))
|
|
|
|
self._store_wrong_frames(frame_number, (width, height),
|
|
frame_history)
|
|
# clean history after they are stored.
|
|
frame_history = [None, None]
|
|
|
|
# Some pixels are wrong.
|
|
if error_number != total_pixels:
|
|
corrupted_frame_count += 1
|
|
logging.warn('[Corrupted]%s', log_string)
|
|
continue
|
|
|
|
# All pixels are wrong.
|
|
# Check if we get postamble where all pixels are black.
|
|
if self._check_rgb(frame, self.RGB_BLACK) == 0:
|
|
logging.info('Find postamble at frame %d', frame_number)
|
|
break
|
|
|
|
wrong_frame_count += 1
|
|
logging.info('[Wrong]%s', log_string)
|
|
# Adjust the check index due to frame drop.
|
|
# The screen should keep the old frame or go to next video frame
|
|
# due to frame drop.
|
|
# Check if color is the same as the previous frame.
|
|
# If it is not the same as previous frame, we assign the color of
|
|
# next frame without checking.
|
|
previous_index = ((check_index + len(self.EXPECTED_RGB) - 1)
|
|
% len(self.EXPECTED_RGB))
|
|
if not self._check_rgb(frame, self.EXPECTED_RGB[previous_index][1]):
|
|
check_index = previous_index
|
|
else:
|
|
check_index = (check_index + 1) % len(self.EXPECTED_RGB)
|
|
|
|
return (corrupted_frame_count, wrong_frame_count)
|
|
|
|
def _dump_wrong_frames(self, description):
|
|
"""Dump wrong frames to files.
|
|
|
|
@param description: Description of the file name.
|
|
|
|
"""
|
|
for frame_number, resolution, frame in self._wrong_frames:
|
|
self._save_frame_to_file(resolution, frame,
|
|
'%s_%d' % (description, frame_number))
|
|
self._wrong_frames = []
|
|
|
|
def _prepare_playback(self):
|
|
"""Prepare playback video."""
|
|
# Workaround for white bar on rightmost and bottommost on samus when we
|
|
# set fullscreen from fullscreen.
|
|
self._display_facade.set_fullscreen(False)
|
|
self._video_facade.prepare_playback(self._video_tempfile.name)
|
|
|
|
def _get_playback_quality(self, description, capture_dimension):
|
|
"""Get the playback quality.
|
|
|
|
This function will playback the video and analysis each video frames.
|
|
It will output performance data too.
|
|
|
|
@param description: Description of the log, file name and performance
|
|
data.
|
|
@param capture_dimension: A tuple (width, height) of the captured video
|
|
frame.
|
|
"""
|
|
logging.info('Start to get %s playback quality', description)
|
|
self._prepare_playback()
|
|
self._chameleon_port.start_capturing_video(capture_dimension)
|
|
self._stream_server.reset_video_session()
|
|
self._stream_server.dump_realtime_video_frame(
|
|
False, chameleon_stream_server.RealtimeMode.BestEffort)
|
|
|
|
self._video_facade.start_playback()
|
|
self._find_and_skip_preamble(description)
|
|
|
|
(corrupted_frame_count, wrong_frame_count) = (
|
|
self._check_color_bars(description))
|
|
|
|
self._stream_server.stop_dump_realtime_video_frame()
|
|
self._chameleon_port.stop_capturing_video()
|
|
self._video_facade.pause_playback()
|
|
self._dump_wrong_frames(description)
|
|
|
|
dropped_frame_count = self._video_facade.dropped_frame_count()
|
|
|
|
graph_name = '%s_%s' % (self._video_description, description)
|
|
self.output_perf_value(description='Corrupted frames',
|
|
value=corrupted_frame_count, units='frame',
|
|
higher_is_better=False, graph=graph_name)
|
|
self.output_perf_value(description='Wrong frames',
|
|
value=wrong_frame_count, units='frame',
|
|
higher_is_better=False, graph=graph_name)
|
|
self.output_perf_value(description='Dropped frames',
|
|
value=dropped_frame_count, units='frame',
|
|
higher_is_better=False, graph=graph_name)
|
|
|
|
def run_once(self, host, video_url, video_description, test_regions,
|
|
skip_check_pixels=5):
|
|
"""Runs video playback quality measurement.
|
|
|
|
@param host: A host object representing the DUT.
|
|
@param video_url: The ULR of the test video.
|
|
@param video_description: a string describes the video to play which
|
|
will be part of entry name in dashboard.
|
|
@param test_regions: An array of tuples (description, capture_dimension)
|
|
for the testing region of video. capture_dimension is a tuple
|
|
(width, height).
|
|
@param skip_check_pixels: We will check one pixel and skip number of
|
|
pixels. 0 means no skip. 1 means check 1 pixel and skip 1 pixel.
|
|
Because we may take more than 1 video frame time for checking
|
|
all pixels. Skip some pixles for saving time.
|
|
|
|
"""
|
|
# Store wrong video frames for dumping and debugging.
|
|
self._video_url = video_url
|
|
self._video_description = video_description
|
|
self._wrong_frames = []
|
|
self._skip_check_pixels = skip_check_pixels
|
|
|
|
factory = remote_facade_factory.RemoteFacadeFactory(
|
|
host, results_dir=self.resultsdir, no_chrome=True)
|
|
chameleon_board = host.chameleon
|
|
browser_facade = factory.create_browser_facade()
|
|
display_facade = factory.create_display_facade()
|
|
self._display_facade = display_facade
|
|
self._video_facade = factory.create_video_facade()
|
|
self._stream_server = chameleon_stream_server.ChameleonStreamServer(
|
|
chameleon_board.host.hostname)
|
|
|
|
chameleon_board.setup_and_reset(self.outputdir)
|
|
self._stream_server.connect()
|
|
|
|
# Download the video to self._video_tempfile.name
|
|
_, ext = os.path.splitext(video_url)
|
|
self._video_tempfile = tempfile.NamedTemporaryFile(suffix=ext)
|
|
# The default permission is 0o600.
|
|
os.chmod(self._video_tempfile.name, 0o644)
|
|
file_utils.download_file(video_url, self._video_tempfile.name)
|
|
|
|
browser_facade.start_default_chrome()
|
|
display_facade.set_mirrored(False)
|
|
|
|
edid_path = os.path.join(self.bindir, 'test_data', 'edids',
|
|
'EDIDv2_1920x1080')
|
|
finder = chameleon_port_finder.ChameleonVideoInputFinder(
|
|
chameleon_board, display_facade)
|
|
for chameleon_port in finder.iterate_all_ports():
|
|
self._chameleon_port = chameleon_port
|
|
|
|
connector_type = chameleon_port.get_connector_type()
|
|
logging.info('See the display on Chameleon: port %d (%s)',
|
|
chameleon_port.get_connector_id(),
|
|
connector_type)
|
|
|
|
with chameleon_port.use_edid(
|
|
edid.Edid.from_file(edid_path, skip_verify=True)):
|
|
resolution = utils.wait_for_value_changed(
|
|
display_facade.get_external_resolution,
|
|
old_value=None)
|
|
if resolution is None:
|
|
raise error.TestFail('No external display detected on DUT')
|
|
|
|
display_facade.move_to_display(
|
|
display_facade.get_first_external_display_id())
|
|
|
|
for description, capture_dimension in test_regions:
|
|
self._get_playback_quality('%s_%s' % (connector_type,
|
|
description),
|
|
capture_dimension)
|