1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright 2017 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Li"""Facade to access the CFM functionality.""" 7*9c5db199SXin Li 8*9c5db199SXin Lifrom __future__ import absolute_import 9*9c5db199SXin Lifrom __future__ import division 10*9c5db199SXin Lifrom __future__ import print_function 11*9c5db199SXin Li 12*9c5db199SXin Liimport glob 13*9c5db199SXin Liimport logging 14*9c5db199SXin Liimport os 15*9c5db199SXin Liimport time 16*9c5db199SXin Liimport six 17*9c5db199SXin Liimport six.moves.urllib.parse 18*9c5db199SXin Liimport six.moves.xmlrpc_client 19*9c5db199SXin Li 20*9c5db199SXin Lifrom autotest_lib.client.bin import utils 21*9c5db199SXin Lifrom autotest_lib.client.common_lib import error 22*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import cfm_hangouts_api 23*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import cfm_meetings_api 24*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import enrollment 25*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import kiosk_utils 26*9c5db199SXin Lifrom autotest_lib.client.cros.graphics import graphics_utils 27*9c5db199SXin Li 28*9c5db199SXin Li 29*9c5db199SXin Liclass TimeoutException(Exception): 30*9c5db199SXin Li """Timeout Exception class.""" 31*9c5db199SXin Li pass 32*9c5db199SXin Li 33*9c5db199SXin Li 34*9c5db199SXin Liclass CFMFacadeLocal(object): 35*9c5db199SXin Li """Facade to access the CFM functionality. 36*9c5db199SXin Li 37*9c5db199SXin Li The methods inside this class only accept Python native types. 38*9c5db199SXin Li """ 39*9c5db199SXin Li _USER_ID = '[email protected]' 40*9c5db199SXin Li _PWD = 'test0000' 41*9c5db199SXin Li _EXT_ID = 'ikfcpmgefdpheiiomgmhlmmkihchmdlj' 42*9c5db199SXin Li _ENROLLMENT_DELAY = 45 43*9c5db199SXin Li _DEFAULT_TIMEOUT = 30 44*9c5db199SXin Li 45*9c5db199SXin Li # Log file locations 46*9c5db199SXin Li _BASE_DIR = '/home/chronos/user/Storage/ext/' 47*9c5db199SXin Li _CALLGROK_LOGS_PATTERN = _BASE_DIR + _EXT_ID + '/0*/File System/000/t/00/0*' 48*9c5db199SXin Li _PA_LOGS_PATTERN = _BASE_DIR + _EXT_ID + '/def/File System/primary/p/00/0*' 49*9c5db199SXin Li 50*9c5db199SXin Li 51*9c5db199SXin Li def __init__(self, resource, screen): 52*9c5db199SXin Li """Initializes a CFMFacadeLocal. 53*9c5db199SXin Li 54*9c5db199SXin Li @param resource: A FacadeResource object. 55*9c5db199SXin Li """ 56*9c5db199SXin Li self._resource = resource 57*9c5db199SXin Li self._screen = screen 58*9c5db199SXin Li 59*9c5db199SXin Li 60*9c5db199SXin Li def enroll_device(self): 61*9c5db199SXin Li """Enroll device into CFM.""" 62*9c5db199SXin Li logging.info('Enrolling device...') 63*9c5db199SXin Li extra_browser_args = ["--force-devtools-available"] 64*9c5db199SXin Li self._resource.start_custom_chrome({ 65*9c5db199SXin Li "auto_login": False, 66*9c5db199SXin Li "disable_gaia_services": False, 67*9c5db199SXin Li "extra_browser_args": extra_browser_args}) 68*9c5db199SXin Li 69*9c5db199SXin Li enrollment.RemoraEnrollment(self._resource._browser, self._USER_ID, 70*9c5db199SXin Li self._PWD) 71*9c5db199SXin Li # Timeout to allow for the device to stablize and go back to the 72*9c5db199SXin Li # OOB screen before proceeding. The device may restart the app a couple 73*9c5db199SXin Li # of times before it reaches the OOB screen. 74*9c5db199SXin Li time.sleep(self._ENROLLMENT_DELAY) 75*9c5db199SXin Li logging.info('Enrollment completed.') 76*9c5db199SXin Li 77*9c5db199SXin Li 78*9c5db199SXin Li def restart_chrome_for_cfm(self, extra_chrome_args=None): 79*9c5db199SXin Li """Restart chrome with custom values for CFM. 80*9c5db199SXin Li 81*9c5db199SXin Li @param extra_chrome_args a list with extra command line arguments for 82*9c5db199SXin Li Chrome. 83*9c5db199SXin Li """ 84*9c5db199SXin Li logging.info('Restarting chrome for CfM...') 85*9c5db199SXin Li custom_chrome_setup = {"clear_enterprise_policy": False, 86*9c5db199SXin Li "dont_override_profile": True, 87*9c5db199SXin Li "disable_gaia_services": False, 88*9c5db199SXin Li "disable_default_apps": False, 89*9c5db199SXin Li "auto_login": False} 90*9c5db199SXin Li custom_chrome_setup["extra_browser_args"] = ( 91*9c5db199SXin Li ["--force-devtools-available"]) 92*9c5db199SXin Li if extra_chrome_args: 93*9c5db199SXin Li custom_chrome_setup["extra_browser_args"].extend(extra_chrome_args) 94*9c5db199SXin Li self._resource.start_custom_chrome(custom_chrome_setup) 95*9c5db199SXin Li logging.info('Chrome process restarted in CfM mode.') 96*9c5db199SXin Li 97*9c5db199SXin Li 98*9c5db199SXin Li def check_hangout_extension_context(self): 99*9c5db199SXin Li """Check to make sure hangout app launched. 100*9c5db199SXin Li 101*9c5db199SXin Li @raises error.TestFail if the URL checks fails. 102*9c5db199SXin Li """ 103*9c5db199SXin Li logging.info('Verifying extension contexts...') 104*9c5db199SXin Li ext_contexts = kiosk_utils.wait_for_kiosk_ext( 105*9c5db199SXin Li self._resource._browser, self._EXT_ID) 106*9c5db199SXin Li ext_urls = [context.EvaluateJavaScript('location.href;') 107*9c5db199SXin Li for context in ext_contexts] 108*9c5db199SXin Li expected_urls = ['chrome-extension://' + self._EXT_ID + '/' + path 109*9c5db199SXin Li for path in ['hangoutswindow.html?windowid=0', 110*9c5db199SXin Li 'hangoutswindow.html?windowid=1', 111*9c5db199SXin Li 'hangoutswindow.html?windowid=2', 112*9c5db199SXin Li '_generated_background_page.html']] 113*9c5db199SXin Li for url in ext_urls: 114*9c5db199SXin Li logging.info('Extension URL %s', url) 115*9c5db199SXin Li if url not in expected_urls: 116*9c5db199SXin Li raise error.TestFail( 117*9c5db199SXin Li 'Unexpected extension context urls, expected one of %s, ' 118*9c5db199SXin Li 'got %s' % (expected_urls, url)) 119*9c5db199SXin Li logging.info('Hangouts extension contexts verified.') 120*9c5db199SXin Li 121*9c5db199SXin Li 122*9c5db199SXin Li def take_screenshot(self, screenshot_name): 123*9c5db199SXin Li """ 124*9c5db199SXin Li Takes a screenshot of what is currently displayed in png format. 125*9c5db199SXin Li 126*9c5db199SXin Li The screenshot is stored in /tmp. Uses the low level graphics_utils API. 127*9c5db199SXin Li 128*9c5db199SXin Li @param screenshot_name: Name of the screenshot file. 129*9c5db199SXin Li @returns The path to the screenshot or None. 130*9c5db199SXin Li """ 131*9c5db199SXin Li try: 132*9c5db199SXin Li return graphics_utils.take_screenshot('/tmp', screenshot_name) 133*9c5db199SXin Li except Exception as e: 134*9c5db199SXin Li logging.warning('Taking screenshot failed', exc_info = e) 135*9c5db199SXin Li return None 136*9c5db199SXin Li 137*9c5db199SXin Li 138*9c5db199SXin Li def get_latest_callgrok_file_path(self): 139*9c5db199SXin Li """ 140*9c5db199SXin Li @return The path to the lastest callgrok log file, if any. 141*9c5db199SXin Li """ 142*9c5db199SXin Li try: 143*9c5db199SXin Li return max(glob.iglob(self._CALLGROK_LOGS_PATTERN), 144*9c5db199SXin Li key=os.path.getctime) 145*9c5db199SXin Li except ValueError as e: 146*9c5db199SXin Li logging.exception('Error while searching for callgrok logs.') 147*9c5db199SXin Li return None 148*9c5db199SXin Li 149*9c5db199SXin Li 150*9c5db199SXin Li def get_latest_pa_logs_file_path(self): 151*9c5db199SXin Li """ 152*9c5db199SXin Li @return The path to the lastest packaged app log file, if any. 153*9c5db199SXin Li """ 154*9c5db199SXin Li try: 155*9c5db199SXin Li return max(self.get_all_pa_logs_file_path(), key=os.path.getctime) 156*9c5db199SXin Li except ValueError as e: 157*9c5db199SXin Li logging.exception('Error while searching for packaged app logs.') 158*9c5db199SXin Li return None 159*9c5db199SXin Li 160*9c5db199SXin Li 161*9c5db199SXin Li def get_all_pa_logs_file_path(self): 162*9c5db199SXin Li """ 163*9c5db199SXin Li @return The paths to the all packaged app log files, if any. 164*9c5db199SXin Li """ 165*9c5db199SXin Li return glob.glob(self._PA_LOGS_PATTERN) 166*9c5db199SXin Li 167*9c5db199SXin Li def reboot_device_with_chrome_api(self): 168*9c5db199SXin Li """Reboot device using chrome runtime API.""" 169*9c5db199SXin Li ext_contexts = kiosk_utils.wait_for_kiosk_ext( 170*9c5db199SXin Li self._resource._browser, self._EXT_ID) 171*9c5db199SXin Li for context in ext_contexts: 172*9c5db199SXin Li context.WaitForDocumentReadyStateToBeInteractiveOrBetter() 173*9c5db199SXin Li ext_url = context.EvaluateJavaScript('document.URL') 174*9c5db199SXin Li background_url = ('chrome-extension://' + self._EXT_ID + 175*9c5db199SXin Li '/_generated_background_page.html') 176*9c5db199SXin Li if ext_url in background_url: 177*9c5db199SXin Li context.ExecuteJavaScript('chrome.runtime.restart();') 178*9c5db199SXin Li 179*9c5db199SXin Li 180*9c5db199SXin Li def _get_webview_context_by_screen(self, screen): 181*9c5db199SXin Li """Get webview context that matches the screen param in the url. 182*9c5db199SXin Li 183*9c5db199SXin Li @param screen: Value of the screen param, e.g. 'hotrod' or 'control'. 184*9c5db199SXin Li """ 185*9c5db199SXin Li def _get_context(): 186*9c5db199SXin Li try: 187*9c5db199SXin Li ctxs = kiosk_utils.get_webview_contexts(self._resource._browser, 188*9c5db199SXin Li self._EXT_ID) 189*9c5db199SXin Li for ctx in ctxs: 190*9c5db199SXin Li parse_result = six.moves.urllib.parse.urlparse(ctx.GetUrl()) 191*9c5db199SXin Li url_path = parse_result.path 192*9c5db199SXin Li logging.info('Webview path: "%s"', url_path) 193*9c5db199SXin Li url_query = parse_result.query 194*9c5db199SXin Li logging.info('Webview query: "%s"', url_query) 195*9c5db199SXin Li params = six.moves.urllib.parse.parse_qs(url_query, 196*9c5db199SXin Li keep_blank_values = True) 197*9c5db199SXin Li is_oobe_node_screen = ( 198*9c5db199SXin Li # Hangouts Classic 199*9c5db199SXin Li ('nooobestatesync' in params and 'oobedone' in params) 200*9c5db199SXin Li # Hangouts Meet 201*9c5db199SXin Li or ('oobesecondary' in url_path)) 202*9c5db199SXin Li if is_oobe_node_screen: 203*9c5db199SXin Li # Skip the oobe node screen. Not doing this can cause 204*9c5db199SXin Li # the wrong webview context to be returned. 205*9c5db199SXin Li continue 206*9c5db199SXin Li if 'screen' in params and params['screen'][0] == screen: 207*9c5db199SXin Li return ctx 208*9c5db199SXin Li except Exception as e: 209*9c5db199SXin Li # Having a MIMO attached to the DUT causes a couple of webview 210*9c5db199SXin Li # destruction/construction operations during OOBE. If we query a 211*9c5db199SXin Li # destructed webview it will throw an exception. Instead of 212*9c5db199SXin Li # failing the test, we just swallow the exception. 213*9c5db199SXin Li logging.exception( 214*9c5db199SXin Li "Exception occured while querying the webview contexts.") 215*9c5db199SXin Li return None 216*9c5db199SXin Li 217*9c5db199SXin Li return utils.poll_for_condition( 218*9c5db199SXin Li _get_context, 219*9c5db199SXin Li exception=error.TestFail( 220*9c5db199SXin Li 'Webview with screen param "%s" not found.' % screen), 221*9c5db199SXin Li timeout=self._DEFAULT_TIMEOUT, 222*9c5db199SXin Li sleep_interval = 1) 223*9c5db199SXin Li 224*9c5db199SXin Li 225*9c5db199SXin Li def skip_oobe_after_enrollment(self): 226*9c5db199SXin Li """Skips oobe and goes to the app landing page after enrollment.""" 227*9c5db199SXin Li # Due to a variying amount of app restarts before we reach the OOB page 228*9c5db199SXin Li # we need to restart Chrome in order to make sure we have the devtools 229*9c5db199SXin Li # handle available and up-to-date. 230*9c5db199SXin Li self.restart_chrome_for_cfm() 231*9c5db199SXin Li self.check_hangout_extension_context() 232*9c5db199SXin Li self.wait_for_telemetry_commands() 233*9c5db199SXin Li self.wait_for_oobe_start_page() 234*9c5db199SXin Li self.skip_oobe_screen() 235*9c5db199SXin Li 236*9c5db199SXin Li 237*9c5db199SXin Li @property 238*9c5db199SXin Li def _webview_context(self): 239*9c5db199SXin Li """Get webview context object.""" 240*9c5db199SXin Li return self._get_webview_context_by_screen(self._screen) 241*9c5db199SXin Li 242*9c5db199SXin Li 243*9c5db199SXin Li @property 244*9c5db199SXin Li def _cfmApi(self): 245*9c5db199SXin Li """Instantiate appropriate cfm api wrapper""" 246*9c5db199SXin Li if self._webview_context.EvaluateJavaScript( 247*9c5db199SXin Li "typeof window.hrRunDiagnosticsForTest == 'function'"): 248*9c5db199SXin Li return cfm_hangouts_api.CfmHangoutsAPI(self._webview_context) 249*9c5db199SXin Li if self._webview_context.EvaluateJavaScript( 250*9c5db199SXin Li "typeof window.hrTelemetryApi != 'undefined'"): 251*9c5db199SXin Li return cfm_meetings_api.CfmMeetingsAPI(self._webview_context) 252*9c5db199SXin Li raise error.TestFail('No hangouts or meet telemetry API available. ' 253*9c5db199SXin Li 'Current url is "%s"' % 254*9c5db199SXin Li self._webview_context.GetUrl()) 255*9c5db199SXin Li 256*9c5db199SXin Li 257*9c5db199SXin Li def wait_for_telemetry_commands(self): 258*9c5db199SXin Li """Wait for telemetry commands.""" 259*9c5db199SXin Li logging.info('Wait for Hangouts telemetry commands') 260*9c5db199SXin Li self._webview_context.WaitForJavaScriptCondition( 261*9c5db199SXin Li """typeof window.hrOobIsStartPageForTest == 'function' 262*9c5db199SXin Li || typeof window.hrTelemetryApi != 'undefined' 263*9c5db199SXin Li """, 264*9c5db199SXin Li timeout=self._DEFAULT_TIMEOUT) 265*9c5db199SXin Li 266*9c5db199SXin Li 267*9c5db199SXin Li def wait_for_meetings_in_call_page(self): 268*9c5db199SXin Li """Waits for the in-call page to launch.""" 269*9c5db199SXin Li self.wait_for_telemetry_commands() 270*9c5db199SXin Li self._cfmApi.wait_for_meetings_in_call_page() 271*9c5db199SXin Li 272*9c5db199SXin Li 273*9c5db199SXin Li def wait_for_meetings_landing_page(self): 274*9c5db199SXin Li """Waits for the landing page screen.""" 275*9c5db199SXin Li self.wait_for_telemetry_commands() 276*9c5db199SXin Li self._cfmApi.wait_for_meetings_landing_page() 277*9c5db199SXin Li 278*9c5db199SXin Li 279*9c5db199SXin Li # UI commands/functions 280*9c5db199SXin Li def wait_for_oobe_start_page(self): 281*9c5db199SXin Li """Wait for oobe start screen to launch.""" 282*9c5db199SXin Li logging.info('Waiting for OOBE screen') 283*9c5db199SXin Li self._cfmApi.wait_for_oobe_start_page() 284*9c5db199SXin Li 285*9c5db199SXin Li 286*9c5db199SXin Li def skip_oobe_screen(self): 287*9c5db199SXin Li """Skip Chromebox for Meetings oobe screen.""" 288*9c5db199SXin Li logging.info('Skipping OOBE screen') 289*9c5db199SXin Li self._cfmApi.skip_oobe_screen() 290*9c5db199SXin Li 291*9c5db199SXin Li 292*9c5db199SXin Li def is_oobe_start_page(self): 293*9c5db199SXin Li """Check if device is on CFM oobe start screen. 294*9c5db199SXin Li 295*9c5db199SXin Li @return a boolean, based on oobe start page status. 296*9c5db199SXin Li """ 297*9c5db199SXin Li return self._cfmApi.is_oobe_start_page() 298*9c5db199SXin Li 299*9c5db199SXin Li 300*9c5db199SXin Li # Hangouts commands/functions 301*9c5db199SXin Li def start_new_hangout_session(self, session_name): 302*9c5db199SXin Li """Start a new hangout session. 303*9c5db199SXin Li 304*9c5db199SXin Li @param session_name: Name of the hangout session. 305*9c5db199SXin Li """ 306*9c5db199SXin Li self._cfmApi.start_new_hangout_session(session_name) 307*9c5db199SXin Li 308*9c5db199SXin Li 309*9c5db199SXin Li def end_hangout_session(self): 310*9c5db199SXin Li """End current hangout session.""" 311*9c5db199SXin Li self._cfmApi.end_hangout_session() 312*9c5db199SXin Li 313*9c5db199SXin Li 314*9c5db199SXin Li def is_in_hangout_session(self): 315*9c5db199SXin Li """Check if device is in hangout session. 316*9c5db199SXin Li 317*9c5db199SXin Li @return a boolean, for hangout session state. 318*9c5db199SXin Li """ 319*9c5db199SXin Li return self._cfmApi.is_in_hangout_session() 320*9c5db199SXin Li 321*9c5db199SXin Li 322*9c5db199SXin Li def is_ready_to_start_hangout_session(self): 323*9c5db199SXin Li """Check if device is ready to start a new hangout session. 324*9c5db199SXin Li 325*9c5db199SXin Li @return a boolean for hangout session ready state. 326*9c5db199SXin Li """ 327*9c5db199SXin Li return self._cfmApi.is_ready_to_start_hangout_session() 328*9c5db199SXin Li 329*9c5db199SXin Li 330*9c5db199SXin Li def join_meeting_session(self, session_name): 331*9c5db199SXin Li """Joins a meeting. 332*9c5db199SXin Li 333*9c5db199SXin Li @param session_name: Name of the meeting session. 334*9c5db199SXin Li """ 335*9c5db199SXin Li self._cfmApi.join_meeting_session(session_name) 336*9c5db199SXin Li 337*9c5db199SXin Li 338*9c5db199SXin Li def start_meeting_session(self): 339*9c5db199SXin Li """Start a meeting. 340*9c5db199SXin Li 341*9c5db199SXin Li @return code for the started meeting 342*9c5db199SXin Li """ 343*9c5db199SXin Li return self._cfmApi.start_meeting_session() 344*9c5db199SXin Li 345*9c5db199SXin Li 346*9c5db199SXin Li def end_meeting_session(self): 347*9c5db199SXin Li """End current meeting session.""" 348*9c5db199SXin Li self._cfmApi.end_meeting_session() 349*9c5db199SXin Li 350*9c5db199SXin Li 351*9c5db199SXin Li def get_participant_count(self): 352*9c5db199SXin Li """Gets the total participant count in a call.""" 353*9c5db199SXin Li return self._cfmApi.get_participant_count() 354*9c5db199SXin Li 355*9c5db199SXin Li 356*9c5db199SXin Li # Diagnostics commands/functions 357*9c5db199SXin Li def is_diagnostic_run_in_progress(self): 358*9c5db199SXin Li """Check if hotrod diagnostics is running. 359*9c5db199SXin Li 360*9c5db199SXin Li @return a boolean for diagnostic run state. 361*9c5db199SXin Li """ 362*9c5db199SXin Li return self._cfmApi.is_diagnostic_run_in_progress() 363*9c5db199SXin Li 364*9c5db199SXin Li 365*9c5db199SXin Li def wait_for_diagnostic_run_to_complete(self): 366*9c5db199SXin Li """Wait for hotrod diagnostics to complete.""" 367*9c5db199SXin Li self._cfmApi.wait_for_diagnostic_run_to_complete() 368*9c5db199SXin Li 369*9c5db199SXin Li 370*9c5db199SXin Li def run_diagnostics(self): 371*9c5db199SXin Li """Run hotrod diagnostics.""" 372*9c5db199SXin Li self._cfmApi.run_diagnostics() 373*9c5db199SXin Li 374*9c5db199SXin Li 375*9c5db199SXin Li def get_last_diagnostics_results(self): 376*9c5db199SXin Li """Get latest hotrod diagnostics results. 377*9c5db199SXin Li 378*9c5db199SXin Li @return a dict with diagnostic test results. 379*9c5db199SXin Li """ 380*9c5db199SXin Li return self._cfmApi.get_last_diagnostics_results() 381*9c5db199SXin Li 382*9c5db199SXin Li 383*9c5db199SXin Li # Mic audio commands/functions 384*9c5db199SXin Li def is_mic_muted(self): 385*9c5db199SXin Li """Check if mic is muted. 386*9c5db199SXin Li 387*9c5db199SXin Li @return a boolean for mic mute state. 388*9c5db199SXin Li """ 389*9c5db199SXin Li return self._cfmApi.is_mic_muted() 390*9c5db199SXin Li 391*9c5db199SXin Li 392*9c5db199SXin Li def mute_mic(self): 393*9c5db199SXin Li """Local mic mute from toolbar.""" 394*9c5db199SXin Li self._cfmApi.mute_mic() 395*9c5db199SXin Li 396*9c5db199SXin Li 397*9c5db199SXin Li def unmute_mic(self): 398*9c5db199SXin Li """Local mic unmute from toolbar.""" 399*9c5db199SXin Li self._cfmApi.unmute_mic() 400*9c5db199SXin Li 401*9c5db199SXin Li 402*9c5db199SXin Li def remote_mute_mic(self): 403*9c5db199SXin Li """Remote mic mute request from cPanel.""" 404*9c5db199SXin Li self._cfmApi.remote_mute_mic() 405*9c5db199SXin Li 406*9c5db199SXin Li 407*9c5db199SXin Li def remote_unmute_mic(self): 408*9c5db199SXin Li """Remote mic unmute request from cPanel.""" 409*9c5db199SXin Li self._cfmApi.remote_unmute_mic() 410*9c5db199SXin Li 411*9c5db199SXin Li 412*9c5db199SXin Li def get_mic_devices(self): 413*9c5db199SXin Li """Get all mic devices detected by hotrod. 414*9c5db199SXin Li 415*9c5db199SXin Li @return a list of mic devices. 416*9c5db199SXin Li """ 417*9c5db199SXin Li return self._cfmApi.get_mic_devices() 418*9c5db199SXin Li 419*9c5db199SXin Li 420*9c5db199SXin Li def get_preferred_mic(self): 421*9c5db199SXin Li """Get mic preferred for hotrod. 422*9c5db199SXin Li 423*9c5db199SXin Li @return a str with preferred mic name. 424*9c5db199SXin Li """ 425*9c5db199SXin Li return self._cfmApi.get_preferred_mic() 426*9c5db199SXin Li 427*9c5db199SXin Li 428*9c5db199SXin Li def set_preferred_mic(self, mic): 429*9c5db199SXin Li """Set preferred mic for hotrod. 430*9c5db199SXin Li 431*9c5db199SXin Li @param mic: String with mic name. 432*9c5db199SXin Li """ 433*9c5db199SXin Li self._cfmApi.set_preferred_mic(mic) 434*9c5db199SXin Li 435*9c5db199SXin Li 436*9c5db199SXin Li # Speaker commands/functions 437*9c5db199SXin Li def get_speaker_devices(self): 438*9c5db199SXin Li """Get all speaker devices detected by hotrod. 439*9c5db199SXin Li 440*9c5db199SXin Li @return a list of speaker devices. 441*9c5db199SXin Li """ 442*9c5db199SXin Li return self._cfmApi.get_speaker_devices() 443*9c5db199SXin Li 444*9c5db199SXin Li 445*9c5db199SXin Li def get_preferred_speaker(self): 446*9c5db199SXin Li """Get speaker preferred for hotrod. 447*9c5db199SXin Li 448*9c5db199SXin Li @return a str with preferred speaker name. 449*9c5db199SXin Li """ 450*9c5db199SXin Li return self._cfmApi.get_preferred_speaker() 451*9c5db199SXin Li 452*9c5db199SXin Li 453*9c5db199SXin Li def set_preferred_speaker(self, speaker): 454*9c5db199SXin Li """Set preferred speaker for hotrod. 455*9c5db199SXin Li 456*9c5db199SXin Li @param speaker: String with speaker name. 457*9c5db199SXin Li """ 458*9c5db199SXin Li self._cfmApi.set_preferred_speaker(speaker) 459*9c5db199SXin Li 460*9c5db199SXin Li 461*9c5db199SXin Li def set_speaker_volume(self, volume_level): 462*9c5db199SXin Li """Set speaker volume. 463*9c5db199SXin Li 464*9c5db199SXin Li @param volume_level: String value ranging from 0-100 to set volume to. 465*9c5db199SXin Li """ 466*9c5db199SXin Li self._cfmApi.set_speaker_volume(volume_level) 467*9c5db199SXin Li 468*9c5db199SXin Li 469*9c5db199SXin Li def get_speaker_volume(self): 470*9c5db199SXin Li """Get current speaker volume. 471*9c5db199SXin Li 472*9c5db199SXin Li @return a str value with speaker volume level 0-100. 473*9c5db199SXin Li """ 474*9c5db199SXin Li return self._cfmApi.get_speaker_volume() 475*9c5db199SXin Li 476*9c5db199SXin Li 477*9c5db199SXin Li def play_test_sound(self): 478*9c5db199SXin Li """Play test sound.""" 479*9c5db199SXin Li self._cfmApi.play_test_sound() 480*9c5db199SXin Li 481*9c5db199SXin Li 482*9c5db199SXin Li # Camera commands/functions 483*9c5db199SXin Li def get_camera_devices(self): 484*9c5db199SXin Li """Get all camera devices detected by hotrod. 485*9c5db199SXin Li 486*9c5db199SXin Li @return a list of camera devices. 487*9c5db199SXin Li """ 488*9c5db199SXin Li return self._cfmApi.get_camera_devices() 489*9c5db199SXin Li 490*9c5db199SXin Li 491*9c5db199SXin Li def get_preferred_camera(self): 492*9c5db199SXin Li """Get camera preferred for hotrod. 493*9c5db199SXin Li 494*9c5db199SXin Li @return a str with preferred camera name. 495*9c5db199SXin Li """ 496*9c5db199SXin Li return self._cfmApi.get_preferred_camera() 497*9c5db199SXin Li 498*9c5db199SXin Li 499*9c5db199SXin Li def set_preferred_camera(self, camera): 500*9c5db199SXin Li """Set preferred camera for hotrod. 501*9c5db199SXin Li 502*9c5db199SXin Li @param camera: String with camera name. 503*9c5db199SXin Li """ 504*9c5db199SXin Li self._cfmApi.set_preferred_camera(camera) 505*9c5db199SXin Li 506*9c5db199SXin Li 507*9c5db199SXin Li def is_camera_muted(self): 508*9c5db199SXin Li """Check if camera is muted (turned off). 509*9c5db199SXin Li 510*9c5db199SXin Li @return a boolean for camera muted state. 511*9c5db199SXin Li """ 512*9c5db199SXin Li return self._cfmApi.is_camera_muted() 513*9c5db199SXin Li 514*9c5db199SXin Li 515*9c5db199SXin Li def mute_camera(self): 516*9c5db199SXin Li """Turned camera off.""" 517*9c5db199SXin Li self._cfmApi.mute_camera() 518*9c5db199SXin Li 519*9c5db199SXin Li 520*9c5db199SXin Li def unmute_camera(self): 521*9c5db199SXin Li """Turned camera on.""" 522*9c5db199SXin Li self._cfmApi.unmute_camera() 523*9c5db199SXin Li 524*9c5db199SXin Li def move_camera(self, camera_motion): 525*9c5db199SXin Li """Move camera(PTZ commands). 526*9c5db199SXin Li 527*9c5db199SXin Li @param camera_motion: Set of allowed commands 528*9c5db199SXin Li defined in cfmApi.move_camera. 529*9c5db199SXin Li """ 530*9c5db199SXin Li self._cfmApi.move_camera(camera_motion) 531*9c5db199SXin Li 532*9c5db199SXin Li def _convert_large_integers(self, o): 533*9c5db199SXin Li if type(o) is list: 534*9c5db199SXin Li return [self._convert_large_integers(x) for x in o] 535*9c5db199SXin Li elif type(o) is dict: 536*9c5db199SXin Li return { 537*9c5db199SXin Li k: self._convert_large_integers(v) 538*9c5db199SXin Li for k, v in six.iteritems(o) 539*9c5db199SXin Li } 540*9c5db199SXin Li else: 541*9c5db199SXin Li if type(o) is int and o > six.moves.xmlrpc_client.MAXINT: 542*9c5db199SXin Li return float(o) 543*9c5db199SXin Li else: 544*9c5db199SXin Li return o 545*9c5db199SXin Li 546*9c5db199SXin Li def get_media_info_data_points(self): 547*9c5db199SXin Li """ 548*9c5db199SXin Li Gets media info data points containing media stats. 549*9c5db199SXin Li 550*9c5db199SXin Li These are exported on the window object when the 551*9c5db199SXin Li ExportMediaInfo mod is enabled. 552*9c5db199SXin Li 553*9c5db199SXin Li @returns A list with dictionaries of media info data points. 554*9c5db199SXin Li @raises RuntimeError if the data point API is not available. 555*9c5db199SXin Li """ 556*9c5db199SXin Li is_api_available_script = ( 557*9c5db199SXin Li '"realtime" in window ' 558*9c5db199SXin Li '&& "media" in realtime ' 559*9c5db199SXin Li '&& "getMediaInfoDataPoints" in realtime.media') 560*9c5db199SXin Li if not self._webview_context.EvaluateJavaScript( 561*9c5db199SXin Li is_api_available_script): 562*9c5db199SXin Li raise RuntimeError( 563*9c5db199SXin Li 'realtime.media.getMediaInfoDataPoints not available. ' 564*9c5db199SXin Li 'Is the ExportMediaInfo mod active? ' 565*9c5db199SXin Li 'The mod is only available for Meet.') 566*9c5db199SXin Li 567*9c5db199SXin Li # Sanitize the timestamp on the JS side to work around crbug.com/851482. 568*9c5db199SXin Li # Use JSON stringify/parse to create a deep copy of the data point. 569*9c5db199SXin Li get_data_points_js_script = """ 570*9c5db199SXin Li var dataPoints = window.realtime.media.getMediaInfoDataPoints(); 571*9c5db199SXin Li dataPoints.map((point) => { 572*9c5db199SXin Li var sanitizedPoint = JSON.parse(JSON.stringify(point)); 573*9c5db199SXin Li sanitizedPoint["timestamp"] /= 1000.0; 574*9c5db199SXin Li return sanitizedPoint; 575*9c5db199SXin Li });""" 576*9c5db199SXin Li 577*9c5db199SXin Li data_points = self._webview_context.EvaluateJavaScript( 578*9c5db199SXin Li get_data_points_js_script) 579*9c5db199SXin Li # XML RCP gives overflow errors when trying to send too large 580*9c5db199SXin Li # integers or longs so we convert media stats to floats. 581*9c5db199SXin Li data_points = self._convert_large_integers(data_points) 582*9c5db199SXin Li return data_points 583