普通文本  |  280行  |  9.79 KB

# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Handler for audio extension functionality."""

import logging

from autotest_lib.client.bin import utils
from autotest_lib.client.cros.multimedia import facade_resource

class AudioExtensionHandlerError(Exception):
    """Class for exceptions thrown from the AudioExtensionHandler"""
    pass


class AudioExtensionHandler(object):
    """Wrapper around test extension that uses chrome.audio API to get audio
    device information
    """
    def __init__(self, extension):
        """Initializes an AudioExtensionHandler.

        @param extension: Extension got from telemetry chrome wrapper.

        """
        self._extension = extension
        self._check_api_available()


    def _check_api_available(self):
        """Checks chrome.audio is available.

        @raises: AudioExtensionHandlerError if extension is not available.

        """
        success = utils.wait_for_value(
                lambda: (self._extension.EvaluateJavaScript(
                         "chrome.audio") != None),
                expected_value=True)
        if not success:
            raise AudioExtensionHandlerError('chrome.audio is not available.')


    @facade_resource.retry_chrome_call
    def get_audio_devices(self, device_filter=None):
        """Gets the audio device info from Chrome audio API.

        @param device_filter: Filter for returned device nodes.
            An optional dict that can have the following properties:
                string array streamTypes
                    Restricts stream types that returned devices can have.
                    It should contain "INPUT" for result to include input
                    devices, and "OUTPUT" for results to include output devices.
                    If not set, returned devices will not be filtered by the
                    stream type.

                boolean isActive
                   If true, only active devices will be included in the result.
                   If false, only inactive devices will be included in the
                   result.

            The filter param defaults to {}, requests all available audio
            devices.

        @returns: An array of audioDeviceInfo.
                  Each audioDeviceInfo dict
                  contains these key-value pairs:
                     string  id
                         The unique identifier of the audio device.

                     string stableDeviceId
                         The stable identifier of the audio device.

                     string  streamType
                         "INPUT" if the device is an input audio device,
                         "OUTPUT" if the device is an output audio device.

                     string displayName
                         The user-friendly name (e.g. "Bose Amplifier").

                     string deviceName
                         The devuce name

                     boolean isActive
                         True if this is the current active device.

                     boolean isMuted
                         True if this is muted.

                     long level
                         The output volume or input gain.

        """
        def filter_to_str(device_filter):
            """Converts python dict device filter to JS object string.

            @param device_filter: Device filter dict.

            @returns: Device filter as a srting representation of a
                      JavaScript object.

            """
            return str(device_filter or {}).replace('True', 'true').replace(
                        'False', 'false')

        self._extension.ExecuteJavaScript('window.__audio_devices = null;')
        self._extension.ExecuteJavaScript(
                "chrome.audio.getDevices(%s, function(devices) {"
                "window.__audio_devices = devices;})"
                % filter_to_str(device_filter))
        utils.wait_for_value(
                lambda: (self._extension.EvaluateJavaScript(
                         "window.__audio_devices") != None),
                expected_value=True)
        return self._extension.EvaluateJavaScript("window.__audio_devices")


    def _get_active_id_for_stream_type(self, stream_type):
        """Gets active node id of the specified stream type.

        Assume there is only one active node.

        @param stream_type: 'INPUT' to get the active input device,
                            'OUTPUT' to get the active output device.

        @returns: A string for the active device id.

        @raises: AudioExtensionHandlerError if active id is not unique.

        """
        nodes = self.get_audio_devices(
            {'streamTypes': [stream_type], 'isActive': True})
        if len(nodes) != 1:
            logging.error(
                    'Node info contains multiple active nodes: %s', nodes)
            raise AudioExtensionHandlerError('Active id should be unique')

        return nodes[0]['id']


    @facade_resource.retry_chrome_call
    def set_active_volume(self, volume):
        """Sets the active audio output volume using chrome.audio API.

        This method also unmutes the node.

        @param volume: Volume to set (0~100).

        """
        output_id = self._get_active_id_for_stream_type('OUTPUT')
        logging.debug('output_id: %s', output_id)

        self.set_mute(False)

        self._extension.ExecuteJavaScript('window.__set_volume_done = null;')
        self._extension.ExecuteJavaScript(
                """
                chrome.audio.setProperties(
                    '%s',
                    {level: %s},
                    function() {window.__set_volume_done = true;});
                """
                % (output_id, volume))
        utils.wait_for_value(
                lambda: (self._extension.EvaluateJavaScript(
                         "window.__set_volume_done") != None),
                expected_value=True)


    @facade_resource.retry_chrome_call
    def set_mute(self, mute):
        """Mutes the audio output using chrome.audio API.

        @param mute: True to mute. False otherwise.

        """
        is_muted_string = 'true' if mute else 'false'

        self._extension.ExecuteJavaScript('window.__set_mute_done = null;')

        self._extension.ExecuteJavaScript(
                """
                chrome.audio.setMute(
                    'OUTPUT', %s,
                    function() {window.__set_mute_done = true;});
                """
                % (is_muted_string))

        utils.wait_for_value(
                lambda: (self._extension.EvaluateJavaScript(
                         "window.__set_mute_done") != None),
                expected_value=True)


    @facade_resource.retry_chrome_call
    def get_mute(self):
        """Determines whether audio output is muted.

        @returns Whether audio output is muted.

        """
        self._extension.ExecuteJavaScript('window.__output_muted = null;')
        self._extension.ExecuteJavaScript(
                "chrome.audio.getMute('OUTPUT', function(isMute) {"
                "window.__output_muted = isMute;})")
        utils.wait_for_value(
                lambda: (self._extension.EvaluateJavaScript(
                         "window.__output_muted") != None),
                expected_value=True)
        return self._extension.EvaluateJavaScript("window.__output_muted")


    @facade_resource.retry_chrome_call
    def get_active_volume_mute(self):
        """Gets the volume state of active audio output using chrome.audio API.

        @param returns: A tuple (volume, mute), where volume is 0~100, and mute
                        is True if node is muted, False otherwise.

        """
        nodes = self.get_audio_devices(
            {'streamTypes': ['OUTPUT'], 'isActive': True})
        if len(nodes) != 1:
            logging.error('Node info contains multiple active nodes: %s', nodes)
            raise AudioExtensionHandlerError('Active id should be unique')

        return (nodes[0]['level'], self.get_mute())


    @facade_resource.retry_chrome_call
    def set_active_node_id(self, node_id):
        """Sets the active node by node id.

        The current active node will be disabled first if the new active node
        is different from the current one.

        @param node_id: Node id obtained from cras_utils.get_cras_nodes.
                        Chrome.audio also uses this id to specify input/output
                        nodes.
                        Note that node id returned by cras_utils.get_cras_nodes
                        is a number, while chrome.audio API expects a string.

        @raises AudioExtensionHandlerError if there is no such id.

        """
        nodes = self.get_audio_devices({})
        target_node = None
        for node in nodes:
            if node['id'] == str(node_id):
                target_node = node
                break

        if not target_node:
            logging.error('Node %s not found.', node_id)
            raise AudioExtensionHandlerError('Node id not found')

        if target_node['isActive']:
            logging.debug('Node %s is already active.', node_id)
            return

        logging.debug('Setting active id to %s', node_id)

        self._extension.ExecuteJavaScript('window.__set_active_done = null;')

        is_input = target_node['streamType'] == 'INPUT'
        stream_type = 'input' if is_input else 'output'
        self._extension.ExecuteJavaScript(
                """
                chrome.audio.setActiveDevices(
                    {'%s': ['%s']},
                    function() {window.__set_active_done = true;});
                """
                % (stream_type, node_id))

        utils.wait_for_value(
                lambda: (self._extension.EvaluateJavaScript(
                         "window.__set_active_done") != None),
                expected_value=True)