• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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