Skip to content

Headphone buttons control PC media (protocol v3), Windows window fix, global switch; release 0.4.0 - #4

Merged
Lyten02 merged 9 commits into
mainfrom
Lyten02/headphones-media-control
Oct 10, 2026
Merged

Lyten02 merged 9 commits into
mainfrom
Lyten02/headphones-media-control

Conversation

@Lyten02

@Lyten02 Lyten02 commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Три изменения: кнопки наушников управляют музыкой на ПК (протокол v3), исправлено самопроизвольное повторное открытие окна на Windows, общий выключатель AudioBridge на телефоне и на ПК.

⚠️ Протокол v3: телефон и ПК нужно обновить вместе. Старая пара отклоняется с понятной причиной («unsupported protocol version 2 (PC: 3)…»), это проверено на эмуляторе.

1. Кнопки наушников → музыка на ПК

Обычный режим «Один наушник» Выключено

(эмулятор Android 14 + тестовый ПК local_server --media; QR и секретов нет)

Как это работает

  • Наушник сам превращает касания в AVRCP-команды; Android отдаёт play/pause/next/previous медиасессии приложения, которое последним играло звук. Пока звук ПК идёт через AudioBridge, это AudioBridge: HeadsetControl.kt держит MediaSession в BridgeService (новых FGS-типов и разрешений не нужно, работает с выключенным экраном).
  • Состояние сессии повторяет ПК (PcMedia: играет/пауза, плеер, трек), поэтому наушник сам выбирает «play» или «pause».
  • Одно нажатие (первый ACTION_DOWN) = одна команда → PcRequest::Media → ПК.
  • Цель без fan-out: выбранный ПК (только пока подключён) или «Авто» — неприглушённый подключённый ПК, где играет музыка; если нигде — последний игравший/управляемый; иначе единственный подключённый. При неоднозначности цели нет, и сессия освобождается (кнопки снова работают для плееров телефона).
  • Windows (media.rs): GSMTC на отдельном MTA-потоке, по событиям, без опроса. Явные TryPlay/TryPause (Chromium/Electron, включая YandexMusicBetaMod через navigator.mediaSession, часто не включает toggle); PlayPause решается по реальному статусу. «Play, когда уже играет» ничего не делает: наушники шлют play при подключении. SendInput-медиаклавиша — только когда медиасессии нет или GSMTC упал; отказ сессии не дублируется клавишей.

«Один наушник» — настраиваются 2 и 3 касания (пауза/продолжить, следующий, предыдущий, громкость +/−, ничего); оба наушника ведут себя одинаково. Настройки сохраняются (после переустановки APK выбранное «3 касания → Громкость +» осталось).

Ограничения (Android API, не баг):

  • 1 касание (громкость) переназначить нельзя. AVRCP volume up/down не является media-session key и отбрасывается MediaSessionService (isMediaSessionKey), а absolute volume меняет STREAM_MUSIC напрямую. Приложение этого не видит; перехват всех изменений громкости не добавлялся (циклы/дубли). Это написано в UI и README.
  • Число касаний и сторона наушника до телефона не доходят: «2 касания» = группа play/pause, «3 касания» = next/previous (обе стороны).
  • Android шлёт кнопки последнему игравшему приложению: после музыки на самом телефоне кнопки уходят тому плееру, пока снова не заиграет звук ПК.

2. Windows: окно само открывалось после крестика

Причина: каждое открытие до создания окна ставило в очередь MainCmd::Show, а трей на клик шлёт WM_LBUTTONUP + WM_LBUTTONDBLCLK + WM_LBUTTONUP (tray.rs). Главный поток разбирал очередь только после возврата блокирующего run_window() (main.rs), поэтому каждое закрытие крестиком открывало следующее окно из очереди — примерно три раза. Исправление: открытия схлопываются (Closed/Opening/Open), а команды обрабатывает отдельный поток-супервизор, независимо от UI-цикла.

3. Общий выключатель AudioBridge

  • Телефон (переключатель в шапке): выкл. останавливает BridgeService, hub, соединения, аудио, медиасессию и постоянное уведомление. Состояние сохраняется (service_enabled, запись через commit); ни загрузка, ни обновление, ни sticky-перезапуск, ни открытие приложения службу не включают. ПК и настройки сохраняются.
  • ПК (окно + меню трея, серая иконка): выкл. закрывает Server (endpoint, телефон, захват/микрофон/media-потоки), возвращает устройства по умолчанию и сохраняет service_enabled в settings.json (нет поля = вкл). При включении поднимается тот же ключ, и QR не меняется.
  • Удалённо ничего не выключается: другая сторона просто видит устройство не в сети и переподключается сама.

Проверки

Что Результат
cargo clippy -p core -p desktop -p android --all-targets -D warnings + cargo ndk … clippy ✅
core lib (28) + rt_alloc (push/fill без аллокаций) ✅
core integration, --test-threads=1 (10, в т.ч. новый media_commands_reach_only_the_target_pc_exactly_once) ✅
desktop (17: выбор сессии, toggle по статусу, «ровно одно действие», схлопывание Show, персистентность выключателя) ✅
android-native host (26: statusJson media, PC_MEDIA коды) ✅
Android JVM (38: MediaKeysTest, ServiceSwitchTest, BridgeStatusTest, PairedPcTest) / lint 0 errors ✅
cargo build --release desktop, assembleDebug ✅

Живые проверки:

  • Эмулятор + тестовый ПК: cmd media_session dispatch pause/play/next/previous → ПК получил Media(Pause/Play/Next/Previous) ровно по одному разу. Экран выключен + forced deep Doze: PlayPause, Next дошли. В режиме «Один наушник» previous → Next, pause/play → PlayPause. Выключатель: служба, уведомление и медиасессия исчезли, ПК → WaitingForPeer; BOOT_COMPLETED/QUICKBOOT_POWERON и открытие приложения службу не подняли; включение → снова connected.
  • Windows GSMTC: временный собственный SMTC-плеер. Pause → Paused, Play → Playing, PlayPause ×2 — переключает, PcMedia репортится. Чужие сессии не трогались.
  • Окно Windows: изолированный debug-экземпляр (AUDIOBRIDGE_DEV_DIR, установленный AudioBridge не трогался). Инъекция UP/DBLCLK/UP в окно трея открыла одно окно; один X — окно не возвращалось ≥10 с; повторный запуск открывает окно. Выкл/вкл: UDP-endpoint исчезает и появляется, хэши server.key/pairing.secret/pairing.txt совпадают, после перезапуска остаётся «Выключено».

Не проверено (честно):

  • Касания реальных наушников. На реальном POCO F5 (Android 15) с подключёнными наушниками сборка PR установлена поверх 0.3.0, ПК обновлён, они соединились, и Android сделал AudioBridge сессией медиакнопок (Media button session is app.audiobridge). Сами касания (2/3 касания, режим «Один наушник») вручную не проверялись, так что реальная раскладка касаний этой модели не подтверждена.
  • Реальная Яндекс Музыка (YandexMusicBetaMod) не была запущена. Совместимость следует из кода мода (navigator.mediaSession: play→resume, pause/stop→pause, next/prev) и проверки GSMTC на собственной сессии.
  • Клики по меню трея мышью (нет разрешения на управление фоном Explorer — проверен путь через сообщения трея), подключённый телефон при выключении ПК, восстановление после захвата CABLE.
  • На x86_64-эмуляторе (arm64-библиотека через libndk_translation) native hub дважды не стартовал после переустановки APK: процесс висел на fork-потомке внутри транслятора (по debuggerd). После свежей загрузки эмулятора стартовал сразу. На main-APK за одну попытку это не воспроизвелось, поэтому связь с PR не исключена до конца, но место зависания — слой трансляции, которого на arm64-телефоне нет. Стоит проверить на телефоне.

Артефакты

Версия 0.4.0 (Rust workspace, Android versionName 0.4.0 / versionCode 4, протокол v3). AudioBridge.exe и AudioBridge.apk для GitHub Release собираются из merge-коммита в main; APK подписан тем же ключом, что и прошлые релизы.

- Protocol v3: PcRequest::Media(MediaCommand) phone -> PC, ControlMsg::PcMedia (playback, app, title, artist) PC -> phone.
- Desktop: GSMTC media transport on its own MTA thread (explicit play/pause, prefers the playing session, redundant
  commands are no-ops, SendInput media keys only without a session); reports the current session via Server::set_media.
- Android: MediaSession in BridgeService receives headset media keys, mirrors the target PC's playback state and sends
  each press exactly once to exactly one PC (chosen or automatic). Single-earbud mode maps the play/pause group and
  next/previous to user-chosen actions; volume taps are not remappable on Android and the UI says so.
- Tests: proto, statusJson, controlPc mapping, integration (target-only, exactly once, state, disconnect), desktop
  routing, JVM key mapping/target/persistence. Docs updated.
…switch

- Cause: every show request made before the window existed queued a MainCmd::Show (tray sends UP + DBLCLK + UP),
  and the main thread only drained them after the blocking window loop returned, so each X close opened the next
  queued window. Show requests now coalesce (Closed/Opening/Open); commands run on a service supervisor thread.
- Switch in the window and tray: off shuts the Server down (endpoint, phone, audio/mic/media workers), restores
  defaults, keeps identity and settings; persisted as service_enabled in settings.json (missing = on).
- Debug-only AUDIOBRIDGE_DEV_DIR for an isolated instance (data dir, logs, instance objects).
Copilot AI balanced review requested due to automatic review settings October 10, 2026 05:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Lyten02 Lyten02 changed the title Кнопки наушников управляют музыкой на ПК (протокол v3), фикс окна Windows, общий выключатель Headphone buttons control PC media (protocol v3), Windows window fix, global switch; release 0.4.0 Oct 10, 2026
@Lyten02
Lyten02 merged commit 58276de into main Oct 10, 2026
1 check passed
@Lyten02
Lyten02 deleted the Lyten02/headphones-media-control branch October 10, 2026 06:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants