Files
ios/PROTOCOL.md
T
ros 6174be5610
iOS build and tests / simulator (push) Has been cancelled
Add native SwiftUI player and macOS CI
2026-09-10 09:11:55 +03:00

277 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MusicBridge protocol v1
Транспорт для будущего **нативного iPhone-клиента**. HTML, Safari/PWA и веб-клиента нет.
## Запуск
### Окно управления и трей
```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 и
ASP.NET Core Runtime. Для установки без отдельных Runtime подготовлен
`dist/MusicBridge-win-x64.zip`; см. [INSTALL.md](INSTALL.md).
Меню трея «Запускать вместе с Windows» включает/выключает автозапуск текущего
пользователя. Сохраняются текущий путь, порт и режим сети. `--minimized` включает
трей и скрывает главное окно при старте. Автозапуск по умолчанию выключен;
лучше включать его после установки в постоянную папку.
Из каталога репозитория:
```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 текущего пользователя.
## Состояние
Авторизованный `GET /v1/info` идентифицирует агента. Клавиша `I` открывает
Windows-окно с QR; `B` создаёт новый код. При `--lan` агент объявляет сервис
`_musicbridge._tcp.local.` по mDNS. Подробности и ограничения — в
[CONNECTION-ROADMAP.md](CONNECTION-ROADMAP.md). iOS-клиент пока не реализован.
`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. После потери соединения клиент
переподключается с задержкой и получает полный снимок. Медленный клиент получает
последнее состояние; промежуточные снимки не накапливаются в неограниченной очереди.
Отправка снимка ограничена 5 секундами. Keepalive отправляется через 20 секунд,
ожидание ответа ограничено 10 секундами; оборванные соединения освобождают слот.
При ошибке чтения медиасессии сервер публикует пустое состояние с `mediaError`
и продолжает опрос. После восстановления очередной снимок содержит свежие данные.
На чтение передаётся отмена через 5 секунд.
Запросы с 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).