# 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":""}`. Отказ 403: `success:false`, `token:null`, `code` равен `invalid_code`, `pairing_closed`, `device_limit` или `invalid_name`. Если сохранить устройство не удалось: HTTP 503, `code:"storage_failed"`, токен не выдаётся и устройство не получает доступа. Все следующие запросы, включая WebSocket handshake, требуют заголовка: ```http Authorization: Bearer ``` ## Хранение и восстановление Файл `%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": "", "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).