Перейти к содержимому

Установка — Docker Compose

  • Docker Engine и Docker Compose
  • Bluetooth-адаптер на хосте
  • PulseAudio или PipeWire на хосте
  • Music Assistant в вашей сети
  • На AMD64-хостах: CPU уровня x86-64-v2 (SSE4.2 + POPCNT)

Публикуемый образ поддерживает linux/amd64, linux/arm64 и linux/arm/v7.

Аудио-стек (PyAV / ffmpeg / NumPy) собран в виде wheel-ов под микроархитектуру x86-64-v2, что требует SSE4.2 и POPCNT. На более старых CPU дочерний процесс демона запускается и тут же падает с signal=4 (SIGILL — «Illegal Instruction»). Начиная с v2.71.0 карточка устройства и блок --- SENDSPIN CONNECTION --- в диагностическом отчёте сразу показывают код выхода и сигнал; до этого релиза такая ошибка выглядела как тихий ~10-секундный restart-loop без traceback’а.

Подтверждённый минимум:

СемействоМинимум
Intel x86Sandy Bridge / 2-е поколение Core (2011), Atom Silvermont, любые современные Celeron / Pentium
AMD x86Bulldozer / FX (2011), Jaguar, Ryzen — всё, что выпущено с 2011 года, кроме нетбучной линейки Bobcat
ARMВсе образы aarch64 и arm/v7 для Raspberry Pi 4+ (ограничения по SSE не применимы)
QEMU / KVMCPU-модель host (рекомендуется) или qemu64,+sse4.2,+popcnt — дефолтный qemu64 ниже baseline’а

Проверка на хосте:

Окно терминала
grep -m1 -E 'sse4_2|popcnt' /proc/cpuinfo

Непустой результат — мост запустится. Пустой — демон будет падать на каждом запуске; см. Troubleshooting › CPU-baseline crash для диагностики и путей решения.

  1. Сначала сопрягите колонку на хосте

    Окно терминала
    bluetoothctl
    scan on
    pair AA:BB:CC:DD:EE:FF
    trust AA:BB:CC:DD:EE:FF
    connect AA:BB:CC:DD:EE:FF
    exit
  2. Создайте .env

    AUDIO_UID=1000
    AUDIO_GID=1000
    TZ=Europe/Moscow
    WEB_PORT=8080
    BASE_LISTEN_PORT=8928
  3. Создайте docker-compose.yml

    services:
    sendspin-client:
    image: ghcr.io/trudenboy/sendspin-bt-bridge:latest
    container_name: sendspin-client
    restart: unless-stopped
    network_mode: host
    volumes:
    - /var/run/dbus:/var/run/dbus
    - /run/user/${AUDIO_UID:-1000}/pulse:/run/user/${AUDIO_UID:-1000}/pulse
    - /run/user/${AUDIO_UID:-1000}/pipewire-0:/run/user/${AUDIO_UID:-1000}/pipewire-0
    - /etc/docker/Sendspin:/config
    environment:
    - SENDSPIN_SERVER=auto
    - TZ=${TZ:-UTC}
    - WEB_PORT=${WEB_PORT:-8080}
    - BASE_LISTEN_PORT=${BASE_LISTEN_PORT:-8928}
    - CONFIG_DIR=/config
    - AUDIO_UID=${AUDIO_UID:-1000}
    - AUDIO_GID=${AUDIO_GID:-1000}
    - PULSE_SERVER=unix:/run/user/${AUDIO_UID:-1000}/pulse/native
    - XDG_RUNTIME_DIR=/run/user/${AUDIO_UID:-1000}
    devices:
    - /dev/bus/usb:/dev/bus/usb
    cap_add:
    - NET_ADMIN
    - NET_RAW
  4. Запустите контейнер

    Окно терминала
    mkdir -p /etc/docker/Sendspin
    docker compose up -d
  5. Откройте веб-интерфейс

    http://<ip-хоста>:<WEB_PORT>
  • WEB_PORT управляет прямым listener’ом веб-интерфейса/API в Docker-режиме.
  • BASE_LISTEN_PORT задаёт базовый Sendspin-порт для устройств без явного listen_port.
  • Каждое устройство без ручного порта получает BASE_LISTEN_PORT + индекс_устройства.
  • В сложных схемах можно задать listen_port и listen_host на уровне устройства через веб-интерфейс или /config/config.json после первого запуска.

Пример блока устройства в /config/config.json:

{
"mac": "11:22:33:44:55:66",
"player_name": "Колонка на кухне",
"listen_port": 8935,
"listen_host": "192.168.1.50"
}

listen_host меняет только рекламируемый host/IP для плеера и не влияет на bind-адрес внутри контейнера.

Если вы запускаете несколько bridge-контейнеров на одной машине:

  • задайте каждому контейнеру уникальный WEB_PORT
  • задайте каждому контейнеру уникальный BASE_LISTEN_PORT
  • не настраивайте одну и ту же Bluetooth-колонку в двух работающих контейнерах

network_mode: host обязателен для:

  • mDNS-обнаружения при SENDSPIN_SERVER=auto
  • доступа к Bluetooth-стеку хоста через D-Bus

Необходимые capabilities:

CapabilityНазначение
NET_ADMINУправление Bluetooth-адаптером
NET_RAWRaw Bluetooth/HCI socket access
Окно терминала
docker logs -f sendspin-client
curl -s http://localhost:${WEB_PORT:-8080}/api/preflight | python3 -m json.tool

В новых образах startup diagnostics также показывают:

  • init UID/GID внутри контейнера
  • app UID/GID запущенного bridge-процесса
  • выбранный путь к audio socket
  • владельца/права сокета
  • результат живого pactl info probe
  • пришлось ли контейнеру подождать позднюю готовность D-Bus / Bluetooth / audio при холодном старте хоста
  • отдельное предупреждение, если bridge-процесс работает под другим UID, чем user-scoped audio socket на хосте

Если у вас уже настроены Bluetooth-устройства, новые образы также недолго ждут позднего появления host-зависимостей перед запуском bridge-процесса. Это убирает частый race, когда после перезагрузки хоста контейнеру нужен ещё один ручной restart.

Если хост стартует особенно медленно, ожидание можно подстроить:

environment:
- STARTUP_DEPENDENCY_WAIT_ATTEMPTS=60
- STARTUP_DEPENDENCY_WAIT_DELAY_SECONDS=1

Спикер коннектится, но bluez_* sinks не появляются

Заголовок раздела «Спикер коннектится, но bluez_* sinks не появляются»

На свежей установке Ubuntu / Raspberry Pi OS PipeWire по умолчанию устанавливается без Bluetooth-бэкенда. bluetoothctl показывает спикер как Connected: yes, но pactl list sinks short выдаёт только sendspin_fallback — никаких bluez_*. Через ~10 секунд BlueZ сам разрывает A2DP-соединение, бридж попадает в цикл переподключения.

Проверка и фикс:

Окно терминала
dpkg -l libspa-0.2-bluetooth 2>/dev/null | grep -qE '^ii' \
&& echo "libspa-0.2-bluetooth установлен" \
|| sudo apt install -y libspa-0.2-bluetooth
# Перезапуск audio-стека после установки
systemctl --user restart wireplumber pipewire pipewire-pulse

После переподключения BT-устройства pactl list sinks short должен показать bluez_output.* или bluez_sink.*.

Если на хосте аудио работает нормально, но в контейнере всё ещё видно Connection refused или pactl не может подключиться, проверьте:

Окно терминала
docker exec sendspin-client ls -la /run/user/${AUDIO_UID:-1000}/pulse/
docker exec sendspin-client env | grep -E 'PULSE|XDG'
docker exec sendspin-client ps -o user:20,pid,command -C python3
docker inspect sendspin-client --format '{{json .Mounts}}'

И на хосте:

Окно терминала
id
pactl info
ls -la /run/user/${AUDIO_UID:-1000}/pulse/

Если аудио всё ещё не работает, сначала убедитесь по startup diagnostics, что App UID совпадает с UID вашего host audio user. Старый глобальный Compose user: override теперь нужен только как временный диагностический тест для старых образов:

services:
sendspin-client:
user: "${AUDIO_UID:-1000}:${AUDIO_UID:-1000}"

После этого перезапустите контейнер и проверьте, начал ли работать pactl. Это именно шаг диагностики: он помогает быстро подтвердить проблему несовпадения UID/session при доступе к host audio socket.

Изменения устройств, адаптеров, WEB_PORT, BASE_LISTEN_PORT и настроек подключения к Music Assistant требуют перезапуска контейнера.