Files

19 KiB
Raw Permalink Blame History

MusicBridge protocol v1

Транспорт для будущего нативного iPhone-клиента. HTML, Safari/PWA и веб-клиента нет.

Запуск

Окно управления и трей

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.

Меню трея «Запускать вместе с Windows» включает/выключает автозапуск текущего пользователя. Сохраняются текущий путь, порт и режим сети. --minimized включает трей и скрывает главное окно при старте. Автозапуск по умолчанию выключен; лучше включать его после установки в постоянную папку.

Из каталога репозитория:

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. После потери соединения клиент переподключается с задержкой и получает полный снимок. Медленный клиент получает последнее состояние; промежуточные снимки не накапливаются в неограниченной очереди. Отправка снимка ограничена 5 секундами. Keepalive отправляется через 20 секунд, ожидание ответа ограничено 10 секундами; оборванные соединения освобождают слот. При ошибке чтения медиасессии сервер публикует пустое состояние с mediaError и продолжает опрос. После восстановления очередной снимок содержит свежие данные. На чтение передаётся отмена через 5 секунд. Запросы с 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.