Controlling Bluetooth Audio on Raspberry Pi


Update (April 21, 2020):
Volume control support added.


This post shows steps to control playback status of Bluetooth audio which is streamed from a connected phone (or any Bluetooth capable media player). The goal is to enable below.

  • Playback control (play, pause, next/previous song, and volume control)
  • Music information display in real time (song title, artist name, and album name)

 

Prerequisites

 

Steps
1. Enabling Bluetooth Audio (A2DP Sink)
1-1. If it’s not already done, please follow this post and enable Bluetooth Audio and connect a phone.

 

2. Implement Playback Control and Music Info Display [1]
2-1. Create a python script. This script prints playback status and music info when it’s updated. It also accept command input (i.e. “play”, “pause”, “next”, “prev”).

import dbus, dbus.mainloop.glib, sys
from gi.repository import GLib

def on_property_changed(interface, changed, invalidated):
    if interface != 'org.bluez.MediaPlayer1':
        return
    for prop, value in changed.items():
        if prop == 'Status':
            print('Playback Status: {}'.format(value))
        elif prop == 'Track':
            print('Music Info:')
            for key in ('Title', 'Artist', 'Album'):
                print('   {}: {}'.format(key, value.get(key, '')))

def on_playback_control(fd, condition):
    str = fd.readline()
    if str.startswith('play'):
        player_iface.Play()
    elif str.startswith('pause'):
        player_iface.Pause()
    elif str.startswith('next'):
        player_iface.Next()
    elif str.startswith('prev'):
        player_iface.Previous()
    elif str.startswith('vol'):
        vol = int(str.split()[1])
        if vol not in range(0, 128):
            print('Possible Values: 0-127')
            return True
        transport_prop_iface.Set(
                'org.bluez.MediaTransport1',
                'Volume',
                dbus.UInt16(vol))
    return True

if __name__ == '__main__':
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    bus = dbus.SystemBus()
    obj = bus.get_object('org.bluez', "/")
    mgr = dbus.Interface(obj, 'org.freedesktop.DBus.ObjectManager')
    player_iface = None
    transport_prop_iface = None
    for path, ifaces in mgr.GetManagedObjects().items():
        if 'org.bluez.MediaPlayer1' in ifaces:
            player_iface = dbus.Interface(
                    bus.get_object('org.bluez', path),
                    'org.bluez.MediaPlayer1')
        elif 'org.bluez.MediaTransport1' in ifaces:
            transport_prop_iface = dbus.Interface(
                    bus.get_object('org.bluez', path),
                    'org.freedesktop.DBus.Properties')
    if not player_iface:
        sys.exit('Error: Media Player not found.')
    if not transport_prop_iface:
        sys.exit('Error: DBus.Properties iface not found.')

    bus.add_signal_receiver(
            on_property_changed,
            bus_name='org.bluez',
            signal_name='PropertiesChanged',
            dbus_interface='org.freedesktop.DBus.Properties')
    GLib.io_add_watch(sys.stdin, GLib.IO_IN, on_playback_control)
    GLib.MainLoop().run()

2-2. Run the script.

python media_control.py

 

3. Test: Playback Status and Music Info
3-1. Start music by operating a media player on the phone. The script should print the info like below:

$ python media_control.py 
Playback Status: playing
Music Info:
   Title: Basket Case
   Artist: Green Day
   Album: Dookie (Explicit)

 

4. Test: Playback/Volume Control
4-1. Enter commands from keyboard. You should be able to control the music. In the example below, line 1, 3, 5 and 10 are commands entered.

pause
Playback Status: paused
play
Playback Status: playing
next
Music Info:
   Title: Sloppy Seconds
   Artist: Watsky
   Album: Cardboard Castles
vol 100

 

What’s Next?
If you are interested in showing album cover art images, please check this out.

 

References
[1] bluez/doc/media-api.txt

 

 

Sponsor Link

27 Comments

  1. im getting an error when trying to run script. “Traceback (most recent call last):
    File “/home/pi/media control.py”, line 32
    for path, ifaces in mgr.GetManagedObjects().iteritems():/home/pi/media control.py
    ^
    SyntaxError: invalid syntax”

  2. Hi! Excellent code you have here. I do appreciate it. I just wonder, is this a way I can do this with th GPIO of the RasPi instead of writing in the terminal? I’ll appreciate your answer.

    1. Hi Carlos, thank you for your comment! Of course you can control it with GPIO pins, with switches, or even via serial communication from another device. What are you planning to connect to the GPIO pins?

      1. I am sorry for the answer until know, I lost my previous PC and with it the web page, plus work I didn’t put to much attention until know. What I want to do is to make a control with the GPIO pins. For example if I send a True signal to the pin # 8 I want to play the music, if I send it to pin # 9 I want to pause it and so on for the other controls. How can I do it? I’ve been searching but I wasn’t able to locate any info on how to do that.

        Greetings!

  3. Hello… Can you help me in setting up this profile to play pause playback with gpio buttons

    How i want to do it is via bash command like “bluecontrol play” and it will start playing.. Right now i have to run script and then issue “play command”

    1. Hi Dheeraj, do you want to control play/pause status via GPIO or console command…?

      To control from GPIO pins, you can modify the script to monitor GPIO state and issue play/pause command based on the state, then run the script in background.

      If you want to control it by console command without the script, you can do with dbus-send command like below.

      Play:

      dbus-send --system --type=method_call --print-reply --dest=org.bluez \
      /org/bluez/hci0/dev_74_9E_AF_82_1F_8B/player0 \
      org.bluez.MediaPlayer1.Play

      Pause:

      dbus-send --system --type=method_call --print-reply --dest=org.bluez \
      /org/bluez/hci0/dev_74_9E_AF_82_1F_8B/player0 \
      org.bluez.MediaPlayer1.Pause

  4. I am using python 3 so got an error ‘dbus.Dictionary’ object has no attribute ‘iteritems’
    fixed it by replacing iteritems() with .items()
    on line 7 & 32

    1. Hi TrilbyRob, thank you for your comment. Since item() works on both python 2 and 3, I updated line 7 and 32 with item(). Thanks again for your feedback!

  5. Thank you very much for this python sample !
    It works great with python3.7 on a RPI3 !

    Do you know where I can find all the available player_iface methods ?

  6. Hi this is regarding the PI wifi hotspot (For some reason I cannot comment there)

    Having a strange issue:

    For 3-2, once it has rebooted the first time the hotspot will work and perform great. Although the moment you shut down the pi (or reboot for that matter) or remove the wifi dongle, you have to repeat that step.

    Wondering if you can help

  7. I tried to run it on a Pi 4 and receive an error that module “dbus” cannot be found:
    —————————————————————————–
    pi@raspberrypi:~/freisprech $ python
    Python 2.7.16 (default, Oct 10 2019, 22:02:15)
    [GCC 8.3.0] on linux2
    Type “help”, “copyright”, “credits” or “license” for more information.
    >>> import dbus
    Traceback (most recent call last):
    File “”, line 1, in
    File “dbus.py”, line 2, in
    import dbus.mainloop.glib
    ImportError: No module named mainloop.glib
    >>>
    —————————————————————————–

    Installing the module with “pip install dbus-python” doesn’t work either.

    Any ideas?

  8. Hi, thanks for the code as they really work well.
    Just want to have some clarification on theory behind these operations.
    Streaming music from handphone to raspberry pi is called as A2DP (Advanced Audio Distribution Profile)?
    And, the media control (play, pause, next, previous) instruction from raspberry pi to handphone is called as AVRCP (Audio Video Remote Control Profile)?
    May I know how can we control the volume as well?

    1. Hi Sharon,
      For your first two questions, yes, those are the profile names defined by Bluetooth SIG.
      Volume control can be done with MediaTransport1 interface (https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/media-api.txt). I updated the code above so please try it out. (e.g. “vol 100”)
      Please note that it try to control phone side volume and not every phone support this feature. At least it worked with my iPhone.
      If your phone doesn’t support it, you can control rpi’s output volume (e.g. with “alsamixer” command).

  9. Thanks for the code!
    It would be cool to connect REST API and have an http bluetooth player.
    Maybe someone knows about such an implementation?

    1. That’s a cool idea, Oleh.
      Few years back I was working on REST API on a small project. I think you can use django/flask & Nginx to create the APIs, then incorporate the above code in it.

  10. Thank you so much for the code! Two things. First thing, when I change the song, the ‘Music Info’ section is printed twice and the second one is empty. Then changing the song either takes a really long time to register or causes my phone to disconnect. Any idea what is going on?

    Second thing: Could this code be set up as thread?

  11. I am getting these errors on exit:

    Traceback (most recent call last):
    File “media_control.py”, line 63, in
    GLib.MainLoop().run()
    File “/usr/lib/python2.7/dist-packages/gi/overrides/GLib.py”, line 498, in run
    super(MainLoop, self).run()
    File “/usr/lib/python2.7/contextlib.py”, line 24, in __exit__
    self.gen.next()
    File “/usr/lib/python2.7/dist-packages/gi/_ossighelper.py”, line 251, in register_sigint_fallback
    signal.default_int_handler(signal.SIGINT, None)

  12. Thank you so much for the guide its been incredibly helpful! I’ve gone through the steps to make bluetooth connectivity headless, and this code works perfect in terminal for controlling it. I’m trying to integrate it into a GUI i’ve made with PySide2. The issue i’m getting is that if this code is inserted into an object it gets stuck in its own loop and never loads the rest of the GUI.

    Is there a way to integrate this so that QPushButtons in Pyside2/PyQt5 can trigger the commands in [on_playback_control] and have it work inside the main programs loop rather than its own? I’d love to have the the album track name pop up as it does with this too and show album art like you have in the later guide. Can happily share my code if you would like to see 🙂

    I’m completely new to programming and have no idea what gLib and dbus is/does! I think its to do with low-level hardware communication but really have no clue!

  13. Hi There,
    For some reason I just get Error: Media Player not found. every time I run this script. (this happens while I am playing music and can hear it through the raspberrypi speaker)

    Any advice?

  14. Hey man I tried this and am getting a few errors (m doing it first time so maybe it’s my own stupid mistakes) can you send your email id so that I can attach the screenshots

  15. Hello, it works great and patches to the console.
    May know how to do print(‘Music Info:’) in KODI

  16. Hi, works great, nice project.

    Can’t figure out how to run it for KODI?
    Tried to run read through
    msg = (‘ {}: {}’.format(key, value.get(key, ”)))
    xbmcgui.Window(10000).setProperty(‘Track’, str(msg)).
    Any ideas how to adapt?

Comments are closed.