Skip to content

Instantly share code, notes, and snippets.

@mill1000
Last active February 8, 2024 16:38
Show Gist options
  • Save mill1000/74c7473ee3b4a5b13f6325e9994ff84c to your computer and use it in GitHub Desktop.
Save mill1000/74c7473ee3b4a5b13f6325e9994ff84c to your computer and use it in GitHub Desktop.
Headless A2DP Audio Streaming on Raspbian Stretch

About

This gist will show how to setup Raspbian Stretch as a headless Bluetooth A2DP audio sink. This will allow your phone, laptop or other Bluetooth device to play audio wirelessly through a Rasperry Pi.

Motivation

A quick search will turn up a plethora of tutorials on setting up A2DP on the Raspberry Pi. However, I felt this gist was necessary because this solution is:

  • Automatic & Headless - Once setup, the system is entirely automatic. No user iteration is required to pair, connect or start playback. Therefore the Raspberry Pi can be run headless.
  • Simple - This solution has few dependencies, readily available packages and minimal configuration.
  • Up to date - As of December 2017. Written for Raspbian Stretch & Bluez 5.43

Prerequisites

  • Raspbian Stretch - I used the Lite version as this is a headless setup. See the official guide if you need help.
  • Bluez-alsa - Available in the Raspbian package repo. This software allows us to stream A2DP audio over Bluetooth without PulseAudio.
  • Raspberry Pi with Bluetooth - The Raspberry Pi 3 has integrated Bluetooth, however there is a known bug when the WiFi is used simultaneously. Cheap USB Bluetooth dongles work equally well.

Disabling Integrated Bluetooth

If you are using a separate USB Bluetooth dongle, disable the integrated Bluetooth to prevent conflicts.

To disable the integrated Bluetooth add the following

# Disable onboard Bluetooth
dtoverlay=pi3-disable-bt

to /boot/config.txt and execute the following command

sudo systemctl disable hciuart.service

Initial Setup

First make sure the system is up to date using the following commands.

sudo apt-get update
sudo apt-get upgrade

Then reboot the Pi to ensure the latest kernel is loaded.

Now install the required packages.

sudo apt-get install bluealsa python-dbus

Make Bluetooth Discoverable

Normally a Bluetooth device is only discoverable for a limited amount of time. Since this is a headless setup we want the device to always be discoverable.

  1. Set the DiscoverableTimeout in /etc/bluetooth/main.conf to 0
# How long to stay in discoverable mode before going back to non-discoverable
# The value is in seconds. Default is 180, i.e. 3 minutes.
# 0 = disable timer, i.e. stay discoverable forever
DiscoverableTimeout = 0
  1. Enable discovery on the Bluetooth controller
sudo bluetoothctl
power on
discoverable on
exit

Install The A2DP Bluetooth Agent

A Bluetooth agent is a piece of software that handles pairing and authorization of Bluetooth devices. The following agent allows the Raspberry Pi to automatically pair and accept A2DP connections from Bluetooth devices. All other Bluetooth services are rejected.

Copy the included file a2dp-agent to /usr/local/bin and make the file executable with

sudo chmod +x /usr/local/bin/a2dp-agent

Testing The Agent

Before continuing, verify that the agent is functional. The Raspberry Pi should be discoverable, pairable and recognized as an audio device.

Note: At this point the device will not output any audio. This step is only to verify the Bluetooth is discoverable and bindable.

  1. Manually run the agent by executing
sudo /usr/local/bin/a2dp-agent
  1. Attempt to pair and connect with the Raspberry Pi using your phone or computer.
  2. The agent should output the accepted and rejected Bluetooth UUIDs
A2DP Agent Registered
AuthorizeService (/org/bluez/hci0/dev_94_01_C2_47_01_AA, 0000111E-0000-1000-8000-00805F9B34FB)
Rejecting non-A2DP Service
AuthorizeService (/org/bluez/hci0/dev_94_01_C2_47_01_AA, 0000110d-0000-1000-8000-00805f9b34fb)
Authorized A2DP Service
AuthorizeService (/org/bluez/hci0/dev_94_01_C2_47_01_AA, 0000111E-0000-1000-8000-00805F9B34FB)
Rejecting non-A2DP Service

If the Raspberry Pi is not recognized as a audio device, ensure that the bluealsa package was installed as part of the Initial Setup

Install The A2DP Bluetooth Agent As A Service

To make the A2DP Bluetooth Agent run on boot copy the included file bt-agent-a2dp.service to /etc/systemd/system. Now run the following command to enable the A2DP Agent service

sudo systemctl enable bt-agent-a2dp.service

Thanks to @matthijskooijman for fixing up some issues in the Bluetooth Agent service.

Bluetooth devices should now be able to discover, pair and connect to the Raspberry Pi without any user intervention.

Testing Audio Playback

Now that Bluetooth devices can pair and connect with the Raspberry Pi we can test the audio playback.

The tool bluealsa-aplay is used to forward audio from the Bluetooth device to the ALSA output device (sound card).

Execute the following command to accept A2DP audio from any connected Bluetooth device.

bluealsa-aplay -vv 00:00:00:00:00:00

Play a song on the Bluetooth device and the Raspberry Pi should output audio on either the headphone jack or the HDMI port. See this guide for configuring the audio output device of the Raspberry Pi.

Install The Audio Playback As A Service

To make the audio playback run on boot copy the included file a2dp-playback.service to /etc/systemd/system. Now run the following command to enable A2DP Playback service

sudo systemctl enable a2dp-playback.service

Reboot and enjoy!

Low Volume Output

If you are experiencing low volume output, run alsamixer and increase the volume of the Pi's soundcard.

#!/usr/bin/python
from __future__ import absolute_import, print_function, unicode_literals
import sys
import dbus
import dbus.service
import dbus.mainloop.glib
try:
from gi.repository import GObject
except ImportError:
import gobject as GObject
AGENT_INTERFACE = "org.bluez.Agent1"
AGENT_PATH = "/test/agent"
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):
print("AuthorizeService (%s, %s)" % (device, uuid))
if uuid == "0000110d-0000-1000-8000-00805f9b34fb":
print("Authorized A2DP Service")
return
print("Rejecting non-A2DP Service")
raise Rejected("Connection rejected")
@dbus.service.method(AGENT_INTERFACE,
in_signature="o", out_signature="s")
def RequestPinCode(self, device):
print("RequestPinCode (%s)" % (device))
return "0000"
@dbus.service.method(AGENT_INTERFACE,
in_signature="o", out_signature="u")
def RequestPasskey(self, device):
print("RequestPasskey (%s)" % (device))
return dbus.UInt32("password")
@dbus.service.method(AGENT_INTERFACE,
in_signature="ouq", out_signature="")
def DisplayPasskey(self, device, passkey, entered):
print("DisplayPasskey (%s, %06u entered %u)" %
(device, passkey, entered))
@dbus.service.method(AGENT_INTERFACE,
in_signature="os", out_signature="")
def DisplayPinCode(self, device, pincode):
print("DisplayPinCode (%s, %s)" % (device, pincode))
@dbus.service.method(AGENT_INTERFACE,
in_signature="ou", out_signature="")
def RequestConfirmation(self, device, passkey):
print("RequestConfirmation (%s, %06d)" % (device, passkey))
return
@dbus.service.method(AGENT_INTERFACE,
in_signature="o", out_signature="")
def RequestAuthorization(self, device):
print("RequestAuthorization (%s)" % (device))
raise Rejected("Pairing rejected")
@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)
obj = bus.get_object("org.bluez", "/org/bluez");
manager = dbus.Interface(obj, "org.bluez.AgentManager1")
manager.RegisterAgent(AGENT_PATH, "NoInputNoOutput")
print("A2DP Agent Registered")
manager.RequestDefaultAgent(AGENT_PATH)
mainloop = GObject.MainLoop()
mainloop.run()
[Unit]
Description=A2DP Playback
After=bluealsa.service syslog.service
Requires=bluealsa.service
[Service]
ExecStartPre=/bin/sleep 3
ExecStart=/usr/bin/bluealsa-aplay --profile-a2dp 00:00:00:00:00:00
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=A2DP-Playback
User=pi
[Install]
WantedBy=multi-user.target
[Unit]
Description=A2DP Bluetooth Agent
After=bluetooth.service
Wants=bluetooth.service
[Service]
ExecStartPre=/bin/sh -c "echo discoverable on | bluetoothctl"
ExecStart=/usr/bin/python -u /usr/local/bin/a2dp-agent
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=A2DP-Agent
[Install]
WantedBy=bluetooth.service
@mill1000
Copy link
Author

@eichblatt Sorry Steve, I won't be of much help. I haven't used Bluetooth in a while and my knowledge of it is now all 4 years old. Have you taken a look at this section of the bluez-alsa docs? https://github.com/Arkq/bluez-alsa#configuration--usage

@eichblatt
Copy link

Thank you! I think my problem might be pulse audio, more than bluetooth.

@spmp
Copy link

spmp commented Jan 18, 2022

Good day @eichblatt ,
I have put my current version in a repo: https://github.com/spmp/promiscuous-bluetooth-audio-sinc
I tested this today on a Ubuntu 21.04 install and it worked like a charm. There are various fixes and no doubt issues....
Maybe we should move this thread over to the issues section of that repo.
@mill1000 I hope you feel properly attributed.

@dumtux
Copy link

dumtux commented Feb 19, 2022

I've tried many ways to accomplish this on Raspberry CM4, but non was successful.
Bluealsa, Bluez, auto paring, fixed PIN code, ... so many tries and fails!

Finally this worked out of the box - https://github.com/spmp/promiscuous-bluetooth-audio-sinc/blob/main/a2dp-agent
Thank you, @spmp

@spmp
Copy link

spmp commented Feb 20, 2022

Glad it worked as expected @hotteshen 8)
Please highlight any issues and let me know if the documentation - largely taken from this gist - is adequate.

@calanor
Copy link

calanor commented Aug 5, 2022

Hello,
with the new version of Raspberry pi OS Bullseye the bluealsa and python-dbus is obsolete. I installed the bluealsa package using this guide.
I don't know how install python-dbus.

Any upgrade of this guide for new Bullseye version?

@DominikStarke
Copy link

@calanor This guide largely still works.

The required dbus package can be installed using pip install dbus-python. After installation @spmp's a2dp agent should work.
For bluealsa it's probably best to compile it yourself. I followed the official instructions and it works even with AAC, aptX and LDAC if enabled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment