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    pass
14
15
16class AudioExtensionHandler(object):
17    def __init__(self, extension):
18        """Initializes an AudioExtensionHandler.
19
20        @param extension: Extension got from telemetry chrome wrapper.
21
22        """
23        self._extension = extension
24        self._check_api_available()
25
26
27    def _check_api_available(self):
28        """Checks chrome.audio is available.
29
30        @raises: AudioExtensionHandlerError if extension is not available.
31
32        """
33        success = utils.wait_for_value(
34                lambda: (self._extension.EvaluateJavaScript(
35                         "chrome.audio") != None),
36                expected_value=True)
37        if not success:
38            raise AudioExtensionHandlerError('chrome.audio is not available.')
39
40
41    @facade_resource.retry_chrome_call
42    def get_audio_info(self):
43        """Gets the audio info from Chrome audio API.
44
45        @returns: An array of [outputInfo, inputInfo].
46                  outputInfo is an array of output node info dicts. Each dict
47                  contains these key-value pairs:
48                     string  id
49                         The unique identifier of the audio output device.
50
51                     string  name
52                         The user-friendly name (e.g. "Bose Amplifier").
53
54                     boolean isActive
55                         True if this is the current active device.
56
57                     boolean isMuted
58                         True if this is muted.
59
60                     double  volume
61                         The output volume ranging from 0.0 to 100.0.
62
63                  inputInfo is an arrya of input node info dicts. Each dict
64                  contains these key-value pairs:
65                     string  id
66                         The unique identifier of the audio input device.
67
68                     string  name
69                         The user-friendly name (e.g. "USB Microphone").
70
71                     boolean isActive
72                         True if this is the current active device.
73
74                     boolean isMuted
75                         True if this is muted.
76
77                     double  gain
78                         The input gain ranging from 0.0 to 100.0.
79
80        """
81        self._extension.ExecuteJavaScript('window.__audio_info = null;')
82        self._extension.ExecuteJavaScript(
83                "chrome.audio.getInfo(function(outputInfo, inputInfo) {"
84                "window.__audio_info = [outputInfo, inputInfo];})")
85        utils.wait_for_value(
86                lambda: (self._extension.EvaluateJavaScript(
87                         "window.__audio_info") != None),
88                expected_value=True)
89        return self._extension.EvaluateJavaScript("window.__audio_info")
90
91
92    def _get_active_id(self):
93        """Gets active output and input node id.
94
95        Assume there is only one active output node and one active input node.
96
97        @returns: (output_id, input_id) where output_id and input_id are
98                  strings for active node id.
99
100        """
101        output_nodes, input_nodes = self.get_audio_info()
102
103        return (self._get_active_id_from_nodes(output_nodes),
104                self._get_active_id_from_nodes(input_nodes))
105
106
107    def _get_active_id_from_nodes(self, nodes):
108        """Gets active node id from nodes.
109
110        Assume there is only one active node.
111
112        @param nodes: A list of input/output nodes got from get_audio_info().
113
114        @returns: node['id'] where node['isActive'] is True.
115
116        @raises: AudioExtensionHandlerError if active id is not unique.
117
118        """
119        active_ids = [x['id'] for x in nodes if x['isActive']]
120        if len(active_ids) != 1:
121            logging.error(
122                    'Node info contains multiple active nodes: %s', nodes)
123            raise AudioExtensionHandlerError(
124                    'Active id should be unique')
125
126        return active_ids[0]
127
128
129
130    @facade_resource.retry_chrome_call
131    def set_active_volume(self, volume):
132        """Sets the active audio output volume using chrome.audio API.
133
134        This method also unmutes the node.
135
136        @param volume: Volume to set (0~100).
137
138        """
139        output_id, _ = self._get_active_id()
140        logging.debug('output_id: %s', output_id)
141
142        self._extension.ExecuteJavaScript('window.__set_volume_done = null;')
143
144        self._extension.ExecuteJavaScript(
145                """
146                chrome.audio.setProperties(
147                    '%s',
148                    {isMuted: false, volume: %s},
149                    function() {window.__set_volume_done = true;});
150                """
151                % (output_id, volume))
152
153        utils.wait_for_value(
154                lambda: (self._extension.EvaluateJavaScript(
155                         "window.__set_volume_done") != None),
156                expected_value=True)
157
158
159    @facade_resource.retry_chrome_call
160    def set_mute(self, mute):
161        """Mutes the active audio output using chrome.audio API.
162
163        @param mute: True to mute. False otherwise.
164
165        """
166        output_id, _ = self._get_active_id()
167        logging.debug('output_id: %s', output_id)
168
169        is_muted_string = 'true' if mute else 'false'
170
171        self._extension.ExecuteJavaScript('window.__set_mute_done = null;')
172
173        self._extension.ExecuteJavaScript(
174                """
175                chrome.audio.setProperties(
176                    '%s',
177                    {isMuted: %s},
178                    function() {window.__set_mute_done = true;});
179                """
180                % (output_id, is_muted_string))
181
182        utils.wait_for_value(
183                lambda: (self._extension.EvaluateJavaScript(
184                         "window.__set_mute_done") != None),
185                expected_value=True)
186
187
188    @facade_resource.retry_chrome_call
189    def get_active_volume_mute(self):
190        """Gets the volume state of active audio output using chrome.audio API.
191
192        @param returns: A tuple (volume, mute), where volume is 0~100, and mute
193                        is True if node is muted, False otherwise.
194
195        """
196        output_nodes, _ = self.get_audio_info()
197        active_id = self._get_active_id_from_nodes(output_nodes)
198        for node in output_nodes:
199            if node['id'] == active_id:
200                return (node['volume'], node['isMuted'])
201
202
203    @facade_resource.retry_chrome_call
204    def set_active_node_id(self, node_id):
205        """Sets the active node by node id.
206
207        The current active node will be disabled first if the new active node
208        is different from the current one.
209
210        @param node_id: The node id obtained from cras_utils.get_cras_nodes.
211                        Chrome.audio also uses this id to specify input/output
212                        nodes.
213
214        @raises AudioExtensionHandlerError if there is no such id.
215
216        """
217        if node_id in self._get_active_id():
218            logging.debug('Node %s is already active.', node_id)
219            return
220
221        logging.debug('Setting active id to %s', node_id)
222
223        self._extension.ExecuteJavaScript('window.__set_active_done = null;')
224
225        self._extension.ExecuteJavaScript(
226                """
227                chrome.audio.setActiveDevices(
228                    ['%s'],
229                    function() {window.__set_active_done = true;});
230                """
231                % (node_id))
232
233        utils.wait_for_value(
234                lambda: (self._extension.EvaluateJavaScript(
235                         "window.__set_active_done") != None),
236                expected_value=True)
237