1# Copyright 2024 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the 'License');
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an 'AS IS' BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""This module provides cras audio utilities."""
15
16import dbus
17import logging
18import re
19import subprocess
20
21from floss.pandora.floss import cmd_utils
22from floss.pandora.floss import utils
23
24_CRAS_TEST_CLIENT = '/usr/bin/cras_test_client'
25
26
27class CrasUtilsError(Exception):
28    """Error in CrasUtils."""
29    pass
30
31
32def playback(blocking=True, stdin=None, *args, **kargs):
33    """A helper function to execute the playback_cmd.
34
35    Args:
36        blocking: Blocks this call until playback finishes.
37        stdin: The standard input of playback process.
38        args: Args passed to playback_cmd.
39        kargs: Kargs passed to playback_cmd.
40
41    Returns:
42        The process running the playback command. Note that if the
43        blocking parameter is true, this will return a finished process.
44    """
45    process = cmd_utils.popen(playback_cmd(*args, **kargs), stdin=stdin)
46    if blocking:
47        cmd_utils.wait_and_check_returncode(process)
48    return process
49
50
51def capture(*args, **kargs):
52    """A helper function to execute the capture_cmd.
53
54    Args:
55        args: Args passed to capture_cmd.
56        kargs: Kargs passed to capture_cmd.
57    """
58    cmd_utils.execute(capture_cmd(*args, **kargs))
59
60
61def playback_cmd(playback_file, block_size=None, duration=None, pin_device=None, channels=2, rate=48000):
62    """Gets a command to playback a file with given settings.
63
64    Args:
65        playback_file: The name of the file to play. '-' indicates to playback raw audio from the stdin.
66        pin_device: The device id to playback on.
67        block_size: The number of frames per callback(dictates latency).
68        duration: Seconds to playback.
69        channels: Number of channels.
70        rate: The sampling rate.
71
72    Returns:
73        The command args put in a list of strings.
74    """
75    args = [_CRAS_TEST_CLIENT]
76    args += ['--playback_file', playback_file]
77    if pin_device is not None:
78        args += ['--pin_device', str(pin_device)]
79    if block_size is not None:
80        args += ['--block_size', str(block_size)]
81    if duration is not None:
82        args += ['--duration', str(duration)]
83    args += ['--num_channels', str(channels)]
84    args += ['--rate', str(rate)]
85    return args
86
87
88def capture_cmd(capture_file,
89                block_size=None,
90                duration=10,
91                sample_format='S16_LE',
92                pin_device=None,
93                channels=1,
94                rate=48000):
95    """Gets a command to capture the audio into the file with given settings.
96
97    Args:
98        capture_file: The name of file the audio to be stored in.
99        block_size: The number of frames per callback(dictates latency).
100        duration: Seconds to record. If it is None, duration is not set,
101                  and command will keep capturing audio until it is terminated.
102        sample_format: The sample format; possible choices: 'S16_LE', 'S24_LE',
103                       and 'S32_LE' default to S16_LE: signed 16 bits/sample, little endian.
104        pin_device: The device id to record from.
105        channels: Number of channels.
106        rate: The sampling rate.
107
108    Returns:
109        The command args put in a list of strings.
110    """
111    args = [_CRAS_TEST_CLIENT]
112    args += ['--capture_file', capture_file]
113    if pin_device is not None:
114        args += ['--pin_device', str(pin_device)]
115    if block_size is not None:
116        args += ['--block_size', str(block_size)]
117    if duration is not None:
118        args += ['--duration', str(duration)]
119    args += ['--num_channels', str(channels)]
120    args += ['--rate', str(rate)]
121    args += ['--format', str(sample_format)]
122    return args
123
124
125def listen_cmd(capture_file, block_size=None, duration=10, channels=1, rate=48000):
126    """Gets a command to listen on hotword and record audio into the file with given settings.
127
128    Args:
129        capture_file: The name of file the audio to be stored in.
130        block_size: The number of frames per callback(dictates latency).
131        duration: Seconds to record. If it is None, duration is not set, and command
132                  will keep capturing audio until it is terminated.
133        channels: Number of channels.
134        rate: The sampling rate.
135
136    Returns:
137        The command args put in a list of strings.
138    """
139    args = [_CRAS_TEST_CLIENT]
140    args += ['--listen_for_hotword', capture_file]
141    if block_size is not None:
142        args += ['--block_size', str(block_size)]
143    if duration is not None:
144        args += ['--duration', str(duration)]
145    args += ['--num_channels', str(channels)]
146    args += ['--rate', str(rate)]
147    return args
148
149
150def loopback(*args, **kargs):
151    """A helper function to execute loopback_cmd.
152
153    Args:
154        args: Args passed to loopback_cmd.
155        kargs: Kargs passed to loopback_cmd.
156    """
157    cmd_utils.execute(loopback_cmd(*args, **kargs))
158
159
160def loopback_cmd(output_file, duration=10, channels=2, rate=48000):
161    """Gets a command to record the loopback.
162
163    Args:
164        output_file: The name of the file the loopback to be stored in.
165        channels: The number of channels of the recorded audio.
166        duration: Seconds to record.
167        rate: The sampling rate.
168
169    Returns:
170        The command args put in a list of strings.
171    """
172    args = [_CRAS_TEST_CLIENT]
173    args += ['--loopback_file', output_file]
174    args += ['--duration_seconds', str(duration)]
175    args += ['--num_channels', str(channels)]
176    args += ['--rate', str(rate)]
177    return args
178
179
180def get_cras_nodes_cmd():
181    """Gets a command to query the nodes from Cras.
182
183    Returns:
184        The command to query nodes information from Cras using dbus-send.
185    """
186    return ('dbus-send --system --type=method_call --print-reply '
187            '--dest=org.chromium.cras /org/chromium/cras '
188            'org.chromium.cras.Control.GetNodes')
189
190
191def _dbus_uint64(x):
192    """Returns a UINT64 python-dbus object.
193
194    *Sometimes* python-dbus fails into the following cases:
195      - Attempt to convert a 64-bit integer into int32 and overflow
196      - Convert `dbus.UInt64(12345678900, variant_level=1)` (we usually get
197        this from some DBus calls) into VARIANT rather than UINT64
198
199    This function is a helper to avoid the above flakiness.
200    """
201    return dbus.types.UInt64(int(x), variant_level=0)
202
203
204def set_system_volume(volume):
205    """Sets the system volume.
206
207    Args:
208        volume: The system output vlume to be set(0 - 100).
209    """
210    get_cras_control_interface().SetOutputVolume(volume)
211
212
213def set_node_volume(node_id, volume):
214    """Sets the volume of the given output node.
215
216    Args:
217        node_id: The id of the output node to be set the volume.
218        volume: The volume to be set(0-100).
219    """
220    get_cras_control_interface().SetOutputNodeVolume(_dbus_uint64(node_id), volume)
221
222
223def get_cras_control_interface(private=False):
224    """Gets Cras DBus control interface.
225
226    Args:
227        private: Set to True to use a new instance for dbus.SystemBus instead of the shared instance.
228
229    Returns:
230        A dBus.Interface object with Cras Control interface.
231    """
232    bus = dbus.SystemBus(private=private)
233    cras_object = bus.get_object('org.chromium.cras', '/org/chromium/cras')
234    return dbus.Interface(cras_object, 'org.chromium.cras.Control')
235
236
237def get_cras_nodes():
238    """Gets nodes information from Cras.
239
240    Returns:
241        A dict containing information of each node.
242    """
243    return get_cras_control_interface().GetNodes()
244
245
246def get_selected_nodes():
247    """Gets selected output nodes and input nodes.
248
249    Returns:
250        A tuple (output_nodes, input_nodes) where each field is a list of selected
251        node IDs returned from Cras DBus API. Note that there may be multiple output/input
252        nodes being selected at the same time.
253    """
254    output_nodes = []
255    input_nodes = []
256    nodes = get_cras_nodes()
257    for node in nodes:
258        if node['Active']:
259            if node['IsInput']:
260                input_nodes.append(node['Id'])
261            else:
262                output_nodes.append(node['Id'])
263    return (output_nodes, input_nodes)
264
265
266def set_selected_output_node_volume(volume):
267    """Sets the selected output node volume.
268
269    Args:
270        volume: The volume to be set (0-100).
271    """
272    selected_output_node_ids, _ = get_selected_nodes()
273    for node_id in selected_output_node_ids:
274        set_node_volume(node_id, volume)
275
276
277def get_active_stream_count():
278    """Gets the number of active streams.
279
280    Returns:
281        The number of active streams.
282    """
283    return int(get_cras_control_interface().GetNumberOfActiveStreams())
284
285
286def set_system_mute(is_mute):
287    """Sets the system mute switch.
288
289    Args:
290        is_mute: Set True to mute the system playback.
291    """
292    get_cras_control_interface().SetOutputMute(is_mute)
293
294
295def set_capture_mute(is_mute):
296    """Sets the capture mute switch.
297
298    Args:
299        is_mute: Set True to mute the capture.
300    """
301    get_cras_control_interface().SetInputMute(is_mute)
302
303
304def node_type_is_plugged(node_type, nodes_info):
305    """Determines if there is any node of node_type plugged.
306
307    This method is used in the AudioLoopbackDongleLabel class, where the call is
308    executed on autotest server. Use get_cras_nodes instead if the call can be executed
309    on Cros device.
310
311    Since Cras only reports the plugged node in GetNodes, we can parse the return value
312    to see if there is any node with the given type. For example, if INTERNAL_MIC is of
313    intereset, the pattern we are looking for is:
314
315    dict entry(
316       string "Type"
317       variant             string "INTERNAL_MIC"
318    )
319
320    Args:
321        node_type: A str representing node type defined in CRAS_NODE_TYPES.
322        nodes_info: A str containing output of command get_nodes_cmd.
323
324    Returns:
325        True if there is any node of node_type plugged. False otherwise.
326    """
327    match = re.search(r'string "Type"\s+variant\s+string "%s"' % node_type, nodes_info)
328    return True if match else False
329
330
331# Cras node types reported from Cras DBus control API.
332CRAS_OUTPUT_NODE_TYPES = [
333    'HEADPHONE', 'INTERNAL_SPEAKER', 'HDMI', 'USB', 'BLUETOOTH', 'LINEOUT', 'UNKNOWN', 'ALSA_LOOPBACK'
334]
335CRAS_INPUT_NODE_TYPES = [
336    'MIC', 'INTERNAL_MIC', 'USB', 'BLUETOOTH', 'POST_DSP_LOOPBACK', 'POST_MIX_LOOPBACK', 'UNKNOWN', 'KEYBOARD_MIC',
337    'HOTWORD', 'FRONT_MIC', 'REAR_MIC', 'ECHO_REFERENCE'
338]
339CRAS_NODE_TYPES = CRAS_OUTPUT_NODE_TYPES + CRAS_INPUT_NODE_TYPES
340
341
342def get_filtered_node_types(callback):
343    """Returns the pair of filtered output node types and input node types.
344
345    Args:
346        callback: A callback function which takes a node as input parameter
347                  and filter the node based on its return value.
348
349    Returns:
350        A tuple (output_node_types, input_node_types) where each field is
351        a list of node types defined in CRAS_NODE_TYPES, and their 'attribute_name' is True.
352    """
353    output_node_types = []
354    input_node_types = []
355    nodes = get_cras_nodes()
356    for node in nodes:
357        if callback(node):
358            node_type = str(node['Type'])
359            if node_type not in CRAS_NODE_TYPES:
360                logging.warning('node type %s is not in known CRAS_NODE_TYPES', node_type)
361            if node['IsInput']:
362                input_node_types.append(node_type)
363            else:
364                output_node_types.append(node_type)
365    return (output_node_types, input_node_types)
366
367
368def get_selected_node_types():
369    """Returns the pair of active output node types and input node types.
370
371    Returns:
372         A tuple (output_node_types, input_node_types) where each field is a list
373         of selected node types defined in CRAS_NODE_TYPES.
374    """
375
376    def is_selected(node):
377        """Checks if a node is selected.
378
379        A node is selected if its Active attribute is True.
380
381        Returns:
382            True is a node is selected, False otherwise.
383        """
384        return node['Active']
385
386    return get_filtered_node_types(is_selected)
387
388
389def get_selected_output_device_name():
390    """Returns the device name of the active output node.
391
392    Returns:
393        device name string. E.g. mtk-rt5650: :0,0.
394    """
395    nodes = get_cras_nodes()
396    for node in nodes:
397        if node['Active'] and not node['IsInput']:
398            return node['DeviceName']
399    return None
400
401
402def get_selected_output_device_type():
403    """Returns the device type of the active output node.
404
405    Returns:
406        device type string. E.g. INTERNAL_SPEAKER.
407    """
408    nodes = get_cras_nodes()
409    for node in nodes:
410        if node['Active'] and not node['IsInput']:
411            return node['Type']
412    return None
413
414
415def set_single_selected_output_node(node_type):
416    """Sets one selected output node.
417
418    Note that Chrome UI uses SetActiveOutputNode of Cras DBus API to select one output node.
419
420    Args:
421        node_type: A node type.
422
423    Returns:
424        True if the output node type is found and set active.
425    """
426    nodes = get_cras_nodes()
427    for node in nodes:
428        if node['IsInput']:
429            continue
430        if node['Type'] == node_type:
431            set_active_output_node(node['Id'])
432            return True
433    return False
434
435
436def set_selected_output_nodes(types):
437    """Sets selected output node types.
438
439    Note that Chrome UI uses SetActiveOutputNode of Cras DBus API to select one
440    output node. Here we use add/remove active output node to support multiple nodes.
441
442    Args:
443        types: A list of output node types.
444    """
445    nodes = get_cras_nodes()
446    for node in nodes:
447        if node['IsInput']:
448            continue
449        if node['Type'] in types:
450            add_active_output_node(node['Id'])
451        elif node['Active']:
452            remove_active_output_node(node['Id'])
453
454
455def set_active_output_node(node_id):
456    """Sets one active output node.
457
458    Args:
459        node_id: node id.
460    """
461    get_cras_control_interface().SetActiveOutputNode(_dbus_uint64(node_id))
462
463
464def add_active_output_node(node_id):
465    """Adds an active output node.
466
467    Args:
468        node_id: node id.
469    """
470    get_cras_control_interface().AddActiveOutputNode(_dbus_uint64(node_id))
471
472
473def remove_active_output_node(node_id):
474    """Removes an active output node.
475
476    Args:
477        node_id: node id.
478    """
479    get_cras_control_interface().RemoveActiveOutputNode(_dbus_uint64(node_id))
480
481
482def get_node_id_from_node_type(node_type, is_input):
483    """Gets node id from node type.
484
485    Args:
486        node_type: A node type defined in CRAS_NODE_TYPES.
487        is_input: True if the node is input. False otherwise.
488
489    Returns:
490        A string for node id.
491
492    Raises:
493        CrasUtilsError: if unique node id can not be found.
494    """
495    nodes = get_cras_nodes()
496    find_ids = []
497    for node in nodes:
498        if node['Type'] == node_type and node['IsInput'] == is_input:
499            find_ids.append(node['Id'])
500    if len(find_ids) != 1:
501        raise CrasUtilsError('Can not find unique node id from node type %s' % node_type)
502    return find_ids[0]
503
504
505def get_device_id_of(node_id):
506    """Gets the device id of the node id.
507
508    The conversion logic is replicated from the CRAS's type definition at
509    third_party/adhd/cras/src/common/cras_types.h.
510
511    Args:
512        node_id: A string for node id.
513
514    Returns:
515        A string for device id.
516
517    Raises:
518        CrasUtilsError: if device id is invalid.
519    """
520    device_id = str(int(node_id) >> 32)
521    if device_id == "0":
522        raise CrasUtilsError('Got invalid device_id: 0')
523    return device_id
524
525
526def get_device_id_from_node_type(node_type, is_input):
527    """Gets device id from node type.
528
529    Args:
530        node_type: A node type defined in CRAS_NODE_TYPES.
531        is_input: True if the node is input. False otherwise.
532
533    Returns:
534        A string for device id.
535    """
536    node_id = get_node_id_from_node_type(node_type, is_input)
537    return get_device_id_of(node_id)
538
539
540def set_floss_enabled(enabled):
541    """Sets whether CRAS stack expects to use Floss.
542
543    Args:
544        enabled: True for Floss, False for Bluez.
545    """
546    get_cras_control_interface().SetFlossEnabled(enabled)
547
548
549class CrasTestClient(object):
550    """An object to perform cras_test_client functions."""
551
552    BLOCK_SIZE = None
553    PIN_DEVICE = None
554    SAMPLE_FORMAT = 'S16_LE'
555    DURATION = 10
556    CHANNELS = 2
557    RATE = 48000
558
559    def __init__(self):
560        self._proc = None
561        self._capturing_proc = None
562        self._playing_proc = None
563        self._capturing_msg = 'capturing audio file'
564        self._playing_msg = 'playing audio file'
565        self._wbs_cmd = '%s --set_wbs_enabled ' % _CRAS_TEST_CLIENT
566        self._enable_wbs_cmd = ('%s 1' % self._wbs_cmd).split()
567        self._disable_wbs_cmd = ('%s 0' % self._wbs_cmd).split()
568        self._info_cmd = [
569            _CRAS_TEST_CLIENT,
570        ]
571        self._select_input_cmd = '%s --select_input ' % _CRAS_TEST_CLIENT
572
573    def start_subprocess(self, proc, proc_cmd, filename, proc_msg):
574        """Starts a capture or play subprocess
575
576        Args:
577            proc: The process.
578            proc_cmd: The process command and its arguments.
579            filename: The file name to capture or play.
580            proc_msg: The message to display in logging.
581
582        Returns:
583            True if the process is started successfully
584        """
585        if proc is None:
586            try:
587                self._proc = subprocess.Popen(proc_cmd)
588                logging.debug('Start %s %s on the DUT', proc_msg, filename)
589            except Exception as e:
590                logging.error('Failed to popen: %s (%s)', proc_msg, e)
591                return False
592        else:
593            logging.error('cannot run the command twice: %s', proc_msg)
594            return False
595        return True
596
597    def stop_subprocess(self, proc, proc_msg):
598        """Stops a subprocess
599
600        Args:
601            proc: The process to stop.
602            proc_msg: The message to display in logging.
603
604        Returns:
605            True if the process is stopped successfully.
606        """
607        if proc is None:
608            logging.error('cannot run stop %s before starting it.', proc_msg)
609            return False
610
611        proc.terminate()
612        try:
613            utils.poll_for_condition(condition=lambda: proc.poll() is not None,
614                                     exception=CrasUtilsError,
615                                     timeout=10,
616                                     sleep_interval=0.5,
617                                     desc='Waiting for subprocess to terminate')
618        except Exception:
619            logging.warn('Killing subprocess due to timeout')
620            proc.kill()
621            proc.wait()
622
623        logging.debug('stop %s on the DUT', proc_msg)
624        return True
625
626    def start_capturing_subprocess(self,
627                                   capture_file,
628                                   block_size=BLOCK_SIZE,
629                                   duration=DURATION,
630                                   pin_device=PIN_DEVICE,
631                                   sample_format=SAMPLE_FORMAT,
632                                   channels=CHANNELS,
633                                   rate=RATE):
634        """Starts capturing in a subprocess.
635
636        Args:
637            capture_file: The name of file the audio to be stored in.
638            block_size: The number of frames per callback(dictates latency).
639            duration: Seconds to record. If it is None, duration is not set, and
640                      will keep capturing audio until terminated.
641            sample_format: The sample format.
642            pin_device: The device id to record from.
643            channels: Number of channels.
644            rate: The sampling rate.
645
646        Returns:
647            True if the process is started successfully.
648        """
649        proc_cmd = capture_cmd(capture_file,
650                               block_size=block_size,
651                               duration=duration,
652                               sample_format=sample_format,
653                               pin_device=pin_device,
654                               channels=channels,
655                               rate=rate)
656        result = self.start_subprocess(self._capturing_proc, proc_cmd, capture_file, self._capturing_msg)
657        if result:
658            self._capturing_proc = self._proc
659        return result
660
661    def stop_capturing_subprocess(self):
662        """Stops the capturing subprocess."""
663        result = self.stop_subprocess(self._capturing_proc, self._capturing_msg)
664        if result:
665            self._capturing_proc = None
666        return result
667
668    def start_playing_subprocess(self,
669                                 audio_file,
670                                 block_size=BLOCK_SIZE,
671                                 duration=DURATION,
672                                 pin_device=PIN_DEVICE,
673                                 channels=CHANNELS,
674                                 rate=RATE):
675        """Starts playing the audio file in a subprocess.
676
677        Args:
678            audio_file: The name of audio file to play.
679            block_size: The number of frames per callback(dictates latency).
680            duration: Seconds to play. If it is None, duration is not set, and
681                      will keep playing audio until terminated.
682            pin_device: The device id to play to.
683            channels: Number of channels.
684            rate: The sampling rate.
685
686        Returns:
687            True if the process is started successfully.
688        """
689        proc_cmd = playback_cmd(audio_file, block_size, duration, pin_device, channels, rate)
690        result = self.start_subprocess(self._playing_proc, proc_cmd, audio_file, self._playing_msg)
691        if result:
692            self._playing_proc = self._proc
693        return result
694
695    def stop_playing_subprocess(self):
696        """Stops the playing subprocess."""
697        result = self.stop_subprocess(self._playing_proc, self._playing_msg)
698        if result:
699            self._playing_proc = None
700        return result
701
702    def play(self,
703             audio_file,
704             block_size=BLOCK_SIZE,
705             duration=DURATION,
706             pin_device=PIN_DEVICE,
707             channels=CHANNELS,
708             rate=RATE):
709        """Plays the audio file.
710
711        This method will get blocked until it has completed playing back. If you
712        do not want to get blocked, use start_playing_subprocess() above instead.
713
714        Args:
715            audio_file: The name of audio file to play.
716            block_size: The number of frames per callback(dictates latency).
717            duration: Seconds to play. If it is None, duration is not set, and
718                      will keep playing audio until terminated.
719            pin_device: The device id to play to.
720            channels: Number of channels.
721            rate: The sampling rate.
722
723        Returns:
724            True if the process is started successfully.
725        """
726        proc_cmd = playback_cmd(audio_file, block_size, duration, pin_device, channels, rate)
727        try:
728            self._proc = subprocess.call(proc_cmd)
729            logging.debug('call "%s" on the DUT', proc_cmd)
730        except Exception as e:
731            logging.error('Failed to call: %s (%s)', proc_cmd, e)
732            return False
733        return True
734
735    def enable_wbs(self, value):
736        """Enables or disable wideband speech (wbs) per the value.
737
738        Args:
739            value: True to enable wbs.
740
741        Returns:
742            True if the operation succeeds.
743        """
744        cmd = self._enable_wbs_cmd if value else self._disable_wbs_cmd
745        logging.debug('call "%s" on the DUT', cmd)
746        if subprocess.call(cmd):
747            logging.error('Failed to call: %s (%s)', cmd)
748            return False
749        return True
750
751    def select_input_device(self, device_name):
752        """Selects the audio input device.
753
754        Args:
755            device_name: The name of the Bluetooth peer device.
756
757        Returns:
758            True if the operation succeeds.
759        """
760        logging.debug('to select input device for device_name: %s', device_name)
761        try:
762            info = subprocess.check_output(self._info_cmd)
763            logging.debug('info: %s', info)
764        except Exception as e:
765            logging.error('Failed to call: %s (%s)', self._info_cmd, e)
766            return False
767
768        flag_input_nodes = False
769        audio_input_node = None
770        for line in info.decode().splitlines():
771            if 'Input Nodes' in line:
772                flag_input_nodes = True
773            elif 'Attached clients' in line:
774                flag_input_nodes = False
775
776            if flag_input_nodes:
777                if device_name in line:
778                    audio_input_node = line.split()[1]
779                    logging.debug('%s', audio_input_node)
780                    break
781
782        if audio_input_node is None:
783            logging.error('Failed to find audio input node: %s', device_name)
784            return False
785
786        select_input_cmd = (self._select_input_cmd + audio_input_node).split()
787        if subprocess.call(select_input_cmd):
788            logging.error('Failed to call: %s (%s)', select_input_cmd)
789            return False
790
791        logging.debug('call "%s" on the DUT', select_input_cmd)
792        return True
793
794    def set_player_playback_status(self, status):
795        """Sets playback status for the registered media player.
796
797        Args:
798            status: Playback status in string.
799        """
800        try:
801            get_cras_control_interface().SetPlayerPlaybackStatus(status)
802        except Exception as e:
803            logging.error('Failed to set player playback status: %s', e)
804            return False
805
806        return True
807
808    def set_player_position(self, position):
809        """Sets media position for the registered media player.
810
811        Args:
812            position: Position in micro seconds.
813        """
814        try:
815            get_cras_control_interface().SetPlayerPosition(position)
816        except Exception as e:
817            logging.error('Failed to set player position: %s', e)
818            return False
819
820        return True
821
822    def set_player_metadata(self, metadata):
823        """Sets title, artist, and album for the registered media player.
824
825        Args:
826            metadata: Dictionary of media metadata.
827        """
828        try:
829            get_cras_control_interface().SetPlayerMetadata(metadata)
830        except Exception as e:
831            logging.error('Failed to set player metadata: %s', e)
832            return False
833
834        return True
835
836    def set_player_length(self, length):
837        """Sets metadata length for the registered media player.
838
839        Media length is a part of metadata information. However, without specify
840        its type to int64. dbus-python will guess the variant type to be int32 by
841        default. Separate it from the metadata function to help prepare the data
842        differently.
843
844        Args:
845            length: DBUS dictionary that contains a variant of int64.
846        """
847        try:
848            get_cras_control_interface().SetPlayerMetadata(length)
849        except Exception as e:
850            logging.error('Failed to set player length: %s', e)
851            return False
852        return True
853