17 KiB
MusicBridge protocol v1
Транспорт для будущего нативного iPhone-клиента. HTML, Safari/PWA и веб-клиента нет.
Запуск
Из каталога репозитория:
dotnet run
По умолчанию агент принимает соединения только на https://127.0.0.1:8765.
Для локальной сети:
dotnet run -- --lan
Другой порт: dotnet run -- --lan --port 8766.
Консоль показывает IPv4-адреса ПК. При нескольких адаптерах нужен адрес домашней сети.
Автоматического изменения брандмауэра нет. Если Windows запросит доступ к сети,
для домашнего использования разрешать только частную сеть. Порт на роутере не открывать.
TLS и сопряжение
- Только HTTPS/WSS. HTTP не поддерживается.
- Агент создаёт самоподписанный сертификат при первом запуске и показывает SHA-256 отпечаток всего DER-сертификата: 64 шестнадцатеричных символа.
- Будущий нативный клиент должен сверять этот отпечаток до отправки кода, затем проверять его на каждом соединении. Пока отпечаток сверяют по экрану ПК; в следующем этапе он войдёт в QR. Отключение проверки всех сертификатов недопустимо.
- Сертификат и доверенные устройства сохраняются между запусками. Новый код после обычного перезапуска не нужен: клиент использует сохранённый токен и отпечаток.
- Восьмизначный код действует 5 минут и принимается один раз. Десять неудачных
попыток закрывают окно сопряжения. Клавиша
Bна ПК открывает новое окно. - Максимум 8 доверенных устройств. В консоли показан нумерованный список;
клавиши
1…8отзывают доступ у соответствующего устройства. Его открытый WSS отключается, новые запросы получают 401. Уже принятые команды не отменяются. - Если устройства уже сохранены, при запуске окно сопряжения закрыто.
Для нового телефона нажать
B. При пустом списке первое окно открывается автоматически. - Bearer-токен не передавать в URL, не записывать в логи. В будущем хранить в Keychain.
Сопряжение:
POST /v1/pair
Content-Type: application/json
{"code":"<код с экрана ПК>","deviceName":"Мой iPhone"}
deviceName необязателен (по умолчанию iPhone), до 64 символов, без управляющих
символов и не только пробелы. Название служит подписью в локальном списке,
а не доказательством личности телефона.
Ответ 200: {"success":true,"token":"<64 hex characters>","code":"ok","deviceId":"<id>"}.
Отказ 403: success:false, token:null, code равен invalid_code,
pairing_closed, device_limit или invalid_name.
Если сохранить устройство не удалось: HTTP 503, code:"storage_failed",
токен не выдаётся и устройство не получает доступа.
Все следующие запросы, включая WebSocket handshake, требуют заголовка:
Authorization: Bearer <token>
Хранение и восстановление
Файл %LOCALAPPDATA%\MusicBridge\identity.dat содержит сертификат с закрытым
ключом, ID/имена/даты сопряжения устройств и SHA-256 хеши токенов. Весь файл
защищён Windows DPAPI текущего пользователя. Сами bearer-токены не сохраняются
на ПК. DPAPI не защищает от программ, уже работающих от имени этого пользователя.
Запись выполняется через защищённый временный файл и замену основного файла.
Ошибка записи не изменяет активное доверие. identity.lock блокирует одновременный
запуск двух агентов с одним хранилищем; после закрытия агента блокировка снимается.
Наличие самого файла блокировки после выхода нормально.
Повреждённое, недоступное или неизвестное по версии хранилище не заменяется автоматически: агент завершится с ошибкой в консоли. Сертификат выдан на 5 лет; истёкший сертификат также не заменяется молча, чтобы не нарушить закреплённый отпечаток. Автоматическое обновление сертификата — будущая задача.
Для осознанного полного сброса: остановить агент, сохранить резервную копию и
переименовать identity.dat, затем запустить агент и заново сопрячь телефоны
с новым отпечатком. Обычный перезапуск не отзывает доступ.
Не переносить этот файл на другой компьютер в расчёте на сохранение подключения:
он привязан к защите Windows текущего пользователя.
Состояние
Авторизованный GET /v1/info идентифицирует агента. Клавиша I открывает
Windows-окно с QR; B создаёт новый код. При --lan агент объявляет сервис
_musicbridge._tcp.local. по mDNS. Подробности и ограничения — в
CONNECTION-ROADMAP.md. iOS-клиент пока не реализован.
GET /v1/state возвращает последний снимок. События смены сессии, метаданных,
воспроизведения и позиции Windows запускают обновление общего снимка для всех
клиентов. События объединяются в течение 25 мс. Резервный таймер примерно раз
в 500 мс поддерживает прогресс и чтение системной громкости.
Метаданные и обложка кешируются: перечитываются по событию либо раз в 10 секунд
на случай пропущенного уведомления. При смене сессии подписки переключаются.
{
"protocolVersion": 1,
"timestamp": "2026-09-09T00:00:00+00:00",
"hasSession": true,
"title": "Название",
"artist": "Исполнитель",
"source": "идентификатор приложения",
"playbackStatus": "Playing",
"positionSeconds": 42.5,
"startSeconds": 0,
"endSeconds": 180,
"minSeekSeconds": 0,
"maxSeekSeconds": 180,
"playbackRate": 1,
"capabilities": {
"play": false, "pause": true, "toggle": true,
"next": true, "previous": true, "seek": true
},
"volume": 0.5,
"muted": false,
"mediaError": null,
"volumeError": null,
"artworkId": "<SHA-256 обложки или null>",
"artworkError": null
}
Время — секунды от начала таймлайна плеера, включая дробную часть.
Длительность — endSeconds - startSeconds, когда конец известен.
positionSeconds уже учитывает время с последнего обновления Windows.
Будущий клиент может плавно продолжать позицию между снимками при Playing,
учитывая скорость; каждый следующий снимок корректирует локальный расчёт.
При отсутствии сессии hasSession:false, строки пустые, возможности выключены.
Громкость доступна независимо от медиасессии; при ошибке аудиоустройства
volume и muted равны null, а volumeError содержит причину.
При ошибке чтения медиасессии заполняется mediaError.
Обложка
GET /v1/artwork/{artworkId} требует того же Bearer-токена и возвращает бинарное
изображение с Content-Type. В JSON и WebSocket передаётся только идентификатор,
поэтому изображение не пересылается при каждом обновлении прогресса.
artworkId— SHA-256 содержимого, 64 hex-символа;null, если обложки нет.- Клиент загружает обложку при изменении ID и может хранить её в своём кеше.
- В агенте хранится только текущая обложка, не более 2 МиБ. Чтение ограничено двумя секундами; ошибка обложки не скрывает название и состояние трека.
- Поддерживаются PNG, JPEG, GIF и WebP; тип определяется по сигнатуре. Сервер не декодирует изображение: клиенту нужно обрабатывать ошибки декодирования.
- При смене трека, отсутствии или ошибке обложки предыдущая удаляется из кеша.
Неизвестный/устаревший ID возвращает 404. При гонке со сменой трека клиент
перечитывает состояние; при
artworkId:nullпоказывает заглушку. artworkErrorсодержит причину ошибки чтения; без ошибки —null.
Команды
POST /v1/command, Content-Type: application/json:
{"id":"seek-1","type":"seek","value":90.5}
| type | value | Действие |
|---|---|---|
| play / pause | отсутствует | Начать / приостановить воспроизведение |
| toggle | отсутствует | Play/pause |
| next / previous | отсутствует | Следующий / предыдущий трек |
| seek | секунды ≥ 0 | Абсолютная позиция, ограниченная диапазоном плеера |
| seekRelative | секунды от −3600 до 3600 | Сдвиг от текущей вычисленной позиции |
| volume | число 0…1 | Абсолютная системная громкость |
| volumeRelative | число −1…1 | Сдвиг громкости с ограничением 0…1 |
| toggleMute | отсутствует | Переключить mute основного устройства вывода |
Изменение громкости сохраняет текущее состояние mute.
Результат: {"success":true,"code":"ok","message":"…","id":"seek-1"}.
У медиакоманд успех означает принятие команды плеером; фактическое состояние
подтверждается последующим снимком. Отказ не должен оптимистично менять UI.
id необязателен, максимум 64 символа. Он связывает ответ с запросом,
не является ключом идемпотентности. Не повторять автоматически next, toggle
или относительные команды после потери ответа. Сначала перечитать состояние.
Ошибки команды: invalid_command, no_session, unsupported, rejected,
operation_failed. Некорректная команда/JSON — HTTP 400, тело больше 4096 байт —
413, отсутствие/неверный токен — 401. Отказ плеера возвращается HTTP 200 с
success:false, чтобы отличать транспорт от выполнения команды.
Общий лимит HTTP-запросов: 60 в секунду, сверх лимита — 429.
WebSocket
wss://<адрес>:<порт>/v1/events, с тем же Authorization.
Сразу после подключения, по событиям Windows и при резервном обновлении
примерно каждые 500 мс сервер присылает текстовый JSON:
{"type":"state","state":{"protocolVersion":1,"…":"поля снимка выше"}}
Поток только для состояния. Команды отправляются через HTTPS; каждый запрос имеет отдельный ответ. Передача клиентского payload в поток закрывает соединение. Поддерживается до 8 одновременных WebSocket. После потери соединения клиент переподключается с задержкой и получает полный снимок. Медленный клиент получает последнее состояние; промежуточные снимки не накапливаются в неограниченной очереди. Запросы с Origin отклоняются: браузерные клиенты в проекте не поддерживаются.
Проверка
dotnet build
dotnet run --project tests/MusicBridge.Agent.Tests.csproj
Проверки используют тестовый медиасервис и настоящий локальный HTTPS/WSS с проверкой отпечатка. Музыку и громкость пользователя они не меняют.
Дополнительно прочитать реальную медиасессию и обложку Windows:
dotnet run --project tests/MusicBridge.Agent.Tests.csproj -- --live-read
Этот режим также не отправляет команд плееру. При отсутствии обложки он сообщает об этом, а проверки её содержимого пропускает.
Проверки постоянного доверия используют отдельный временный каталог и удаляют
его после выполнения. Рабочее хранилище пользователя не затрагивается.
Ошибки тестов выводятся как TEST FAILED с кодом завершения 1. Диагностический
аргумент --test-failure проверяет этот путь контролируемым исключением.
Реализация транспорта опирается на документацию Microsoft: Kestrel HTTPS и WebSocket. Обновления таймлайна используют событие Windows TimelinePropertiesChanged. Защита хранилища использует Windows CryptProtectData.