# Copyright 2020 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 collections import enum import json import os import logging import time from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib.cros import chrome from autotest_lib.client.common_lib.cros import power_load_util from autotest_lib.client.cros.input_playback import keyboard from autotest_lib.client.cros.power import power_dashboard from autotest_lib.client.cros.power import power_status from autotest_lib.client.cros.power import power_test class power_MeetClient(power_test.power_Test): """class for power_MeetClient test. This test should be call from power_MeetCall server test only. """ version = 1 video_url = 'http://meet.google.com' doc_url = 'http://doc.new' def initialize(self, seconds_period=5., pdash_note='', force_discharge=False): """initialize method.""" super(power_MeetClient, self).initialize( seconds_period=seconds_period, pdash_note=pdash_note, force_discharge=force_discharge) def run_once(self, meet_code, duration=180, layout='Tiled', username=None, password=None): """run_once method. @param meet_code: Meet code generated in power_MeetCall. @param duration: duration in seconds. @param layout: string of meet layout to use. @param username: Google account to use. @param password: password for Google account. """ if not username and not password: username = power_load_util.get_meet_username() password = power_load_util.get_meet_password() if not username or not password: raise error.TestFail('Need to supply both username and password.') extra_browser_args = self.get_extra_browser_args_for_camera_test() with keyboard.Keyboard() as keys,\ chrome.Chrome(init_network_controller=True, gaia_login=True, username=username, password=password, extra_browser_args=extra_browser_args, autotest_ext=True) as cr: # Move existing window to left half and open video page tab = cr.browser.tabs[0] tab.Activate() # Run in full-screen. fullscreen = tab.EvaluateJavaScript('document.webkitIsFullScreen') if not fullscreen: keys.press_key('f4') url = self.video_url + '/' + meet_code logging.info('Navigating left window to %s', url) tab.Navigate(url) # Workaround when camera isn't init for some unknown reason. time.sleep(10) tab.EvaluateJavaScript('location.reload()') tab.WaitForDocumentReadyStateToBeComplete() logging.info(meet_code) self.keyvals['meet_code'] = meet_code def wait_until(cond, error_msg): """Helper for javascript polling wait.""" for _ in range(60): time.sleep(1) if tab.EvaluateJavaScript(cond): return raise error.TestFail(error_msg) wait_until('window.hasOwnProperty("hrTelemetryApi")', 'Meet API does not existed.') wait_until('hrTelemetryApi.isInMeeting()', 'Can not join meeting.') wait_until('hrTelemetryApi.getParticipantCount() > 1', 'Meeting has no other participant.') # Make sure camera and mic are on. tab.EvaluateJavaScript('hrTelemetryApi.setCameraMuted(false)') tab.EvaluateJavaScript('hrTelemetryApi.setMicMuted(false)') if layout == 'Tiled': tab.EvaluateJavaScript('hrTelemetryApi.setTiledLayout()') elif layout == 'Auto': tab.EvaluateJavaScript('hrTelemetryApi.setAutoLayout()') elif layout == 'Sidebar': tab.EvaluateJavaScript('hrTelemetryApi.setSidebarLayout()') elif layout == 'Spotlight': tab.EvaluateJavaScript('hrTelemetryApi.setSpotlightLayout()') else: raise error.TestError('Unknown layout %s' % layout) self.keyvals['layout'] = layout self.start_measurements() time.sleep(duration) end_time = self._start_time + duration # Collect stat if not tab.EvaluateJavaScript('window.hasOwnProperty("realtime")'): logging.info('Account %s is not in allowlist for MediaInfoAPI', username) return meet_data = tab.EvaluateJavaScript( 'realtime.media.getMediaInfoDataPoints()') power_dashboard.get_dashboard_factory().registerDataType( MeetStatLogger, MeetStatDashboard) self._meas_logs.append( MeetStatLogger(self._start_time, end_time, meet_data)) class MeetStatLogger(power_status.MeasurementLogger): """Class for logging meet data point to power dashboard. Format of meet_data http://google3/logs/proto/buzz/callstats.proto """ def __init__(self, start_ts, end_ts, meet_data): # Do not call parent constructor to avoid making a new thread. self.times = [start_ts] # Meet epoch timestamp uses millisec unit. self.meet_data = [data_point for data_point in meet_data if start_ts * 1000 <= data_point['timestamp'] <= end_ts * 1000] def calc(self, mtype=None): return {} def save_results(self, resultsdir, fname_prefix=None): # Save raw dict from meet to file. Ignore fname_prefix. with open(os.path.join(resultsdir, 'meet_powerlog.json'), 'w') as f: json.dump(self.meet_data , f, indent=4, separators=(',', ': '), ensure_ascii=False) class MeetStatDashboard(power_dashboard.MeasurementLoggerDashboard): """Dashboard class for MeetStatLogger class.""" # Direction and type numbers map to constants in the proto class Direction(enum.IntEnum): """Possible directions for media entries of a data point.""" SENDER = 0 RECEIVER = 1 class MediaType(enum.IntEnum): """Possible media types for media entries of a data point.""" VIDEO = 2 # Important metrics to collect. MEET_KEYS = [ 'encodeUsagePercent', 'fps', 'height', 'width', ] def _get_ssrc_dict(self, meet_data): """ Extract http://what/ssrc for all video stream and map to string. The format of the string would be sender_# / receiver_# where # denotes index for the video counting from 0. Returns: dict from ssrc to video stream string. """ ret = {} count = [0, 0] # We only care about video streams. for media in meet_data[-1]['media']: if media['mediatype'] != self.MediaType.VIDEO: continue if (media['direction'] != self.Direction.SENDER and media['direction'] != self.Direction.RECEIVER): continue name = [media['directionStr'], str(count[media['direction']])] if media['direction'] == self.Direction.SENDER: name.append(media['sendercodecname']) else: name.append(media['receiverCodecName']) count[media['direction']] += 1 ret[media['ssrc']] = '_'.join(name) return ret def _get_meet_unit(self, key): """Return unit from name of the key.""" if key.endswith('fps'): return 'fps' if key.endswith('Percent'): return 'percent' if key.endswith('width') or key.endswith('height') : return 'point' raise error.TestError('Unexpected key: %s' % key) def _get_meet_type(self, key): """Return type from name of the key.""" if key.endswith('fps'): return 'meet_fps' if key.endswith('Percent'): return 'meet_encoder_load' if key.endswith('width'): return 'meet_width' if key.endswith('height'): return 'meet_height' raise error.TestError('Unexpected key: %s' % key) def _convert(self): """Convert meet raw dict to data to power dict.""" meet_data = self._logger.meet_data ssrc_dict = self._get_ssrc_dict(meet_data) # Dict from timestamp to dict of meet_key to value parse_dict = collections.defaultdict( lambda: collections.defaultdict(int)) key_set = set() testname='power_MeetCall' for data_point in meet_data: timestamp = data_point['timestamp'] for media in data_point['media']: ssrc = media.get('ssrc', 0) if ssrc not in ssrc_dict: continue name = ssrc_dict[media['ssrc']] for meet_key in self.MEET_KEYS: if meet_key not in media: continue key = '%s_%s' % (name, meet_key) key_set.add(key) parse_dict[timestamp][key] = media[meet_key] timestamps = sorted(parse_dict.keys()) sample_count = len(timestamps) powerlog_data = collections.defaultdict(list) for ts in sorted(parse_dict.keys()): for key in key_set: powerlog_data[key].append(parse_dict[ts][key]) powerlog_dict = { 'sample_count': sample_count, 'sample_duration': 1, 'average': {k: 1.0 * sum(v) / sample_count for k, v in powerlog_data.iteritems()}, 'data': powerlog_data, 'unit': {k: self._get_meet_unit(k) for k in key_set}, 'type': {k: self._get_meet_type(k) for k in key_set}, 'checkpoint': [[testname]] * sample_count, } return powerlog_dict