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