• 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_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