Playing bluetooth music sources through linux

The bluetooth on my RPi seems to be dead… I did not use it since a while, so I don’t know what happened there, I’ll look into it.
But!
This one seems to work on my laptop. The original recype is for RPi with Pipewire, my laptop has now Debian with PW as sound server.
So slightly modified the recype.
If you have PW in your Linux box, this should work.
For me the dependencies were already installed, I did not need to apt install anything.

So what I did now (everything done as root, so append “sudo” before the commands if you do it from your regular user account):

  1. put this into /opt/pw-speaker, for example
    nano /opt/pw-speaker
#!/usr/bin/python3
# SPDX-License-Identifier: LGPL-2.1-or-later

import dbus
import dbus.service
import dbus.mainloop.glib
from gi.repository import GLib

BUS_NAME = 'org.bluez'
AGENT_INTERFACE = 'org.bluez.Agent1'
AGENT_PATH = "/speaker/agent"

A2DP = '0000110d-0000-1000-8000-00805f9b34fb'
AVRCP = '0000110e-0000-1000-8000-00805f9b34fb'

bus = None


class Rejected(dbus.DBusException):
    _dbus_error_name = "org.bluez.Error.Rejected"


class Agent(dbus.service.Object):
    exit_on_release = True

    def set_exit_on_release(self, exit_on_release):
        self.exit_on_release = exit_on_release

    @dbus.service.method(AGENT_INTERFACE,
                         in_signature="", out_signature="")
    def Release(self):
        print("Release")
        if self.exit_on_release:
            mainloop.quit()

    @dbus.service.method(AGENT_INTERFACE,
                         in_signature="os", out_signature="")
    def AuthorizeService(self, device, uuid):
        # Always authorize A2DP and AVRCP connection
        if uuid in [A2DP, AVRCP]:
            print("AuthorizeService (%s, %s)" % (device, uuid))
            return
        else:
            print("Service rejected (%s, %s)" % (device, uuid))
        raise Rejected("Connection rejected by user")

    @dbus.service.method(AGENT_INTERFACE,
                         in_signature="", out_signature="")
    def Cancel(self):
        print("Cancel")


if __name__ == '__main__':
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

    bus = dbus.SystemBus()

    agent = Agent(bus, AGENT_PATH)

    mainloop = GLib.MainLoop()

    # By default Bluetooth adapter is not discoverable and there's
    # a 3 min timeout
    # Set it as always discoverable
    adapter = dbus.Interface(bus.get_object(BUS_NAME, "/org/bluez/hci0"),
                             "org.freedesktop.DBus.Properties")
    adapter.Set("org.bluez.Adapter1", "DiscoverableTimeout", dbus.UInt32(0))
    adapter.Set("org.bluez.Adapter1", "Discoverable", True)

    print("RPi speaker discoverable")

    # As the RPi speaker will not have any interface, create a pairing
    # agent with NoInputNoOutput capability
    obj = bus.get_object(BUS_NAME, "/org/bluez")
    manager = dbus.Interface(obj, "org.bluez.AgentManager1")
    manager.RegisterAgent(AGENT_PATH, "NoInputNoOutput")

    print("Agent registered")

    manager.RequestDefaultAgent(AGENT_PATH)

    mainloop.run()

Don’t forget to chmod +x:

chmod +x /opt/pw-speaker

Create a systemd service as follows:

Make a file with this content in
/etc/systemd/user/speaker-agent.service

[Unit]
Description=Bluetooth speaker agent

[Service]
ExecStart=/opt/pw-speaker

[Install]
WantedBy=default.target

Make systemd know about changes, and enable this service for all users:

systemctl --global daemon-reload
systemctl --global enable speaker-agent
sed -i 's/#JustWorksRepairing.*/JustWorksRepairing = always/' /etc/bluetooth/main.conf

I rebooted, logged in, and bingo.
My phone connects to my laptop, and plays audio on the laptops speaker.
I think the sound would be routed to the headphone output when attached, I still need to confirm, this really works.

Let me know, how it goes for you :wink:

3 Likes