# Copyright 2015 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. """Handler for audio extension functionality.""" import logging from autotest_lib.client.bin import utils from autotest_lib.client.cros.multimedia import facade_resource class AudioExtensionHandlerError(Exception): """Class for exceptions thrown from the AudioExtensionHandler""" pass class AudioExtensionHandler(object): """Wrapper around test extension that uses chrome.audio API to get audio device information """ def __init__(self, extension): """Initializes an AudioExtensionHandler. @param extension: Extension got from telemetry chrome wrapper. """ self._extension = extension self._check_api_available() def _check_api_available(self): """Checks chrome.audio is available. @raises: AudioExtensionHandlerError if extension is not available. """ success = utils.wait_for_value( lambda: (self._extension.EvaluateJavaScript( "chrome.audio") != None), expected_value=True) if not success: raise AudioExtensionHandlerError('chrome.audio is not available.') @facade_resource.retry_chrome_call def get_audio_devices(self, device_filter=None): """Gets the audio device info from Chrome audio API. @param device_filter: Filter for returned device nodes. An optional dict that can have the following properties: string array streamTypes Restricts stream types that returned devices can have. It should contain "INPUT" for result to include input devices, and "OUTPUT" for results to include output devices. If not set, returned devices will not be filtered by the stream type. boolean isActive If true, only active devices will be included in the result. If false, only inactive devices will be included in the result. The filter param defaults to {}, requests all available audio devices. @returns: An array of audioDeviceInfo. Each audioDeviceInfo dict contains these key-value pairs: string id The unique identifier of the audio device. string stableDeviceId The stable identifier of the audio device. string streamType "INPUT" if the device is an input audio device, "OUTPUT" if the device is an output audio device. string displayName The user-friendly name (e.g. "Bose Amplifier"). string deviceName The devuce name boolean isActive True if this is the current active device. boolean isMuted True if this is muted. long level The output volume or input gain. """ def filter_to_str(device_filter): """Converts python dict device filter to JS object string. @param device_filter: Device filter dict. @returns: Device filter as a srting representation of a JavaScript object. """ return str(device_filter or {}).replace('True', 'true').replace( 'False', 'false') self._extension.ExecuteJavaScript('window.__audio_devices = null;') self._extension.ExecuteJavaScript( "chrome.audio.getDevices(%s, function(devices) {" "window.__audio_devices = devices;})" % filter_to_str(device_filter)) utils.wait_for_value( lambda: (self._extension.EvaluateJavaScript( "window.__audio_devices") != None), expected_value=True) return self._extension.EvaluateJavaScript("window.__audio_devices") def _get_active_id_for_stream_type(self, stream_type): """Gets active node id of the specified stream type. Assume there is only one active node. @param stream_type: 'INPUT' to get the active input device, 'OUTPUT' to get the active output device. @returns: A string for the active device id. @raises: AudioExtensionHandlerError if active id is not unique. """ nodes = self.get_audio_devices( {'streamTypes': [stream_type], 'isActive': True}) if len(nodes) != 1: logging.error( 'Node info contains multiple active nodes: %s', nodes) raise AudioExtensionHandlerError('Active id should be unique') return nodes[0]['id'] @facade_resource.retry_chrome_call def set_active_volume(self, volume): """Sets the active audio output volume using chrome.audio API. This method also unmutes the node. @param volume: Volume to set (0~100). """ output_id = self._get_active_id_for_stream_type('OUTPUT') logging.debug('output_id: %s', output_id) self.set_mute(False) self._extension.ExecuteJavaScript('window.__set_volume_done = null;') self._extension.ExecuteJavaScript( """ chrome.audio.setProperties( '%s', {level: %s}, function() {window.__set_volume_done = true;}); """ % (output_id, volume)) utils.wait_for_value( lambda: (self._extension.EvaluateJavaScript( "window.__set_volume_done") != None), expected_value=True) @facade_resource.retry_chrome_call def set_mute(self, mute): """Mutes the audio output using chrome.audio API. @param mute: True to mute. False otherwise. """ is_muted_string = 'true' if mute else 'false' self._extension.ExecuteJavaScript('window.__set_mute_done = null;') self._extension.ExecuteJavaScript( """ chrome.audio.setMute( 'OUTPUT', %s, function() {window.__set_mute_done = true;}); """ % (is_muted_string)) utils.wait_for_value( lambda: (self._extension.EvaluateJavaScript( "window.__set_mute_done") != None), expected_value=True) @facade_resource.retry_chrome_call def get_mute(self): """Determines whether audio output is muted. @returns Whether audio output is muted. """ self._extension.ExecuteJavaScript('window.__output_muted = null;') self._extension.ExecuteJavaScript( "chrome.audio.getMute('OUTPUT', function(isMute) {" "window.__output_muted = isMute;})") utils.wait_for_value( lambda: (self._extension.EvaluateJavaScript( "window.__output_muted") != None), expected_value=True) return self._extension.EvaluateJavaScript("window.__output_muted") @facade_resource.retry_chrome_call def get_active_volume_mute(self): """Gets the volume state of active audio output using chrome.audio API. @param returns: A tuple (volume, mute), where volume is 0~100, and mute is True if node is muted, False otherwise. """ nodes = self.get_audio_devices( {'streamTypes': ['OUTPUT'], 'isActive': True}) if len(nodes) != 1: logging.error('Node info contains multiple active nodes: %s', nodes) raise AudioExtensionHandlerError('Active id should be unique') return (nodes[0]['level'], self.get_mute()) @facade_resource.retry_chrome_call def set_active_node_id(self, node_id): """Sets the active node by node id. The current active node will be disabled first if the new active node is different from the current one. @param node_id: Node id obtained from cras_utils.get_cras_nodes. Chrome.audio also uses this id to specify input/output nodes. Note that node id returned by cras_utils.get_cras_nodes is a number, while chrome.audio API expects a string. @raises AudioExtensionHandlerError if there is no such id. """ nodes = self.get_audio_devices({}) target_node = None for node in nodes: if node['id'] == str(node_id): target_node = node break if not target_node: logging.error('Node %s not found.', node_id) raise AudioExtensionHandlerError('Node id not found') if target_node['isActive']: logging.debug('Node %s is already active.', node_id) return logging.debug('Setting active id to %s', node_id) self._extension.ExecuteJavaScript('window.__set_active_done = null;') is_input = target_node['streamType'] == 'INPUT' stream_type = 'input' if is_input else 'output' self._extension.ExecuteJavaScript( """ chrome.audio.setActiveDevices( {'%s': ['%s']}, function() {window.__set_active_done = true;}); """ % (stream_type, node_id)) utils.wait_for_value( lambda: (self._extension.EvaluateJavaScript( "window.__set_active_done") != None), expected_value=True)