Локальное управление колонками SberBoom из браузера — без Home Assistant, без облака и без аккаунта Sber. Один процесс Node.js, веб-интерфейс доступен с любого устройства в вашей сети.
┌──────────────┐ HTTP/SSE ┌──────────────┐ WSS :20000 ┌────────────┐
│ браузер/тел. │ ───────────► │ Node.js :8080│ ─────────────► │ SberBoom 1 │
└──────────────┘ │ │ ─────────────► │ SberBoom 2 │
│ │ ─────────────► │ SberBoom 3 │
└──────────────┘ └────────────┘
- Несколько колонок одновременно (карточка на каждую) + общая панель «Все колонки»
- Поиск и отправка из Звука: кнопка «Найти» прямо на карточке колонки открывает поиск по названию — или вставку ссылки
zvuk.com/ числового ID — и запускает выбранное только на этой колонке - История поиска: найденное и запущенное сохраняется, часто используемое предлагается первым
- Очередь колонки: что играет и что дальше (до 20 треков) с названиями из каталога
- Мультирум: текущий трек Звука запускается на остальных колонках одним нажатием — все начинают с одной секунды источника, play/pause/next/seek зеркалируются
- Play / Pause / Next / Prev, seek по прогресс-бару
- Громкость и mute (в том числе для всех колонок сразу)
- Shuffle, repeat (off → all → one), лайк
- Обложка, название, исполнитель, альбом, позиция и длительность
- Живые обновления по push от колонки (смена трека — мгновенно)
- Сопряжение нажатием
+на корпусе, токен сохраняется между запусками - Bluetooth: режим сопряжения колонки, список спаренных устройств, подключение / отключение / забывание, скан устройств рядом с RSSI
- Автопереподключение с backoff, keep-alive, детект half-open TCP
- Поиск колонок через mDNS и добавление по IP вручную
- Полностью локально: наружу уходят только запросы к каталогу/обложкам Звука, без аккаунта и облака Сбера
- Node.js 20+ (проверено на 26)
- Колонка и компьютер в одной сети; порт
20000доступен
git clone https://github.com/sergvax/sboom_node.git
cd sboom_node
npm install
npm startОткройте http://<ip-компьютера>:8080 — адрес печатается в консоли при старте.
Порт можно поменять: PORT=3000 npm start.
- Нажмите «+ Колонка», введите IP колонки (или найдите через «Искать»).
- На карточке нажмите «Сопрячь».
- В течение 2 минут нажмите и удерживайте «+» на корпусе колонки (там, где показывается громкость).
- Готово — токен сохранится в
data/speakers.json, повторять не нужно.
Повторите для каждой из трёх колонок. Имена можно менять (карандаш на карточке).
Нажмите «Найти» на карточке нужной колонки. Поиск откроется в контексте именно этой колонки: её имя есть в заголовке и в подсказке, а все результаты запускаются только на ней — выбрать «не ту» колонку нельзя.
- Введите название трека, плейлиста, исполнителя или альбома — приложение покажет результаты по разделам, клик по строке (или по кнопке «Играть») запускает контент;
- если поиск не работает, раскройте «Поиск не работает? Вставьте ссылку или ID Звука» и вставьте
https://zvuk.com/track/123,/playlist/123,/artist/123,/album/123(то же, что/release/123) либо просто числовой ID.
После успешного запуска окно поиска закрывается и возвращает к карточке колонки — она подсвечивается. Крестик в поисковой строке тоже закрывает поиск и возвращает к карточке; повторное открытие с пустым запросом показывает историю.
История поиска. Сохраняется только то, что вы сами нашли и запустили из этого окна: при следующем открытии показываются «Часто запускаете» (2+ запуска) и «Недавние». Очередь, прямые ссылки и автозапуски в историю не попадают. История лежит в data/search-history.json (права 600), общая для всех браузеров, и очищается кнопкой «Очистить».
Колонка запускает контент из своей сессии Звука через deeplink staros://music:
| Контент | Deeplink |
|---|---|
| Трек | staros://music?tid=<id>&pt=track |
| Подкаст / эпизод | staros://music?tid=<id>&pt=podcast |
| Плейлист | staros://music?pid=<id>&pt=playlist |
| Исполнитель | staros://music?pid=<id>&pt=artist |
| Альбом | staros://music?pid=<id>&pt=release |
Все пять типов проверены на реальных колонках; альбом, исполнитель и плейлист подтверждаются по playlistId/playlistTitle/playlistType в GET_STATE.
Если поиск недоступен, приложение не теряет функциональность: вставьте ссылку или ID в разделе «Поиск не работает?» — deeplink собирается без каталога. Названия и обложки в этом случае могут не отобразиться, а очередь покажет только ID треков. Поиск идёт параллельно двумя каналами (REST /api/tiny/search и GraphQL /api/v1/graphql), поэтому сбой одного из них не ломает выдачу. Анонимный токен и cookie Звука кэшируются в data/zvuk-session.json (права 600) и переиспользуются между запусками.
Очередь (кнопка на карточке) читается командой op=17 и содержит только ID треков (до 20, без названий) — названия подтягиваются из каталога. Для радио, Bluetooth и «волны» прошивка очередь не отдаёт.
Ограничения отправки:
- запуск заменяет текущий контекст воспроизведения; команды «добавить в конец очереди» в протоколе нет;
- числовой ID без типа может совпасть у разных сущностей (трек и плейлист) — в этом случае приложение попросит вставить полную ссылку, чтобы не запустить не то;
- на ведомую колонку мультирума отправлять контент нельзя — отправляйте на источник, группа подхватит тот же deeplink.
Кнопка «Bluetooth» на карточке открывает панель встроенного Bluetooth-модуля колонки. Отдельный Bluetooth-адаптер на компьютере не нужен: команды идут по тому же локальному WebSocket-протоколу, что и управление музыкой.
- «Режим сопряжения» (
op=22) — колонка становится видимой как A2DP-приёмник. Откройте Bluetooth на телефоне или компьютере, найдите колонку и подключитесь к ней; звук пойдёт с вашего устройства на колонку. Длительность окна видимости задаёт прошивка. - Спаренные устройства (
op=19) — имя, MAC и статус подключения. Кнопки позволяют подключить, отключить или забыть устройство (op=20: connect / disconnect / remove). - Рядом (
op=21) — скан видимых Bluetooth-устройств с уровнем сигнала (RSSI). Список справочный: подключать нужно уже спаренные устройства из верхнего списка.
Текущий трек, играющий по Bluetooth, показывается на карточке с источником Bluetooth: название и исполнитель приходят из GET_STATE, обложки у такого трека нет. Мультирум и перенос воспроизведения на другие колонки для Bluetooth не работают — это отдельный физический источник, а не контекст Звука.
Включите трек или волну Звука на одной колонке и нажмите «Мультирум» на её карточке (или кнопку мультирума в панели «Все колонки»). Тот же трек заиграет на всех остальных подключённых колонках.
Что происходит под капотом:
- на ведомые колонки отправляется
DEEPLINK(staros://music?tid=<id>&pt=track) — тот же канал, что использует интеграцияsboom_ha; - сразу после deeplink ведомая ставится на паузу: трек успевает загрузиться, но не играет с нуля, пока идёт выравнивание;
- при старте источник замораживается (пауза), все ведомые переводятся на его секунду и группа запускается одной волной — первая же секунда звука совпадает, подготовка занимает доли секунды, а не 20–40 c;
seekпринимает только целые секунды и может не примениться с первого раза, поэтому приложение повторяет его, пока колонка не подтвердит нужную позицию;- после next/prev и при расхождении больше 1.5 c ведомая быстро догоняет источник по текущей позиции и только потом возобновляет звук — трек не начинается с нуля;
- дальше каждые 4 секунды сверяются play/pause и трек, а раз в ~45 секунд — точная фаза; уехавшую колонку подтягивает только при расхождении больше 0.6 c;
- play/pause/next/prev/seek/shuffle/repeat/like, сделанные на источнике или на любой карточке группы, применяются ко всей группе; для next/prev ведомая перезапускает новый трек через deeplink и сразу догоняет его по секунде;
- «Стоп» на источнике (или групповая кнопка) выключает мультирум и ставит все колонки на паузу, «Выйти» убирает из группы одну колонку.
Это локальная синхронизация, а не нативный SberCast/PTP-мультирум:
- работает только для треков Звука, у которых есть
trackId; радио, Bluetooth и аудиокниги так перенести нельзя — кнопка будет неактивна с подсказкой; - на старте и после next/prev колонки совпадают в пределах ~1 c:
seekцелосекундный, а момент его применения на колонке плавает; в установившемся режиме рассинхрон ~0.2–0.5 c; - нативный
op=16 action=8(START_MULTIROOM) локально принимается сack, но группу не создаёт — для него нужен облачный SberCast/PTP-сценарий и один Сбер ID на всех колонках. Вdzerik/sboom_haиTohaRG2/sboom_haготовой реализации мультирума нет: определена только константаMEDIA_CMD_START_MULTIROOM = 8, а создание/вход в группу (GroupManagementMessage,MultiRoomMessage) осталось неразобранным.
Встроенная мок-колонка:
npm run mock # wss://127.0.0.1:20000
npm start # в другом терминалеДобавьте 127.0.0.1 и нажмите «Сопрячь» — «+» жать не нужно.
npm testПокрыто: TLV-кодек, парсеры track/state/queue, нормализация адреса, полный e2e-цикл
pair → reconnect → state/metadata → команды → push → сохранение токена на мок-колонке,
а также e2e мультирума (старт со средней секунды источника, синхронная пауза, next через deeplink, стоп),
запуск контента и очередь с метаданными каталога, клиент Звука (REST+GraphQL, сессия, 401,
недоступность каталога, неоднозначный ID), история поиска (дедупликация, счётчики, права 600,
очистка), Bluetooth (op=19…22: парсинг списков, скан с RSSI, режим сопряжения,
connect/disconnect/remove) и UI-сценарии «карточка → поиск → запуск/история → возврат к карточке»
и «карточка → Bluetooth → скан/сопряжение/команды».
data/speakers.json— IP, имена и pin-токены колонок. Файл создаётся автоматически с правами 600; не публикуйте его.data/zvuk-session.json— анонимный токен и cookie каталога Звука (права 600). Это не аккаунт Сбера: токен выдаётся анонимно и нужен только для поиска и метаданных.data/search-history.json— личная история запусков из поиска (права 600): названия, обложки, счётчики. Общая для всех браузеров; очищается из окна поиска.- Сервер слушает
0.0.0.0:8080без пароля. Это осознанно: приложение рассчитано на доверенную домашнюю сеть. Если сеть не доверенная — поставьтеHOST=127.0.0.1и/или заверните в reverse-proxy с авторизацией. - Наружу уходят HTTPS-запросы к
zvuk.com(поиск, метаданные, обложки) иcdn-image.zvuk.com. Управление колонкой — исключительно LAN.
Протокол повторяет логику sboom_ha; Bluetooth-опкоды взяты из TohaRG2/sboom_ha:
wss://<ip>:20000/с самоподписанным сертификатом (проверка сертификата отключена).- Бинарный TLV (protobuf-подобный):
varint+ length-delimited + float32. - Pair: запрос
op=4, пользователь нажимает+, колонка отдаёт pin-токен; далее токен идёт в поле 3 каждого запроса. - Состояние:
op=12(громкость/mute/подсистемы), метаданные:op=10. - Однократный
op=10подписывает соединение на push-обновления трека. - Команды:
op=14громкость,op=15seek,op=16медиа-команды,op=18keep-alive. - Bluetooth:
op=19список спаренных,op=20команда устройству (connect/disconnect/remove),op=21скан видимых устройств,op=22режим сопряжения. - Громкость пушится не всегда, поэтому раз в 10 секунд идёт лёгкий поллинг
op=12; метаданные — раз в 15 секунд как страховка, основной канал — push.
~/Library/LaunchAgents/local.sboom.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>local.sboom</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/node</string>
<string>/Users/ВАШ_ПУТЬ/sboom/server.js</string>
</array>
<key>WorkingDirectory</key><string>/Users/ВАШ_ПУТЬ/sboom</string>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/tmp/sboom.log</string>
<key>StandardErrorPath</key><string>/tmp/sboom.err.log</string>
</dict>
</plist>launchctl load ~/Library/LaunchAgents/local.sboom.plist| Симптом | Что проверить |
|---|---|
| «Колонка не ответила» | Верный ли IP, включена ли колонка, открыт ли порт: nc -zv <ip> 20000 |
| «Порт 20000 закрыт» | Другой IP после перезагрузки роутера — поправьте адрес (удалить/добавить) |
| Не находится через «Искать» | Роутер режет multicast (AP isolation) — добавьте по IP вручную |
| Сопряжение зависает | Нажимайте именно + на корпусе, окно — 2 минуты |
| Токен перестал работать после сброса колонки | На карточке «Сопрячь заново» (иконка звена) |
| Нет обложек | Обложки берутся с CDN Звука; без интернета будет градиент-заглушка |
- Поиск и каталог используют недокументированный публичный API веб-плеера Звука. Он может измениться без предупреждения; ссылки и числовые ID при этом продолжат работать.
- Очередь отдаёт только ID треков и не поддерживает «добавить в очередь»: запуск контента заменяет текущий контекст.
- Мультирум — локальная синхронизация треков Звука; радио и Bluetooth не переносятся, нативный SberCast/PTP-мультирум локально недоступен.
- Тексты песен и будильники не включены. Bluetooth-панель управляет встроенным модулем колонки; отдельного BT-адаптера на сервере нет.
- Скан Bluetooth (
op=21) может занимать несколько секунд и зависит от прошивки; список «Рядом» не гарантирует, что устройство можно подключить без предварительного спаривания. - Громкость, изменённая голосом или кнопками колонки, доезжает до UI с задержкой до 10 секунд (поллинг).
- Если прошивка изменит протокол, адаптер может перестать работать — как и любая локальная интеграция.
sboom_node/
├── server.js HTTP API + SSE + статика
├── lib/
│ ├── tlv.js бинарный TLV-кодек
│ ├── protocol.js опкоды и константы
│ ├── parsers.js разбор track/state/queue/BT
│ ├── speaker.js WS-клиент колонки + pair + Bluetooth
│ ├── manager.js supervisor на колонку, optimistic-команды
│ ├── zvuk.js каталог Звука: поиск, метаданные, deeplink
│ ├── history.js история запусков из поиска
│ ├── discovery.js mDNS _staros._tcp
│ └── store.js JSON-хранилище
├── public/ веб-интерфейс (без сборки)
├── docs/screenshot.png скриншот интерфейса для README
├── mock/speaker.js мок-колонка для демо и тестов
├── test/ node:test
├── CHANGELOG.md история версий
└── .github/workflows/ CI (npm test)
MIT. Проект не аффилирован со Сбером; работает только с колонками, которыми вы владеете, и требует физического нажатия + для авторизации.
