πΈπ π β
Bluetooth Speaker Radio
Starting an internet radio stream on my phone and having it stream audio to a Bluetooth speaker is madness. Way to many interactions, where I mostly want the behavior of an old FM radio: switch on, done.
Replicating the FM radio UX was simpler than expected. The Linux Bluetooth and audio subsystems already send all the DBus events needed to tie a media player to a Bluetooth speaker connection.1
Implementation
I use Raspberry Pi 3 with Raspbian 10.3 and blue-alsa.2 pulseaudio-bluetooth should work as well, but since my pi runs headless, it felt simpler without pulseaudio.
Configuration and State
Boring stuff first. All state and configuration is stored in global
variables to keep it simple. To adapt the tool to other devices,
simply change DEVICE_MAC
.
Handling Events
The Linux Bluetooth stack creates DBus events that the script can listen for. There is a PropertiesChanged event from bluez that fires on connect and disconnect and the PCMAdded event from blue-alsa.3
PCMAdded will start the player, PropertiesChanged starts a background thread that listens for media key events and stops the player on disconnect.
There is just one button on my speaker that emits an event, just
enough to cycle through stations. For some reason the evdev library
does not enumerate my Bluetooth device, but when the script runs it is
likely to be the last one added and there are less than 10 event
devices on that host. So, we can get away find_input_device
.
Player Interaction
Starting and stopping is pretty much standard Popen and process.terminate. I chose mpv, because it works nicely on the command line and has an RPC interface. To change the station the script sends a JSON RPC message to mpc.4
player_command
takes another shortcut by using socat to interact with
mpv over the IPC socket. That socat call saves a 100 lines of python
easy.
Boilerplate
Imports, a small helper that emulates the shell '&' and the entry point:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import dbus
import glob
import json
import os
import subprocess
import time
from concurrent.futures import ThreadPoolExecutor
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
from evdev import InputDevice, categorize, ecodes, KeyEvent
<<configuration>>
<<state>>
THREADPOOL = ThreadPoolExecutor(max_workers=10)
def background(func):
def swallow():
try:
func()
except Exception as e:
print("Func threw:", func.__name__, e)
THREADPOOL.submit(swallow)
<<player>>
<<evdev>>
<<dbus>>
try:
loop = GLib.MainLoop()
loop.run()
except KeyboardInterrupt:
stop_player()
Footnotes
Full script: bluetooth_radio.py
I assume that the device is already paired, connected and working. Once connected, it will automatically reconnect when turning if off and on again.
I found the events by running dbus-monitor --system
and tuning
the speaker off and on again.
Bus bus names can be queried by
dbus-send --system --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.ListNames
Restarting mpv would also be an option, but did not feel "right" to me :)