1# Copyright 2020 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. 4import collections 5import enum 6import json 7import os 8import logging 9import time 10 11from autotest_lib.client.common_lib import error 12from autotest_lib.client.common_lib.cros import chrome 13from autotest_lib.client.common_lib.cros import power_load_util 14from autotest_lib.client.cros.input_playback import keyboard 15from autotest_lib.client.cros.power import power_dashboard 16from autotest_lib.client.cros.power import power_status 17from autotest_lib.client.cros.power import power_test 18 19 20class power_MeetClient(power_test.power_Test): 21 """class for power_MeetClient test. 22 23 This test should be call from power_MeetCall server test only. 24 """ 25 version = 1 26 27 video_url = 'http://meet.google.com' 28 doc_url = 'http://doc.new' 29 30 def initialize(self, 31 seconds_period=5., 32 pdash_note='', 33 force_discharge=False): 34 """initialize method.""" 35 super(power_MeetClient, self).initialize( 36 seconds_period=seconds_period, 37 pdash_note=pdash_note, 38 force_discharge=force_discharge) 39 40 def run_once(self, 41 meet_code, 42 duration=180, 43 layout='Tiled', 44 username=None, 45 password=None): 46 """run_once method. 47 48 @param meet_code: Meet code generated in power_MeetCall. 49 @param duration: duration in seconds. 50 @param layout: string of meet layout to use. 51 @param username: Google account to use. 52 @param password: password for Google account. 53 """ 54 if not username and not password: 55 username = power_load_util.get_meet_username() 56 password = power_load_util.get_meet_password() 57 if not username or not password: 58 raise error.TestFail('Need to supply both username and password.') 59 extra_browser_args = self.get_extra_browser_args_for_camera_test() 60 with keyboard.Keyboard() as keys,\ 61 chrome.Chrome(init_network_controller=True, 62 gaia_login=True, 63 username=username, 64 password=password, 65 extra_browser_args=extra_browser_args, 66 autotest_ext=True) as cr: 67 68 # Move existing window to left half and open video page 69 tab = cr.browser.tabs[0] 70 tab.Activate() 71 72 # Run in full-screen. 73 fullscreen = tab.EvaluateJavaScript('document.webkitIsFullScreen') 74 if not fullscreen: 75 keys.press_key('f4') 76 77 url = self.video_url + '/' + meet_code 78 logging.info('Navigating left window to %s', url) 79 tab.Navigate(url) 80 81 # Workaround when camera isn't init for some unknown reason. 82 time.sleep(10) 83 tab.EvaluateJavaScript('location.reload()') 84 85 tab.WaitForDocumentReadyStateToBeComplete() 86 logging.info(meet_code) 87 self.keyvals['meet_code'] = meet_code 88 89 def wait_until(cond, error_msg): 90 """Helper for javascript polling wait.""" 91 for _ in range(60): 92 time.sleep(1) 93 if tab.EvaluateJavaScript(cond): 94 return 95 raise error.TestFail(error_msg) 96 97 wait_until('window.hasOwnProperty("hrTelemetryApi")', 98 'Meet API does not existed.') 99 wait_until('hrTelemetryApi.isInMeeting()', 100 'Can not join meeting.') 101 wait_until('hrTelemetryApi.getParticipantCount() > 1', 102 'Meeting has no other participant.') 103 104 # Make sure camera and mic are on. 105 tab.EvaluateJavaScript('hrTelemetryApi.setCameraMuted(false)') 106 tab.EvaluateJavaScript('hrTelemetryApi.setMicMuted(false)') 107 108 if layout == 'Tiled': 109 tab.EvaluateJavaScript('hrTelemetryApi.setTiledLayout()') 110 elif layout == 'Auto': 111 tab.EvaluateJavaScript('hrTelemetryApi.setAutoLayout()') 112 elif layout == 'Sidebar': 113 tab.EvaluateJavaScript('hrTelemetryApi.setSidebarLayout()') 114 elif layout == 'Spotlight': 115 tab.EvaluateJavaScript('hrTelemetryApi.setSpotlightLayout()') 116 else: 117 raise error.TestError('Unknown layout %s' % layout) 118 119 self.keyvals['layout'] = layout 120 121 self.start_measurements() 122 time.sleep(duration) 123 end_time = self._start_time + duration 124 125 # Collect stat 126 if not tab.EvaluateJavaScript('window.hasOwnProperty("realtime")'): 127 logging.info('Account %s is not in allowlist for MediaInfoAPI', 128 username) 129 return 130 131 meet_data = tab.EvaluateJavaScript( 132 'realtime.media.getMediaInfoDataPoints()') 133 134 power_dashboard.get_dashboard_factory().registerDataType( 135 MeetStatLogger, MeetStatDashboard) 136 137 self._meas_logs.append( 138 MeetStatLogger(self._start_time, end_time, meet_data)) 139 140 141class MeetStatLogger(power_status.MeasurementLogger): 142 """Class for logging meet data point to power dashboard. 143 144 Format of meet_data http://google3/logs/proto/buzz/callstats.proto 145 """ 146 147 def __init__(self, start_ts, end_ts, meet_data): 148 # Do not call parent constructor to avoid making a new thread. 149 self.times = [start_ts] 150 151 # Meet epoch timestamp uses millisec unit. 152 self.meet_data = [data_point for data_point in meet_data 153 if start_ts * 1000 <= data_point['timestamp'] <= end_ts * 1000] 154 155 def calc(self, mtype=None): 156 return {} 157 158 def save_results(self, resultsdir, fname_prefix=None): 159 # Save raw dict from meet to file. Ignore fname_prefix. 160 with open(os.path.join(resultsdir, 'meet_powerlog.json'), 'w') as f: 161 json.dump(self.meet_data , f, indent=4, separators=(',', ': '), 162 ensure_ascii=False) 163 164 165class MeetStatDashboard(power_dashboard.MeasurementLoggerDashboard): 166 """Dashboard class for MeetStatLogger class.""" 167 168 # Direction and type numbers map to constants in the proto 169 class Direction(enum.IntEnum): 170 """Possible directions for media entries of a data point.""" 171 SENDER = 0 172 RECEIVER = 1 173 174 class MediaType(enum.IntEnum): 175 """Possible media types for media entries of a data point.""" 176 VIDEO = 2 177 178 # Important metrics to collect. 179 MEET_KEYS = [ 180 'encodeUsagePercent', 181 'fps', 182 'height', 183 'width', 184 ] 185 186 def _get_ssrc_dict(self, meet_data): 187 """ Extract http://what/ssrc for all video stream and map to string. 188 189 The format of the string would be sender_# / receiver_# where # denotes 190 index for the video counting from 0. 191 192 Returns: 193 dict from ssrc to video stream string. 194 """ 195 ret = {} 196 count = [0, 0] 197 198 # We only care about video streams. 199 for media in meet_data[-1]['media']: 200 if media['mediatype'] != self.MediaType.VIDEO: 201 continue 202 if (media['direction'] != self.Direction.SENDER and 203 media['direction'] != self.Direction.RECEIVER): 204 continue 205 name = [media['directionStr'], str(count[media['direction']])] 206 if media['direction'] == self.Direction.SENDER: 207 name.append(media['sendercodecname']) 208 else: 209 name.append(media['receiverCodecName']) 210 count[media['direction']] += 1 211 ret[media['ssrc']] = '_'.join(name) 212 213 return ret 214 215 def _get_meet_unit(self, key): 216 """Return unit from name of the key.""" 217 if key.endswith('fps'): 218 return 'fps' 219 if key.endswith('Percent'): 220 return 'percent' 221 if key.endswith('width') or key.endswith('height') : 222 return 'point' 223 raise error.TestError('Unexpected key: %s' % key) 224 225 def _get_meet_type(self, key): 226 """Return type from name of the key.""" 227 if key.endswith('fps'): 228 return 'meet_fps' 229 if key.endswith('Percent'): 230 return 'meet_encoder_load' 231 if key.endswith('width'): 232 return 'meet_width' 233 if key.endswith('height'): 234 return 'meet_height' 235 raise error.TestError('Unexpected key: %s' % key) 236 237 def _convert(self): 238 """Convert meet raw dict to data to power dict.""" 239 240 meet_data = self._logger.meet_data 241 ssrc_dict = self._get_ssrc_dict(meet_data) 242 243 # Dict from timestamp to dict of meet_key to value 244 parse_dict = collections.defaultdict( 245 lambda: collections.defaultdict(int)) 246 247 key_set = set() 248 testname='power_MeetCall' 249 250 for data_point in meet_data: 251 timestamp = data_point['timestamp'] 252 for media in data_point['media']: 253 ssrc = media.get('ssrc', 0) 254 if ssrc not in ssrc_dict: 255 continue 256 name = ssrc_dict[media['ssrc']] 257 for meet_key in self.MEET_KEYS: 258 if meet_key not in media: 259 continue 260 key = '%s_%s' % (name, meet_key) 261 key_set.add(key) 262 parse_dict[timestamp][key] = media[meet_key] 263 264 timestamps = sorted(parse_dict.keys()) 265 sample_count = len(timestamps) 266 267 powerlog_data = collections.defaultdict(list) 268 for ts in sorted(parse_dict.keys()): 269 for key in key_set: 270 powerlog_data[key].append(parse_dict[ts][key]) 271 272 powerlog_dict = { 273 'sample_count': sample_count, 274 'sample_duration': 1, 275 'average': {k: 1.0 * sum(v) / sample_count 276 for k, v in powerlog_data.iteritems()}, 277 'data': powerlog_data, 278 'unit': {k: self._get_meet_unit(k) for k in key_set}, 279 'type': {k: self._get_meet_type(k) for k in key_set}, 280 'checkpoint': [[testname]] * sample_count, 281 } 282 283 return powerlog_dict 284