2026-09-09 01:05:52 +03:00
# MusicBridge protocol v1
Транспорт для будущего **нативного iPhone-клиента** . HTML, Safari/PWA и веб-клиента нет.
## Запуск
2026-09-09 10:01:39 +03:00
### Окно управления и трей
```powershell
dotnet run -c Release -- - -tray - -lan
```
`--tray` открывает Windows-окно и значок в области уведомлений. Крестик скрывает
окно; двойной щелчок по значку снова его показывает. Для остановки выбрать
«Завершить MusicBridge» в меню значка. При старте агент отсоединяется от консоли,
не закрывая терминал пользователя. Без `--tray` сохраняется диагностическая консоль.
Окно показывает трек/обложку, позицию и системную громкость. Поддерживает
play/pause, следующий/предыдущий трек, ползунки позиции/громкости, mute,
новое сопряжение, QR и отзыв доступа выбранного устройства. Недоступные команды
отключены. Без `--lan` агент остаётся доступен только с самого ПК.
Чтобы пользоваться уже собранной версией, можно запускать
`bin\Release\net10.0-windows10.0.19041.0\MusicBridge.Agent.exe --tray --lan` .
Пока это framework-dependent сборка: необходим .NET 10 Desktop Runtime и
2026-09-09 18:27:32 +03:00
ASP.NET Core Runtime. Для установки без отдельных Runtime подготовлен
`dist/MusicBridge-win-x64.zip` ; см. [INSTALL.md ](INSTALL.md ).
Меню трея «Запускать вместе с Windows» включает/выключает автозапуск текущего
пользователя. Сохраняются текущий путь, порт и режим сети. `--minimized` включает
трей и скрывает главное окно при старте. Автозапуск по умолчанию выключен;
лучше включать его после установки в постоянную папку.
2026-09-09 10:01:39 +03:00
2026-09-09 01:05:52 +03:00
Из каталога репозитория:
```powershell
dotnet run
```
По умолчанию агент принимает соединения только на `https://127.0.0.1:8765` .
Для локальной сети:
```powershell
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.
Сопряжение:
```http
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, требуют заголовка:
```http
Authorization: Bearer <token>
```
## Хранение и восстановление
Файл `%LOCALAPPDATA%\MusicBridge\identity.dat` содержит сертификат с закрытым
ключом, ID/имена/даты сопряжения устройств и SHA-256 хеши токенов. Весь файл
защищён Windows DPAPI текущего пользователя. Сами bearer-токены не сохраняются
на ПК. DPAPI не защищает от программ, уже работающих от имени этого пользователя.
Запись выполняется через защищённый временный файл и замену основного файла.
Ошибка записи не изменяет активное доверие. `identity.lock` блокирует одновременный
запуск двух агентов с одним хранилищем; после закрытия агента блокировка снимается.
Наличие самого файла блокировки после выхода нормально.
Повреждённое, недоступное или неизвестное по версии хранилище не заменяется
автоматически: агент завершится с ошибкой в консоли. Сертификат выдан на 5 лет;
истёкший сертификат также не заменяется молча, чтобы не нарушить закреплённый отпечаток.
Автоматическое обновление сертификата — будущая задача.
Для осознанного полного сброса: остановить агент, сохранить резервную копию и
переименовать `identity.dat` , затем запустить агент и заново сопрячь телефоны
с новым отпечатком. Обычный перезапуск **не отзывает** доступ.
Не переносить этот файл на другой компьютер в расчёте на сохранение подключения:
он привязан к защите Windows текущего пользователя.
## Состояние
2026-09-09 09:53:37 +03:00
Авторизованный `GET /v1/info` идентифицирует агента. Клавиша `I` открывает
Windows-окно с QR; `B` создаёт новый код. При `--lan` агент объявляет сервис
`_musicbridge._tcp.local.` по mDNS. Подробности и ограничения — в
[CONNECTION-ROADMAP.md ](CONNECTION-ROADMAP.md ). iOS-клиент пока не реализован.
2026-09-09 01:05:52 +03:00
`GET /v1/state` возвращает последний снимок. События смены сессии, метаданных,
воспроизведения и позиции Windows запускают обновление общего снимка для всех
клиентов. События объединяются в течение 25 мс. Резервный таймер примерно раз
в 500 мс поддерживает прогресс и чтение системной громкости.
Метаданные и обложка кешируются: перечитываются по событию либо раз в 10 секунд
на случай пропущенного уведомления. При смене сессии подписки переключаются.
```json
{
"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` :
```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:
```json
{ "type" : "state" , "state" :{ "protocolVersion" : 1 , "…" : "поля снимка выше" }}
```
Поток только для состояния. Команды отправляются через HTTPS; каждый запрос
имеет отдельный ответ. Передача клиентского payload в поток закрывает соединение.
Поддерживается до 8 одновременных WebSocket. После потери соединения клиент
переподключается с задержкой и получает полный снимок. Медленный клиент получает
последнее состояние; промежуточные снимки не накапливаются в неограниченной очереди.
2026-09-10 08:56:22 +03:00
Отправка снимка ограничена 5 секундами. Keepalive отправляется через 20 секунд,
ожидание ответа ограничено 10 секундами; оборванные соединения освобождают слот.
При ошибке чтения медиасессии сервер публикует пустое состояние с `mediaError`
и продолжает опрос. После восстановления очередной снимок содержит свежие данные.
На чтение передаётся отмена через 5 секунд.
2026-09-09 01:05:52 +03:00
Запросы с Origin
отклоняются: браузерные клиенты в проекте не поддерживаются.
## Проверка
```powershell
dotnet build
dotnet run - -project tests / MusicBridge . Agent . Tests . csproj
```
Проверки используют тестовый медиасервис и настоящий локальный HTTPS/WSS с
проверкой отпечатка. Музыку и громкость пользователя они не меняют.
Дополнительно прочитать реальную медиасессию и обложку Windows:
```powershell
dotnet run - -project tests / MusicBridge . Agent . Tests . csproj -- - -live-read
```
Этот режим также не отправляет команд плееру. При отсутствии обложки он сообщает
об этом, а проверки её содержимого пропускает.
Проверки постоянного доверия используют отдельный временный каталог и удаляют
его после выполнения. Рабочее хранилище пользователя не затрагивается.
Ошибки тестов выводятся как `TEST FAILED` с кодом завершения 1. Диагностический
аргумент `--test-failure` проверяет этот путь контролируемым исключением.
Реализация транспорта опирается на документацию Microsoft:
[Kestrel HTTPS ](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0 )
и [WebSocket ](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-10.0 ).
Обновления таймлайна используют
[событие Windows TimelinePropertiesChanged ](https://learn.microsoft.com/en-us/uwp/api/windows.media.control.globalsystemmediatransportcontrolssession.timelinepropertieschanged ).
Защита хранилища использует
[Windows CryptProtectData ](https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata ).