Home | History | Annotate | Download | only in multimedia
      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 
      7 import logging
      8 
      9 from autotest_lib.client.bin import utils
     10 from autotest_lib.client.cros.multimedia import facade_resource
     11 
     12 class AudioExtensionHandlerError(Exception):
     13     """Class for exceptions thrown from the AudioExtensionHandler"""
     14     pass
     15 
     16 
     17 class 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