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
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,
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
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
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()
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 :)