1# Copyright 2015 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. 4 5"""Handler for audio extension functionality.""" 6 7import logging 8 9from autotest_lib.client.bin import utils 10from autotest_lib.client.cros.multimedia import facade_resource 11 12class AudioExtensionHandlerError(Exception): 13 """Class for exceptions thrown from the AudioExtensionHandler""" 14 pass 15 16 17class AudioExtensionHandler(object): 18 """Wrapper around test extension that uses chrome.audio API to get audio 19 device information 20 """ 21 def __init__(self, extension): 22 """Initializes an AudioExtensionHandler. 23 24 @param extension: Extension got from telemetry chrome wrapper. 25 26 """ 27 self._extension = extension 28 self._check_api_available() 29 30 31 def _check_api_available(self): 32 """Checks chrome.audio is available. 33 34 @raises: AudioExtensionHandlerError if extension is not available. 35 36 """ 37 success = utils.wait_for_value( 38 lambda: (self._extension.EvaluateJavaScript( 39 "chrome.audio") != None), 40 expected_value=True) 41 if not success: 42 raise AudioExtensionHandlerError('chrome.audio is not available.') 43 44 45 @facade_resource.retry_chrome_call 46 def get_audio_api_availability(self): 47 """Gets whether the chrome.audio is available.""" 48 return self._extension.EvaluateJavaScript("chrome.audio") != None 49 50 51 @facade_resource.retry_chrome_call 52 def get_audio_devices(self, device_filter=None): 53 """Gets the audio device info from Chrome audio API. 54 55 @param device_filter: Filter for returned device nodes. 56 An optional dict that can have the following properties: 57 string array streamTypes 58 Restricts stream types that returned devices can have. 59 It should contain "INPUT" for result to include input 60 devices, and "OUTPUT" for results to include output devices. 61 If not set, returned devices will not be filtered by the 62 stream type. 63 64 boolean isActive 65 If true, only active devices will be included in the result. 66 If false, only inactive devices will be included in the 67 result. 68 69 The filter param defaults to {}, requests all available audio 70 devices. 71 72 @returns: An array of audioDeviceInfo. 73 Each audioDeviceInfo dict 74 contains these key-value pairs: 75 string id 76 The unique identifier of the audio device. 77 78 string stableDeviceId 79 The stable identifier of the audio device. 80 81 string streamType 82 "INPUT" if the device is an input audio device, 83 "OUTPUT" if the device is an output audio device. 84 85 string displayName 86 The user-friendly name (e.g. "Bose Amplifier"). 87 88 string deviceName 89 The devuce name 90 91 boolean isActive 92 True if this is the current active device. 93 94 boolean isMuted 95 True if this is muted. 96 97 long level 98 The output volume or input gain. 99 100 """ 101 def filter_to_str(device_filter): 102 """Converts python dict device filter to JS object string. 103 104 @param device_filter: Device filter dict. 105 106 @returns: Device filter as a srting representation of a 107 JavaScript object. 108 109 """ 110 return str(device_filter or {}).replace('True', 'true').replace( 111 'False', 'false') 112 113 self._extension.ExecuteJavaScript('window.__audio_devices = null;') 114 self._extension.ExecuteJavaScript( 115 "chrome.audio.getDevices(%s, function(devices) {" 116 "window.__audio_devices = devices;})" 117 % filter_to_str(device_filter)) 118 utils.wait_for_value( 119 lambda: (self._extension.EvaluateJavaScript( 120 "window.__audio_devices") != None), 121 expected_value=True) 122 return self._extension.EvaluateJavaScript("window.__audio_devices") 123 124 125 def _get_active_id_for_stream_type(self, stream_type): 126 """Gets active node id of the specified stream type. 127 128 Assume there is only one active node. 129 130 @param stream_type: 'INPUT' to get the active input device, 131 'OUTPUT' to get the active output device. 132 133 @returns: A string for the active device id. 134 135 @raises: AudioExtensionHandlerError if active id is not unique. 136 137 """ 138 nodes = self.get_audio_devices( 139 {'streamTypes': [stream_type], 'isActive': True}) 140 if len(nodes) != 1: 141 logging.error( 142 'Node info contains multiple active nodes: %s', nodes) 143 raise AudioExtensionHandlerError('Active id should be unique') 144 145 return nodes[0]['id'] 146 147 148 @facade_resource.retry_chrome_call 149 def set_active_volume(self, volume): 150 """Sets the active audio output volume using chrome.audio API. 151 152 This method also unmutes the node. 153 154 @param volume: Volume to set (0~100). 155 156 """ 157 output_id = self._get_active_id_for_stream_type('OUTPUT') 158 logging.debug('output_id: %s', output_id) 159 160 self.set_mute(False) 161 162 self._extension.ExecuteJavaScript('window.__set_volume_done = null;') 163 self._extension.ExecuteJavaScript( 164 """ 165 chrome.audio.setProperties( 166 '%s', 167 {level: %s}, 168 function() {window.__set_volume_done = true;}); 169 """ 170 % (output_id, volume)) 171 utils.wait_for_value( 172 lambda: (self._extension.EvaluateJavaScript( 173 "window.__set_volume_done") != None), 174 expected_value=True) 175 176 177 @facade_resource.retry_chrome_call 178 def set_active_input_gain(self, gain): 179 """Sets the active audio input gain using chrome.audio API. 180 181 @param gain: Gain to set (0~100). 182 183 """ 184 input_id = self._get_active_id_for_stream_type('INPUT') 185 logging.debug('input_id: %s', input_id) 186 187 self._extension.ExecuteJavaScript('window.__set_volume_done = null;') 188 self._extension.ExecuteJavaScript( 189 """ 190 chrome.audio.setProperties( 191 '%s', 192 {level: %s}, 193 function() {window.__set_volume_done = true;}); 194 """ 195 % (input_id, gain)) 196 utils.wait_for_value( 197 lambda: (self._extension.EvaluateJavaScript( 198 "window.__set_volume_done") != None), 199 expected_value=True) 200 201 202 @facade_resource.retry_chrome_call 203 def set_mute(self, mute): 204 """Mutes the audio output using chrome.audio API. 205 206 @param mute: True to mute. False otherwise. 207 208 """ 209 is_muted_string = 'true' if mute else 'false' 210 211 self._extension.ExecuteJavaScript('window.__set_mute_done = null;') 212 213 self._extension.ExecuteJavaScript( 214 """ 215 chrome.audio.setMute( 216 'OUTPUT', %s, 217 function() {window.__set_mute_done = true;}); 218 """ 219 % (is_muted_string)) 220 221 utils.wait_for_value( 222 lambda: (self._extension.EvaluateJavaScript( 223 "window.__set_mute_done") != None), 224 expected_value=True) 225 226 227 @facade_resource.retry_chrome_call 228 def get_mute(self): 229 """Determines whether audio output is muted. 230 231 @returns Whether audio output is muted. 232 233 """ 234 self._extension.ExecuteJavaScript('window.__output_muted = null;') 235 self._extension.ExecuteJavaScript( 236 "chrome.audio.getMute('OUTPUT', function(isMute) {" 237 "window.__output_muted = isMute;})") 238 utils.wait_for_value( 239 lambda: (self._extension.EvaluateJavaScript( 240 "window.__output_muted") != None), 241 expected_value=True) 242 return self._extension.EvaluateJavaScript("window.__output_muted") 243 244 245 @facade_resource.retry_chrome_call 246 def get_active_volume_mute(self): 247 """Gets the volume state of active audio output using chrome.audio API. 248 249 @param returns: A tuple (volume, mute), where volume is 0~100, and mute 250 is True if node is muted, False otherwise. 251 252 """ 253 nodes = self.get_audio_devices( 254 {'streamTypes': ['OUTPUT'], 'isActive': True}) 255 if len(nodes) != 1: 256 logging.error('Node info contains multiple active nodes: %s', nodes) 257 raise AudioExtensionHandlerError('Active id should be unique') 258 259 return (nodes[0]['level'], self.get_mute()) 260 261 262 @facade_resource.retry_chrome_call 263 def set_active_node_id(self, node_id): 264 """Sets the active node by node id. 265 266 The current active node will be disabled first if the new active node 267 is different from the current one. 268 269 @param node_id: Node id obtained from cras_utils.get_cras_nodes. 270 Chrome.audio also uses this id to specify input/output 271 nodes. 272 Note that node id returned by cras_utils.get_cras_nodes 273 is a number, while chrome.audio API expects a string. 274 275 @raises AudioExtensionHandlerError if there is no such id. 276 277 """ 278 nodes = self.get_audio_devices({}) 279 target_node = None 280 for node in nodes: 281 if node['id'] == str(node_id): 282 target_node = node 283 break 284 285 if not target_node: 286 logging.error('Node %s not found.', node_id) 287 raise AudioExtensionHandlerError('Node id not found') 288 289 if target_node['isActive']: 290 logging.debug('Node %s is already active.', node_id) 291 return 292 293 logging.debug('Setting active id to %s', node_id) 294 295 self._extension.ExecuteJavaScript('window.__set_active_done = null;') 296 297 is_input = target_node['streamType'] == 'INPUT' 298 stream_type = 'input' if is_input else 'output' 299 self._extension.ExecuteJavaScript( 300 """ 301 chrome.audio.setActiveDevices( 302 {'%s': ['%s']}, 303 function() {window.__set_active_done = true;}); 304 """ 305 % (stream_type, node_id)) 306 307 utils.wait_for_value( 308 lambda: (self._extension.EvaluateJavaScript( 309 "window.__set_active_done") != None), 310 expected_value=True) 311