MusicBee UPnP Plugin Помощь

Что нового

2.0.2 - 2026-07-20

  • Шаблон больше нельзя удалить, пока за ним следует какой-либо узел — кнопка удаления просто остается неактивной, поэтому узел никогда не останется осиротевшим. Каждая строка шаблона теперь показывает текущее количество (n) его подписчиков, что сразу объясняет неактивное удаление; удаление неиспользуемого шаблона теперь также сохраняется, вместо того чтобы поставляемые по умолчанию значения тихо появлялись при следующей загрузке.
  • Новая кнопка Воронка рядом с деревом просмотра показывает, какие именно узлы следуют за шаблоном: она фильтрует дерево, оставляя только подписчиков выбранного шаблона, повторно фильтрует при выборе других шаблонов и восстанавливает полное дерево при отключении.
  • Узлы Радио и Подкасты теперь постоянно связаны с шаблоном своей категории — измените шаблон на вкладке «Пути», и узел будет следовать за ним сам по себе; применять ничего не нужно, поэтому кнопка «Применить» для них отключена. Список шаблонов отражает это двумя группами: Стандартные (где создаются все новые шаблоны) и Зарезервированные (Радио + Подкасты).

2.0.1 - 2026-07-20

  • Шаблоны путей теперь динамически связаны с узлами, которые их используют. Применение шаблона заставляет узел следовать ему: отредактируйте шаблон позже, и каждый узел, следующий ему, немедленно изменится — не нужно искать его и повторно применять узел за узлом. Дерево представлений показывает, какому шаблону следует каждый узел сразу после его имени, переименование мгновенно отображается там, а удаление шаблона сначала сообщает вам, сколько узлов следует ему (они сохраняют свой текущий макет и просто перестают следовать чему-либо).
  • Применение шаблона к скрытому узлу также делает его снова видимым — применение является жестом «покажи мне это, оформленное так», в то время как скрытие остается с флажком «Видимый». Зарезервированный шаблон «Скрытый», который он заменяет, исчез.
  • Дерево представлений больше не теряет ваше место: отметки, развернутые папки и положение прокрутки сохраняются после применения шаблонов и других обновлений.

2.0.0 - 2026-06-16

Это первый публичный выпуск форка yaiol плагина MusicBee UPnP с открытым исходным кодом. Он представлен в двух частях: все новое для этого форка, затем исправления и улучшения, внесенные в исходный плагин. Каждый пункт сохраняет форму Что / Почему из внутреннего каталога функций проекта, так что обоснование каждого изменения находится на странице, а не только само изменение.

Новое в этом форке

MediaRenderer - воспроизведение в MusicBee

N01 - MusicBee как средство воспроизведения

Что: обычно этот плагин работает в одном направлении: телефон или другое устройство просматривает библиотеку MusicBee и воспроизводит музыку на себе. Эта функция добавляет противоположное направление - она позволяет MusicBee быть проигрывателем. Из приложения-контроллера на вашем телефоне (например, BubbleUPnP) вы можете выбрать свой настольный MusicBee в качестве устройства воспроизведения, а затем управлять им с руки: воспроизводить, приостанавливать, останавливать, пропускать вперед или назад, переходить к определенной точке в треке и изменять громкость или отключать звук.

Почему: это превращает ваш телефон в пульт дистанционного управления для музыки, уже находящейся на вашем ПК. Сядьте на диван, просматривайте свою библиотеку на телефоне, нажмите на трек, и он зазвучит из колонок, подключенных к вашему компьютеру - с полным контролем с того места, где вы сидите. Исходный плагин никогда не поставлял это как рабочую функцию.

Включение: по умолчанию отключено, потому что включение позволяет любому устройству в вашей домашней сети начать воспроизведение на вашем ПК. Вы включаете его, установив флажок на вкладке «Общие» в диалоговом окне настроек. Каждая из трех ролей плагина имеет свой собственный флажок: поделиться моей библиотекой (Сервер), позволить другим воспроизводить на меня (Рендерер) и воспроизводить на другие устройства (Точка управления) - и диалоговое окно показывает только те вкладки настроек, которые действительно нужны включенным ролям, поэтому вы никогда не столкнетесь с опциями, которые к вам не относятся.

Различение ваших машин: вы можете дать рендереру любое имя, которое вам нравится (по умолчанию "MusicBee (yaiol)"). Это имя будет отображаться в списке целей воспроизведения на вашем телефоне, поэтому, когда на нескольких ПК запущен MusicBee, вы сможете различать, какой из них какой. Изменение имени вступает в силу немедленно, без перезапуска.

Наилучший возможный звук при воспроизведении на себя: когда вы просматриваете собственную библиотеку MusicBee со своего телефона и отправляете трек обратно в тот же MusicBee, плагин распознает, что его просят воспроизвести один из его собственных файлов, и просто воспроизводит его прямо с вашего диска. Результат точный и мгновенный - бит-в-бит, с применением собственного эквалайзера и выравнивания громкости MusicBee - вместо бессмысленного вывода звука в сеть и обратно на себя.

Запуск в приватном режиме: три роли работают независимо, поэтому вы можете включить рендерер, оставив общий доступ к библиотеке выключенным. В такой конфигурации "только рендерер" ваша библиотека остается полностью скрытой от сети - объявляется только цель воспроизведения - и MusicBee никогда не будет предлагать воспроизводить на себя.


Поведение воспроизведения

N02 - 5.1 FLAC не автоматически понижается до стерео

Что: ограничение количества каналов в MediaServerDevice.GetEncodedFile было If StereoOnly OrElse Not isPcmData Then channelCount = 2. Пункт Not isPcmData молча понижал до стерео каждый не-PCM транскод (FLAC, MP3, AAC, Ogg) независимо от количества каналов источника, что делало бесполезными рендереры, поддерживающие 5.1, когда источником был 5.1 FLAC. Теперь второй пункт исключает FLAC: Not isPcmData AndAlso encoder.Codec <> FileCodec.Flac. FLAC 5.1 проходит без изменений; MP3/AAC/Ogg по-прежнему принудительно стерео, потому что командные кодировщики MusicBee для этих форматов ожидают 2-канальный вход.

Почему: весь смысл транскодирования источника 5.1 FLAC в выходной FLAC заключается в сохранении многоканального микса. Тихое понижение до стерео делало опцию транскодирования FLAC бесполезной для объемного прослушивания. С N02 это работает правильно.


Архитектура

Структурное изменение, которое делает форк жизнеспособным для большой библиотеки - отсутствует в оригинальном плагине.

N03 - Ленивое (по требованию) дерево просмотра

Что: оригинальный плагин строил все дерево просмотра при запуске MusicBee - перечислял каждый трек, выполнял полный Library_GetFileTags для каждого файла, собирал всю иерархию контейнеров - до открытия HTTP-порта. Для реальной библиотеки (50k+ треков, 5400 эпизодов подкастов, сотни станций) это минуты холодного запуска, и дерево остается в ОЗУ навсегда, включая ветви, которые ни один клиент никогда не открывает. Этот форк ничего не строит заранее: корень предоставляет один заполнитель с префиксом L: для каждой конечной точки (L:music, L:podcast, L:filter:…); каждый уровень вычисляется только тогда, когда клиент просматривает его (LazyBrowseEnsureLazyEndpointInMemory → кэши для каждого уровня), а уведомления об изменении библиотеки очищают кэши (SetLibraryDirty).

Почему: холодный запуск практически мгновенный - HTTP-порт открыт к тому времени, как MusicBee завершает инициализацию плагина - и память остается пропорциональной тому, что было просмотрено, а не размеру библиотеки. Компромисс: первый просмотр конечной точки оплачивает стоимость ее загрузки; повторный вход кэшируется до следующего изменения библиотеки. Это основа, от которой зависит все остальное. Полные примечания: FIXES.md.


Сеть и надежность

Укрепление пути привязки HTTP-сервера. Оригинальный плагин молча умирает, когда его порт недоступен.

N04 - Самовосстанавливающаяся привязка HTTP-порта

Что: HTTP-сервер плагина больше не умирает, когда его настроенный порт недоступен. Три связанных изменения:

  1. Автоматический откат при сбое привязки. HttpServer.Start пытается использовать настроенный порт, и при SocketException сканирует до 20 портов вверх в поисках первого свободного. Фактический привязанный порт записывается в новое поле Plugin.boundServerPort, и все, что рекламирует сервер - URL-адреса SSDP LOCATION (уведомления + ответы M-SEARCH), URL-адрес устройства (PrimaryHostUrl), перенаправление портов маршрутизатора и самофильтры SSDP/точки управления - теперь считывает boundServerPort вместо Settings.ServerPort. Клиенты UPnP обнаруживают реальный порт через SSDP, поэтому перемещенный порт прозрачен для рендереров.
  2. Уведомление пользователя. Когда происходит откат (сохраненный порт не используется), локализованное MessageBox (WarnPortInUse) сообщает пользователю, какой порт фактически обслуживает, и что устройства все равно найдут его - потому что плагин работает без графического интерфейса, и сообщение в диалоговом окне будет видно только тому, кто уже подозревал проблему.
  3. Восстановление после перезапуска. RestartServer (путь перезапуска при сохранении настроек) раньше слепо разыменовывал Plugin.controller / Plugin.server. Если начальный Initialise выдавал исключение до их создания (что именно и вызывал сбой привязки), следующее сохранение настроек приводило к NullReferenceException - оставляя полумертвый плагин. Теперь он воссоздает и запускает их, когда Nothing, поэтому сохранение рабочего порта оживляет плагин без полного перезапуска MusicBee.

Почему: причиной стал реальный инцидент с пользователем. Старый порт по умолчанию 49382 находится в динамическом диапазоне Windows (49152-65535), где Hyper-V/WSL2/Docker/WinNAT резервируют большие блоки, которые смещаются при каждой загрузке - поэтому привязка завершалась с WSAEACCES ("доступ запрещен") на машине, где она работала месяцами. Изменение порта по умолчанию на свободный затем столкнулось с Serviio (отдельным DLNA-сервером, уже работающим на новом порту), завершившись с WSAEADDRINUSE. Каждый сбой был проглочен в Initialise, оставляя плагин молча мертвым, а затем вызывая NRE при следующем сохранении настроек. После N04 коллизия портов самовосстанавливается - сервер продолжает работать на следующем свободном порту, пользователь уведомляется, и клиенты снова обнаруживают его - вместо того, чтобы выводить из строя весь плагин.

Реализация:

  • Порт по умолчанию перемещен 493829779 (ниже динамического диапазона, поэтому Windows никогда не резервирует его автоматически; не является известным портом по умолчанию для медиасерверов) во всех трех объявлениях ServerPort + откат при сбое анализа настроек.
  • Plugin.boundServerPort (новое общее поле) хранит активный порт прослушивания; activeServerPort остается настроенным снимком, чтобы логика значка "Требуется перезапуск" не срабатывала ложно при откате.
  • HttpServer.PortScanRange = 20; сканирование останавливается при первом успешном TcpListener.Start() и выдает последнее исключение только в случае сбоя всех попыток.
  • Новый ключ ресурса EN WarnPortInUse (переводы следуют за проходом локализации во время публикации).

N05 - Объявления SSDP через многоадресную группу (VPN / точка-точка)

Что: объявления SSDP отправляются в многоадресную группу UPnP (239.255.255.250) вместо широковещательного IP-адреса. Также подавляется безвредная ошибка "cannot access a disposed object", регистрируемая, когда ответ на поиск SSDP опережает перезапуск сервера.

Почему: на сетевых адаптерах точка-точка / VPN IP-широковещание не применяется - старая широковещательная рассылка завершалась с ошибкой "invalid argument", и объявления пропускались, поэтому плагин был невидим для клиентов на этих соединениях. Объявление в правильную многоадресную группу исправляет обнаружение именно на этих адаптерах.

Навигация по библиотеке

Эти функции были реализованы в этом форке и отсутствуют в оригинальном плагине. Они появились в результате фактического просмотра вывода плагина с реальных клиентов UPnP.

N06 - Отображение библиотеки на основе фильтров

Что: вкладки фильтров MusicBee (файлы .xautopf в папке MusicBee пользователя) становятся корневыми контейнерами UPnP в библиотеке плагина. Треки каждого фильтра затем можно просматривать в иерархии AlbumArtistSort → Album → Tracks.

Почему: пользователи с тщательно подобранными фильтрами MusicBee (например, "5-звездочные треки", "Недавно добавленные", "Классика → Барокко") ожидают найти их при просмотре плагина с клиента UPnP. Оригинальный плагин отображал только необработанное дерево библиотеки.


N07 - Подключение поля SortAlbumArtist

Что: плагин теперь считывает MetaDataType 165 (Sort Album Artist) MusicBee и использует его для группировки/сортировки исполнителей в представлениях просмотра.

Почему: hi-fi браузеры и аудиофилы используют имена исполнителей для сортировки ("Beethoven, Ludwig van" вместо "Ludwig van Beethoven") для организации библиотек. Стандартное ожидание для серьезных слушателей. Отсутствует в обоих исходных кодах.


N08 - Обработка многозначного поля AlbumArtist

Что: когда поле AlbumArtist альбома содержит несколько исполнителей, разделенных "; " (например, "yaiol; Ars Ricercata"), трек теперь отображается под каждым исполнителем в представлениях просмотра, а не под одним "Франкенштейном", объединяющим имена.

Почему: совместные альбомы и сборники должны отображаться под каждым соавтором. Без этого половина путей поиска альбома не работает.


N09 - Обложка контейнера альбома (upnp:albumArtURI)

Что: узлы контейнера альбома в ответах DIDL Browse теперь включают элемент upnp:albumArtURI, указывающий на обложку альбома.

Почему: без этого каждый альбом в представлении просмотра клиента UPnP отображает общий значок вместо обложки альбома. Визуальная подсказка для навигации; ожидается каждым современным hi-fi браузером.


N10 - Порядок треков внутри альбомов фильтра

Что: треки внутри альбома, отображаемого фильтром, теперь сортируются по номеру диска, затем по номеру трека.

Почему: стандартный порядок альбома. Без явной сортировки треки возвращались в том порядке, в котором их возвращал фильтр - обычно выглядело случайным.


N11 - Исправление дерева папок плейлистов

Что: функция LoadLibraryPlaylists (изначально Стивена Мэйалла, ~2014) не спускалась в недавно созданные папки плейлистов. Первый плейлист в каждой папке, а также любые подпапки, оказывались осиротевшими на корневом уровне.

Почему: присутствовало в оригинальном плагине одиннадцать лет. Видно в течение 30 секунд после открытия BubbleUPnP и нажатия "Плейлисты". Исправлено в yaiol путем правильной рекурсии в недавно созданные папки во время построения дерева.


N12 - Санитизация XML-недопустимых управляющих символов

Что: любой трек с тегом, содержащим управляющий символ C0 (например, 0x19 из-за неудачного кодирования - UTF-8 → Latin-1 → обратное усечение 0x99 в 0x19), приводил к сбою всего ответа Browse с Action Failed, как только плохой трек попадал в постраничный пакет.

Почему: XML 1.0 запрещает большинство управляющих символов C0, и XmlWriter выдает исключение при попытке их записи. Присутствовало в оригинальном плагине. Исправлено путем удаления недопустимых символов на каждой точке выхода Library_GetFileTags с помощью XmlConvert.IsXmlChar.


N13 - Детерминированный список радиостанций при постраничном просмотре

Что: просмотр контейнера "Радио" попадал в общую ветвь списка файлов, которая вызывала files.Sort(AlbumFileComparer) при каждом вызове. Записи радио имеют пустые теги "Альбом/Диск/Трек", поэтому каждое сравнение возвращало 0 - List(Of T).Sort нестабилен, производя разный порядок при каждом вызове. Точки управления UPnP разбивают на страницы (BubbleUPnP получает 0..15, затем 16..конец); между двумя вызовами список перетасовывался, поэтому некоторые станции появлялись на обеих страницах (дубликаты), а некоторые ни на одной (отсутствующие) - выглядя случайным образом при каждом обновлении.

Почему: присутствовало в оригинальном плагине (его автор никогда не просматривает радио через UPnP). Исправлено здесь с помощью выделенной ветви ContainerCategory.Radio в Browse, без сортировки при каждом вызове; radioFiles сортируется один раз при загрузке по названию (стабильно). Постраничный просмотр теперь видит детерминированный порядок; страница 1 и страница 2 не пересекаются.


N14 - UPnP Search для класса альбомов возвращает контейнеры альбомов

Что: поиск UPnP для запросов класса альбомов (upnp:class = "object.container.album.musicAlbum", например, "Случайные альбомы" BubbleUPnP) возвращал полный список треков вместо контейнеров альбомов, поэтому клиент показывал ноль альбомов. Исходный обработчик анализировал только критерии в скобках, затем выгружал все треки независимо от запрошенного класса.

Почему: исправлено здесь - запросы класса альбомов теперь перечисляют отдельные альбомы (сгруппированные по AlbumArtist+Album) и выдают каждый как правильный контейнер musicAlbum с обложкой, доступный через виртуальное пространство ID Salb<idx>, чтобы клиент мог углубиться в результат и воспроизвести его.


N15 - Рабочий, учитывающий область действия поиск UPnP с возможностью перехода

Что: оригинал не рекламировал никаких возможностей поиска (GetSearchCapabilities возвращал пустое значение), поэтому клиенты даже отказывались отправлять поиск; а старый бэкэнд считывал данные из musicFiles, постоянно пустого в эпоху ленивого дерева. Этот форк рекламирует реальные доступные для поиска свойства, реализует поиск треков по названию и альбомов по названию в ленивой библиотеке (HandleLazySearch), ограничивает запрос текущей ветвью клиента, когда отправляется реальный ID контейнера (в противном случае подставляет L:music, чтобы поиск в верхней строке не подтягивал шум подкастов/радио/аудиокниг), и делает результаты альбомов кликабельными через синтетические ID Ssrch_alb_*, которые ранняя ветвь Browse сопоставляет с треками альбома. (Часть, касающаяся результатов класса альбомов как контейнеров, это N14.)

Почему: поиск в BubbleUPnP изменился с "Библиотека не поддерживает поиск" на возврат полезных, ограниченных по области действия, воспроизводимых результатов. Полный дизайн + отклоненные подходы: SEARCH.md.


N16 - Инвалидация кэша UPnP (SystemUpdateID)

Что: оригинал возвращал постоянный SystemUpdateID=0 - контракт UPnP ContentDirectory на инвалидацию кэша - поэтому клиенты, соответствующие спецификации (BubbleUPnP), рассматривали библиотеку как неизменную: устаревшие результаты просмотра, 404 миниатюры после изменения схемы URL и танец "перезапустите MusicBee дважды, чтобы увидеть изменения". Этот форк инициализирует SystemUpdateID из секунд эпохи при загрузке (так что каждый перезапуск строго опережает предыдущий) и увеличивает его при каждой мутации библиотеки и изменении настроек (SetLibraryDirty / ResetCacheBumpSystemUpdateId).

Почему: клиенты надежно подхватывают изменения, новые файлы и изменения настроек при следующем просмотре. Известное ограничение: подписанные клиенты не получают активно новое значение через GENA (отложено на будущее); они по-прежнему видят его при следующем просмотре.


N17 - Обложки подкастов

Что: плитки подкастов не отображали изображения - каждый запрос /PodcastThumbnail/ возвращал 404. Две связанные ошибки: цепочка разрешения никогда не проверяла фактический кэш обложек MusicBee (%LocalAppData%\MusicBee\InternalCache\Subscriptions\<name>.jpg, откуда загружается настольный интерфейс), а слой HTTP unescape+lowercase искажал ключ маршрута URL-адреса канала до последнего сегмента пути. Этот форк разрешает обложки из InternalCache MB и маршрутизирует запросы через URL-безопасный slug, который остается нетронутым на уровне HTTP (PodcastSlug / podcastSubIdBySlug).

Почему: обложки подписок теперь отображаются в представлениях просмотра (все 22 ранее возвращавших 404 запроса разрешены).


N18 - Иерархический (с разделителями) просмотр тегов

Что: любое поле может быть помечено как иерархическое на вкладке "Параметры библиотеки" и ему может быть присвоен односимвольный разделитель (выбор поля + поле разделителя с кнопками "добавить/удалить", сохраняется в настройках плагина). Установите Группировка на / и значение, например Jazz/Cool Jazz, затем просмотр будет выглядеть как Jazz › Cool Jazz вместо одной плоской записи. Треки, помеченные точно на ветви (просто Jazz), получают свой собственный узел [Jazz], чтобы ничего не скрывалось, ветвь с одним дочерним элементом сворачивается сама по себе, а ; отклоняется как разделитель, потому что это собственный разделитель многозначных полей MusicBee.

Почему: глубокие таксономии тегов, которые пользователь уже закодировал в одном поле (деревья жанров, иерархии настроений, "Классика/Барокко/Концерт"), наконец-то просматриваются как дерево, которое описывает тег, вместо плоской стены строк, разделенных косой чертой, которые пользователь должен читать от начала до конца.


N19 - Единый корневой путь, помеченный по полю группировки

Что: единый путь просмотра в корне помечен по полю группировки (например, "Жанр"), а не по полному короткому пути, что соответствует тому, как именуются объединенные группы первого поля.

Почему: дерево просмотра читается последовательно - одно правило именования, независимо от того, стоит ли корневая запись отдельно или была объединена с братьями и сестрами (N20) - вместо того, чтобы одиночная корневая запись показывала подробный внутренний путь, в то время как ее объединенные соседи показывали чистое имя поля.


N20 - Объединение путей просмотра, имеющих общее первое поле

Что: два пути просмотра, имеющие одно и то же первое поле - "Жанр / Исполнитель альбома для сортировки" и "Жанр / Люди подкаста" - сворачиваются в одну корневую папку Жанр, которая сначала перечисляет значения жанров, а затем разделяется на два представления, вместо двух почти дублирующихся записей "Жанр / ..." рядом друг с другом в корне.

Почему: пользователь с несколькими связанными представлениями, вложенными под общим полем, видел, что корень загроможден почти идентичными записями верхнего уровня. Объединение их делает корень читаемым и группирует связанные представления там, где им место - под их общим полем.


N21 - Пути просмотра по категориям (Стандартный / Радио / Подкаст)

Что: каждый путь просмотра типизируется по категории - Стандартный, Радио или Подкаст. Список шаблонов сгруппирован в эти три раздела, выбор поля для каждого шаблона предлагает только те поля, которые данные этой категории могут фактически предоставить, и шаблон может быть применен только к соответствующим узлам в дереве просмотра (несовместимые узлы становятся серыми и не могут быть отмечены). Зарезервированные шаблоны "Радио" и "Подкасты" не могут быть удалены, поэтому их раздел категорий никогда не исчезает.

Почему: без типизации пользователь мог бы создать макет, который молча оказывался пустым - у радиостанции нет "альбома", у эпизода подкаста нет "исполнителя альбома" - и обнаруживал это только при просмотре мертвой папки с клиента UPnP. Ограничение меню полей и целей применения реальными данными категории делает невозможным создание пустых макетов.


N22 - Группировка подкастов по году публикации

Что: дата публикации каждого эпизода подкаста считывается, поэтому путь просмотра подкаста с уровнем "Год" распределяет эпизоды по годам вместо того, чтобы сворачивать их под одним "Неизвестно".

Почему: большие подписки на подкасты становятся навигационными по годам, как и остальная часть библиотеки, вместо того, чтобы каждый эпизод попадал в одну недатированную кучу, потому что плагин никогда не смотрел на дату публикации каждого эпизода.


N23 - Сворачивание уровней группировки с одним результатом

Что: уровень группировки, который разрешается в одно значение - уровень "Тип записи", показывающий только "LP" для исполнителя, который делал только LP, или буквенный уровень с одной буквой - пропускается автоматически, перенося пользователя прямо к его содержимому.

Почему: просмотр папки, содержащей ровно одну папку, является чистым трением. Сворачивание уровня с одним выбором устраняет мертвый клик, не меняя того, что пользователь может достичь.


N24 - Группировка/поиск по году по полю даты MusicBee

Что: условие года больше не запрашивает полное поле даты "Год" MusicBee с голым четырехзначным значением, а жестко закодированное псевдоним поля года исчезло, так что каждое поле группировки теперь разрешается универсально из определения пути.

Почему: для библиотек, у которых тег "Год" содержит полную дату, группировка или поиск по году ранее ничего не возвращали - четырехзначный запрос никогда не совпадал с полем полной даты. Запрос правильного поля снова позволяет находить треки по году группировки и поиска.


N25 - Отдельные поля группировки "Год" и "Год (гггг)"

Что: пути группировки и просмотра альбомов теперь отображают оба поля года MusicBee - Год (полный тег даты) и Год (гггг) (только четырехзначный год) - так что пользователь может выбрать любое из них при определении группировки альбомов или пути просмотра.

Почему: эти два поля означают разные вещи в MusicBee, и их объединение приводило к потере этого различия. Отображение обоих позволяет пользователю собирать все релизы одного года вместе (гггг) или сохранять точный порядок по дате (полный тег года), как они задумали.


N32 - Закрепленные фильтры и плейлисты сгруппированы по типу в корне

Что: закрепленный фильтр теперь появляется непосредственно под папкой Фильтры в корне обзора, а закрепленный плейлист — непосредственно под папкой Плейлисты, вместо того чтобы все закрепления собирались в одну кучу в конце корня. Каждый закрепленный ярлык находится рядом со своим типом.

Почему: чем больше ярлыков закрепляет пользователь, тем труднее просматривать единую замыкающую кучу из перемешанных фильтров и плейлистов, и она отделяет каждый ярлык от папки, к которой он относится. Группировка закрепленных элементов под собственной категорией сохраняет корень читаемым, а каждый ярлык — рядом с тем, к чему он относится.

Диалоговое окно настроек и упаковка

N26 - Разделенное диалоговое окно настроек

Что: страница "Настройки" получила левую навигацию с разделами: Общие / Воспроизведение / Библиотека / Профили устройств / Диагностика.

Почему: оригинал представлял собой один длинный плоский список всех настроек - хорошо для разработчика, который его создал, но сбивал с толку всех остальных. Разделение группирует связанные параметры и делает диалоговое окно более похожим на современные настройки приложений.


Переименование сборки + плагина (без F-id - примечание по упаковке)

Что: скомпилированный DLL называется mb_UPnP_yaiol.dll, а плагин сообщает о себе как "MusicBee UPnP (yaiol)". Отличается от оригинального mb_Upnp.dll.

Почему: пользователи могут установить yaiol рядом с оригинальным плагином и сравнивать поведение бок о бок.


Система значков - отображение состояния выполнения (механизм, лежащий в основе F40)

Что: общий шаблон пользовательского интерфейса для отображения важных условий выполнения в виде видимых цветных значков в диалоговом окне настроек. Текущие экземпляры:

  • ⚠ Макс. подключений (N04) - срабатывает, когда ограничение максимального количества подключений было достигнуто хотя бы один раз с момента запуска MusicBee. Флаг постоянной сессии Plugin.MaxConnectionsHit. Устанавливается внутри WaitOnSendBarrier, когда нет свободного слота.
  • ⚠ Требуется перезапуск - срабатывает, когда сохраненная настройка требует перезапуска MusicBee для вступления в силу. Флаг постоянной сессии Plugin.RestartRequired. Устанавливается в обработчике сохранения диалогового окна, когда новое сохраненное значение отличается от снимка времени выполнения (Plugin.activeMaxConnections, Plugin.activeServerPort, Plugin.activeIpAddress). Настройки, требующие перезапуска, ограничены теми, которые действительно не могут быть перезагружены "на лету" - параметры привязки HTTP-сервера и SemaphoreSlim, созданный один раз при инициализации.

Почему: файл журнала плагина хорош для технических пользователей, занимающихся отладкой, но нетехнический пользователь, столкнувшийся с "устройство звучит неправильно" или "воспроизведение медленное", никогда не откроет Диагностика → Просмотр журнала. Значки фиксируют случаи, когда пользователю необходимо знать, что что-то произошло, и отображают это при следующем открытии плагина - обнаруживаемо без чтения чего-либо.

Многоразовое использование в будущем:

  • Обнаружено несоответствие профиля (пользовательский агент устройства никогда не соответствовал ни одному профилю, произошел откат к Generic).
  • Сработал откат NextURI (F13 - бесшовное воспроизведение отключено для сессии на нестабильном устройстве).
  • Сбой/частичное сканирование библиотеки.
  • Потеряно соединение с рендерером в середине сессии.
  • Любое другое условие, когда "произошло один раз, пользователь должен знать" лучше, чем "записано молча среди 1000 других строк".

Соглашения по реализации:

  • Метки значков находятся на уровне диалогового окна (не внутри какой-либо панели), поэтому они видны независимо от того, в каком разделе находится пользователь.
  • Расположены в нижнем ряду рядом с кнопками "Сохранить/Отмена" (текущее: y=410, горизонтально).
  • Каждый значок имеет соответствующий флаг постоянной сессии в Plugin, который становится True при возникновении условия и сбрасывается только при перезапуске MusicBee.
  • Ресурсы: <Condition>Badge (текст метки, с префиксом ⚠) + <Condition>BadgeTip (подсказка, объясняющая причину + способ устранения).
  • Для значков "сохраненная настройка требует перезапуска" сделайте снимок времени выполнения в Plugin.Initialise() и сравните с Settings.* после Settings.SaveSettings() в обработчике сохранения диалогового окна.

N27 - Отмена отменяет изменения пути/шаблона

Что: изменения, внесенные в пути и шаблоны в диалоговом окне настроек, теперь отменяются, когда пользователь нажимает "Отмена", вместо того, чтобы тихо применяться, и любой зарезервированный шаблон, удаленный во время сеанса, воссоздается. (Шаблоны в противном случае сохраняются в реальном времени по мере их редактирования - на вкладке "Пути" нет отдельной кнопки "Сохранить".)

Почему: "Отмена" должна означать отмену. Ранее пользователь, который экспериментировал с изменениями пути/шаблона и отменял их, обнаруживал, что изменения уже были зафиксированы, без возможности отменить их, кроме как вручную.


N28 - Буквальные амперсанды в меню выбора поля

Что: поле, имя которого содержит "&" - например, "Настроение & Контекст" - отображает амперсанд буквально в меню выбора поля вместо того, чтобы поглощать его как префикс Alt-мнемоники.

Почему: имена полей с амперсандом отображались неправильно (символ исчезал, а следующая буква становилась акселератором), что затрудняло распознавание пункта меню.


N29 - Стабильный, непереводимый заголовок окна настроек

Что: заголовок окна настроек фиксирован на строке бренда "MusicBee UPnP Plugin" и больше не меняется в зависимости от языка интерфейса; строка DialogTitle для каждого языка была удалена из каждого пакета локализации.

Почему: заголовок окна, который менял формулировку в зависимости от языка, был переводимой поверхностью без пользы - заголовок является торговой маркой. Закрепление его делает его стабильным и последовательным везде.

Локализация

Оригинальный плагин только на английском языке. Этот форк полностью локализуем - каждая строка, видимая пользователю, проходит через пакет ресурсов, и плагин автоматически определяет язык пользовательского интерфейса MusicBee.

N30 - Многоязычный интерфейс (переводы ожидаются)

Что: механизм локализации завершен и поставляется. Localisation.vb считывает выбранный язык MusicBee из MusicBee3Settings.ini (эндоним <SystemLanguage>) и применяет соответствующую культуру .NET к потоку, так что My.Resources.Resources.* возвращает локализованную строку. Каждая метка/кнопка/сообщение, видимое пользователю, привязано к ключу ресурса (элементы управления дизайнера через ApplyDesignerExtras + sync-en-locale.js; строки времени выполнения, такие как WarnPortInUse, добавлены вручную). Что не сделано пока, так это фактический перевод: существует только пакет исходных английских ресурсов (Resources.resx) - сателлитные пакеты для других языков будут созданы одним пакетным проходом, когда плагин будет полностью функционален (перевод по частям, пока строки все еще меняются, тратит усилия).

Целевые языки (набор, предлагаемый самим MusicBee, сопоставленный 1:1 с endonymToCulture, так что плагин автоматически следует языку MusicBee):

Арабский (ar) Чешский (cs) Немецкий (de) Греческий (el)
Испанский (es) Французский (fr) Венгерский (hu) Итальянский (it)
Корейский (ko) Нидерландский (nl) Норвежский (nb) Польский (pl)
Португальский BR (pt-BR) Португальский PT (pt-PT) Шведский (sv) Турецкий (tr)
Украинский (uk) Русский (ru) Японский (ja) Упрощенный китайский (zh-CN)
Традиционный китайский (zh-TW) Английский (en, источник)

Политика вариантов (согласно правилу локали рабочего пространства): PT и ZH разделены на отдельные пакеты, потому что лексика/письменность действительно различаются (pt-BR/pt-PT, zh-CN/zh-TW). EN - это один пакет - "Английский (США)" MusicBee (en-US) возвращается к en через цепочку культур .NET, поэтому отдельный пакет для США не создается. ES и FR также являются одноязычными.

Почему: настройки плагина UPnP ("не использовать необработанный PCM", "принудительно использовать PCM с младшим байтом вперед", предупреждения об откате порта) достаточно загадочны на родном языке. Следование языку пользовательского интерфейса MusicBee - вместо принудительного английского - это разница между инструментом, который неанглоязычный пользователь может настроить, и тем, который он не может. Ни один из исходных проектов не пытался этого сделать.


N31 - Ссылка на справку открывается на полном языке интерфейса

Что: открытие ссылки на справку из плагина учитывает полный язык интерфейса пользователя (например, pt-BR, zh-CN) вместо того, чтобы сворачиваться до базового языка, и отправляет более четкий идентификатор проверки обновлений.

Почему: пользователь, запускающий MusicBee в региональном варианте (бразильский португальский, упрощенный китайский), отправлялся на страницу справки на базовом языке. Передача полной культуры приводит его на страницу справки на том же языке, который он использует.

Исправления и улучшения оригинального плагина

Основной протокол и воспроизведение

F01 - Обновленные профили устройств DLNA по умолчанию

Что: поставляются свежие профили по умолчанию для PlayStation 4, Xbox 360/One и современного BubbleUPnP, с флагами возможностей (частоты дискретизации, битовая глубина, кодеки), которые отражают то, что эти устройства фактически поддерживают сегодня.

Почему: настройки по умолчанию оригинального плагина были заморожены примерно в 2014 году. PS4/Xbox/BubbleUPnP с тех пор получили поддержку аудио высокого разрешения. Из коробки новая установка воспроизводит наилучшее качество на этих устройствах без вмешательства пользователя в настройки профиля устройства.


F02 - Управление устройствами, которые рекламируют MediaRenderer:3

Что: плагин проверяет описание службы UPnP рендерера, чтобы решить, может ли MusicBee управлять им. Оригинал соответствовал только urn:schemas-upnp-org:device:MediaRenderer:1. Современные устройства рекламируют :2 или :3. F02 расширяет соответствие.

Почему: без этого недавние устройства Sonos / WiiM / Eversolo просто не отображаются в списке устройств "Воспроизвести на" MusicBee - хотя они говорят на том же протоколе. Исправление одного совпадения префикса строки разблокирует все современное поколение устройств.


F03 - Опция "Принудительный нативный поток" для каждого профиля (по умолчанию ВКЛ)

Что: при установке флажка плагин отправляет исходные байты файла на устройство без транскодирования, без DSP, без обработки ReplayGain. Просто исходный файл, выбранный пользователем, байт за байтом (за исключением HTTP-кадрирования).

Почему: по отзывам на форумах, это самое большое улучшение качества воспроизведения. Hi-fi пользователи, покупающие дорогие рендереры, явно хотят бит-в-бит вывод; любое вмешательство DSP сводит это на нет. По умолчанию ВКЛ, потому что большинство современных устройств обрабатывают любой кодек, который им подсунул пользователь, а ReplayGain/EQ должны быть опциональными. Это для каждого профиля, поэтому вы можете продолжать транскодирование для старого Xbox, отправляя нативный поток на hi-fi ЦАП.


F04 - "Принудительное транскодирование" для каждого профиля

Что: переопределение для каждого профиля, которое принудительно пропускает каждый поток на это устройство через транскодер, независимо от поддержки нативного кодека. Обратное F03 (ForceNativeStream). Взаимоисключающее с F03 - пользовательский интерфейс автоматически снимает флажок с другого, когда один из них включен.

Почему: один глобальный переключатель противоречил бы опции ForceNativeStream (F03) для каждого профиля. Реальный случай: устройство A - это hi-fi ЦАП, которому нужны бит-в-бит нативные потоки; устройство B - старый AV-ресивер, который задыхается от FLAC. С глобальным переключателем пользователю пришлось бы выбирать - за счет другого устройства. С профилем для каждого устройства получается правильный ответ.

Реализация:

  • StreamingProfile.ForceTranscoding As Boolean = False.
  • Схема сохранения данных обновлена до v9. Файлы до v9 загружают устаревшее глобальное значение один раз и копируют его во все профили, сохраняя старое поведение при обновлении.
  • UI: удалено с панели диагностики, добавлено в раздел "Профили устройств" рядом с ForceNativeStream. Двусторонние обработчики взаимоисключения (CheckedChanged на каждом отписывает другой перед переключением, чтобы избежать бесконечного цикла).
  • Место принятия решения: Settings.ForceTranscodingstreamingProfile.ForceTranscoding в WriteAudioFileDIDL.

F05 - "Принудительный PCM с младшим байтом вперед" для каждого профиля

Что: потоки PCM (типы mime L16/L24) по спецификации являются big-endian. Некоторые устройства ошибочно ожидают little-endian и воспроизводят белый шум, когда им передаются правильные big-endian данные. F05 переключает порядок байтов для каждого профиля.

Почему: без этого некоторые устройства выдают стену статики. Симптом драматичен, а причина невидима без знания кодирования PCM - переключатель дает пользователям возможность угадать и проверить исправление.


F06 - "Не использовать Raw PCM" для каждого профиля

Что: когда устройство заявляет, что поддерживает необработанный PCM, плагин использует его. Некоторые устройства лгут - они принимают рукопожатие SOAP, но искажают фактические необработанные данные PCM, при этом правильно обрабатывая PCM, обернутый в контейнер WAVE. F06 принудительно использует PCM-over-Wave независимо от того, что рекламирует устройство.

Почему: некоторые модели Marantz, в частности, рекламируют необработанный PCM, но работает только WAVE. Без этого необработанные потоки PCM искажаются, без сообщения об ошибке, указывающего на причину.


F07 - "Длина содержимого" для каждого профиля

Что: какое значение отправлять в HTTP-заголовке Content-Length. Четыре варианта:

  • По умолчанию - фактическое количество байтов, если известно, опустить, если неизвестно.
  • Нет - никогда не отправлять заголовок (только фрагментированное кодирование).
  • Только PCM - отправлять только для необработанного PCM; опускать для всего остального.
  • Фиксированный - отправлять UInt32.MaxValue - 8192 (маркер для "очень большой неизвестной длины").

Почему: устройства UPnP/DLNA сильно различаются в своей реакции на Content-Length. Некоторым требуется точное число, некоторые ненавидят его в потоках, некоторым требуется маркер "очень большое" значение для поддержания буферизации. Позже это было расширено с только PCM на все выходные форматы, потому что те же проблемы появились в транскодированных потоках MP3/AAC.


F08 - "Не очищать NextURI" для каждого профиля

Что: обычно плагин очищает NextURI устройства, когда очередь опустошается (отправляя SetNextAVTransportURI с пустым URL). Некоторые устройства (в частности, Denon) интерпретируют пустой NextURI как "остановить все" и немедленно прекращают воспроизведение. F08 предотвращает очистку плагином.

Почему: без этого владельцы Denon сталкиваются с тем, что устройство обрывает воспроизведение в середине трека, когда очередь опустошается. С установленным F08 устройство сохраняет устаревший NextURI в памяти (безвредно - он просто перезаписывается в следующий раз, когда что-то ставится в очередь).


F09 - FLAC как выходной формат транскодирования

Что: выпадающий список форматов транскодирования в профилях устройств теперь предлагает FLAC наряду с PCM 16/24, MP3, AAC, Ogg. Выбор его направляет кодировщик BASS через стандартную командную строку конвертации FLAC MusicBee (тот же механизм, который уже используют MP3/AAC/Ogg).

Почему: для устройств, которые хорошо обрабатывают FLAC, но не могут декодировать исходный кодек (например, Eversolo, получающий библиотеку WMA MusicBee, преобразованную в FLAC), это сохраняет качество без потерь, где MP3/AAC отбросили бы аудиоданные. Разблокирует N02 (управление понижением 5.1), которое нельзя было решить без опции транскодирования без потерь.

Реализация: добавление одной строки в блок Select Case Codec Encoder.StartEncode - FLAC присоединяется к MP3/AAC/Ogg в ветви, управляемой командной строкой. Выпадающий список UI получает "FLAC" в качестве 6-й опции. Сопоставление загрузки/сохранения в SettingsDialog расширяется для распознавания FileCodec.FlacSelectedIndex = 5. Mime, тип DLNA и функция кодирования уже были подключены в ItemManager.GetMimes / GetDlnaType / GetEncodeFeature из более ранней работы (F21, F26).


Бесшовное воспроизведение (SetNextAVTransportURI)

F10 - SetNextAVTransportURI / NextURI core

Что: истинное бесшовное воспроизведение. Когда устройство объявляет поддержку SetNextAVTransportURI в своем описании службы UPnP, плагин предварительно ставит следующий трек в очередь на устройстве до того, как закончится текущий. Устройство переключается внутренне без слышимого разрыва между треками - то, что вы слышите на CD-плеере. Это не хак "непрерывного потока" (который объединяет все в один длинный поток и теряет метаданные для каждого трека).

Почему: флагманская функция Tier-2. Альбомы, записанные как непрерывное живое выступление (живые записи, классические произведения, диджейские сеты), звучат неправильно, когда между треками есть полусекундная тишина. Правильное решение этой проблемы является флагманской функцией, теперь в этом форке.

Примечания: аудио в очереди обслуживается HTTP-сервером плагина с использованием streamHandle=0 (режим получения библиотеки), что означает, что аудиодвижок MusicBee не участвует в воспроизведении трека в очереди. Компромисс: эффекты ReplayGain/DSP/EQ не применяются к следующему треку. Приемлемо, когда включена опция "принудительный нативный поток" (по умолчанию).


F11 - "Отключить поддержку NextURI" для каждого профиля

Что: даже если устройство рекламирует SetNextAVTransportURI, этот флажок заставляет плагин игнорировать это объявление и возвращаться к воспроизведению по одному треку за раз.

Почему: некоторые устройства рекламируют NextURI, но имеют ошибочную реализацию (сбои, частичные переходы, зависания). Вместо того, чтобы реверс-инжинирить каждое сломанное устройство, пользователь получает переключатель "просто выключить здесь".


F12 - Жизненный цикл NextURI в списке воспроизведения

Что: когда MusicBee вызывает NowPlayingListChanged, плагин переоценивает, что должно быть поставлено в очередь для бесшовного перехода. Он запрашивает у MusicBee новый "следующий" трек через NowPlayingList_GetNextIndex(1) + NowPlayingList_GetListFileUrl, сравнивает с тем, что в данный момент находится в очереди на устройстве (отслеживается через новое поле nextPlaySourceUrl), и снова ставит в очередь, если оно изменилось (или очищает очередь, если MusicBee говорит, что следующего трека нет).

Почему: без F12 устройство продолжало воспроизводить устаревший NextURI, когда пользователь удалял/переупорядочивал трек в очереди. Исторически это занимало несколько итераций, потому что каждая мутация списка требует разной обработки - мы упростили, доверяя NowPlayingList_GetNextIndex (который уже учитывает перемешивание и повтор всего списка), поэтому все варианты проходят через одно и то же сравнение.

Реализация:

  • Новое поле nextPlaySourceUrl хранит URL-адрес библиотеки MusicBee того, что находится в очереди (URL-адрес потоковой передачи с суффиксом дескриптора не сопоставим с путем библиотеки).
  • Новая Public Sub RefreshQueuedNextUri() в MediaRendererDevice. Три результата: NextURI не в очереди → бездействует; в очереди соответствует новому "следующему" → бездействует; в очереди отличается → вызов QueueNext с новым URL (или QueueNext("") для очистки - что учитывает F08 DoNotClearNextUri).
  • Подключено в Plugin.ReceiveNotification под NotificationType.NowPlayingListChanged.

F13 - Откат при сбое NextURI

Что: после 4 последовательных сбоев SetNextAVTransportURI на одном и том же устройстве плагин отключает бесшовное воспроизведение для этого устройства до перезапуска MusicBee.

Почему: если устройство действительно неисправно для NextURI (периодические ошибки SOAP, сбои сети), плагин в противном случае будет продолжать повторять попытки на каждом треке. F13 останавливает шум и молча возвращается к воспроизведению по одному треку за раз.


F14 - Режим повтора + интеграция NextURI

Что: F14 разделяется на два случая, обрабатываемых детектором перехода F15 в OnAvTransportStatusCheck:

  • Повтор всего: MusicBee сам передает правильный URL "обертки" (трек 1 в конце списка) в Plugin.QueueNext. Специальная логика плагина не требуется - устройство переходит к нему, и детектор F15 вызывает Player_PlayNextTrack как обычно, что возвращает индекс NPL MusicBee к 0.
  • Повтор одного: MusicBee передает ОДИН И ТОТ ЖЕ URL трека в Plugin.QueueNext. Устройство переходит к нему (новый дескриптор потока, тот же источник). Детектор F15 теперь запрашивает Player_GetRepeat() - если это RepeatMode.One, он пропускает вызов Player_PlayNextTrack, чтобы MusicBee не продвигал индекс NPL от зацикленного трека.

Почему: без пропуска Repeat-One вызов Player_PlayNextTrack при бесшовном переходе продвинул бы MusicBee к следующему треку в списке (Repeat-One влияет только на поведение автоматического перехода в конце трека в пользовательском интерфейсе проигрывателя - Next Track всегда движется вперед), что противоречило бы значению Repeat-One.

Предупреждение о количестве воспроизведений: в режиме Repeat-One увеличение количества воспроизведений зависит от того, заметит ли MusicBee 3.7.9563+ зацикленное воспроизведение. Старые версии MusicBee правильно воспроизводят бесшовный повтор, но пропускают увеличение количества воспроизведений. Документировано; не блокирует.


F15 - Конечный автомат обнаружения перехода трека

Что: когда устройство внутренне переходит от текущего трека к NextURI, плагин должен это заметить и сообщить MusicBee, чтобы он продвинул свой индекс воспроизведения. В противном случае MusicBee думает, что он все еще на предыдущем треке, и количество воспроизведений / пользовательский интерфейс / скробблинг расходятся.

Реализация: опрашивает GetPositionInfo.TrackURI при каждом тике таймера состояния. Когда сообщаемый URI совпадает с тем, который мы поставили в очередь через NextURI, мы вызываем Player_PlayNextTrack в MusicBee и устанавливаем suppressNextSoapCall, чтобы результирующий PlayToDevice не отправлял SetAVTransportURI повторно (что прервало бы бесшовное воспроизведение).

Почему: без F15 устройство воспроизводит следующий трек, но пользовательский интерфейс MusicBee говорит, что он все еще на предыдущем. Запутанно, нарушает скробблинг, нарушает отслеживание количества воспроизведений. Обнаружение перехода трека требует длительной итерации для каждого рендерера, потому что каждая марка рендерера имеет свои особенности в момент сообщения об изменении URI (некоторые сначала сообщают TRANSITIONING, некоторые сразу переходят к PLAYING с новым URI, некоторые имеют короткий STOPPED между ними).

Примечания: наша первая версия работает на рендерере BubbleUPnP. Краевые случаи для каждого устройства остаются в B6.


F16 - Исправление щелчка при бесшовном переходе

Что: щелчок происходит, когда формат источника (частота дискретизации / каналы / кодек) трека в очереди отличается от текущего воспроизводимого трека, что заставляет ЦАП устройства повторно блокироваться при переходе. F16 добавляет диагностику NextUri:FormatChange, которая срабатывает во время постановки в очередь всякий раз, когда форматы отличаются, называя обе стороны - так что пользователи, слышащие щелчки, могут сопоставить.

Диагностика также указывает на смягчение: установите флажок ForceTranscoding в профиле устройства. Это гомогенизирует каждый трек до одного кодека/частоты дискретизации/битовой глубины транскодирования, полностью устраняя разницу в формате источника.

Почему отложено для фактического исправления транскодирования для соответствия: структурное исправление (транскодирование трека в очереди для соответствия формату воспроизводимого трека) требует изменений в схеме URL HTTP-сервера плагина - в настоящее время /encode/{id}0.{ext} обслуживает файл в очереди нативно. Будущая версия F16 добавит маршруты /encode/{id}0_{rate}_{depth}.ext для каждого формата и подключит их через кодировщик. Это более крупное архитектурное изменение, которое стоит сделать, если реальное устройство демонстрирует щелчок после того, как ForceTranscoding не помогает.

Реализация сегодня:

  • Поле lastSourceUrl отслеживает текущий воспроизводимый URL-адрес источника.
  • QueueNext считывает FilePropertyType.SampleRate/Channels/Kind для текущего и поставленного в очередь треков и регистрирует NextUri:FormatChange при несоответствии.

F17 - Пересинхронизация полосы прокрутки после перемотки

Что: функция Seek() уже вызывала GetPlayPositionInformation() после успешного SOAP-запроса Seek, что исправляет случай "полной рассинхронизации". F17 устраняет оставшийся дрейф до 1 секунды, вызванный квантованием RelTime UPnP в 1 секунду: когда сообщаемая позиция устройства округляется до 1 секунды от запрошенной пользователем цели, плагин теперь доверяет субсекундному точному значению пользователя вместо усечения устройства. Только когда устройство сообщает что-то кардинально отличающееся (отклонение >1 с), мы используем его значение (поиск приземлился где-то в другом месте, чем запрашивалось, например, привязка к ключевому кадру на некоторых кодеках).

Почему: без этого перемотка до 2:30.500, привязанная к отчету устройства "2:30", приводила бы к тому, что полоса прокрутки показывала бы отставание примерно на 500 мс от реальности. После F17 полоса соответствует намерению пользователя для обычного случая прокрутки внутри трека и по-прежнему учитывает отчет устройства для исключения привязки к ключевому кадру.


F18 - Взаимоблокировка непрерывного потока / NextURI

Что: теперь действуют две взаимоблокировки:

  1. Время выполнения: QueueNext возвращает False на ранней стадии, когда Settings.ContinuousOutput включен. Непрерывный поток - это собственный механизм бесшовного воспроизведения (один длинный объединенный поток); отправка SetNextAVTransportURI поверх него сбивает устройство с толку относительно того, является ли каждый трек отдельным URI или частью непрерывного потока.
  2. Пользовательский интерфейс: когда пользователь устанавливает глобальный флажок непрерывного потока, forceNativeStream текущего отображаемого профиля автоматически снимается. Непрерывный поток всегда транскодируется, поэтому принудительный нативный поток бессмыслен в комбинации.

Почему: предотвращает одновременное включение пользователем двух конфликтующих механизмов бесшовного воспроизведения. Без F18 устройство получало бы как URI непрерывного потока, так и NextURI для каждого последующего трека, с неопределенным поведением в зависимости от рендерера.


F19 - Ошибки пустого NextURI игнорируются

Что: когда SetNextAVTransportURI вызывается с пустым URL (например, последний трек в списке), некоторые устройства возвращают ошибку SOAP. F19 молча проглатывает их - регистрируются, но не распространяются как ошибки.

Почему: условие "нет следующего трека" является нормальным, а не ошибкой. Обработка его как фатального загрязняет журнал и (в некоторых потоках) вызывает штормы повторных попыток.


Типы MIME и метаданные DLNA

F20 - MP3 mime → audio/mpeg

Что: стандартный тип MIME для MP3 - audio/mpeg, а не audio/mp3. Последний является распространенным ошибочным названием, которое большинство устройств терпят, но более строгие рендереры его отклоняют.

Почему: молча исправляет воспроизведение на более строгих устройствах, которые следуют стандарту. Кодовая база yaiol уже имела это правильно; никаких изменений не требовалось.


F21 - Порядок типов MIME: сначала не-x- вариант

Что: когда устройство рекламирует как audio/flac, так и audio/x-flac, плагин сначала возвращает не-x- вариант. То же самое для любого кодека со стандартными и экспериментальными типами MIME.

Почему: префикс x- обозначает экспериментальные/неофициальные типы MIME. Некоторые рендереры лучше работают со стандартной формой. Небольшое изменение порядка, реальное влияние.


F22 - Поддержка типа MIME Opus

Что: распознает Opus как потоковый аудиокодек; отправляет тип MIME audio/opus при обслуживании треков Opus.

Почему: Opus теперь распространен (современный компромиссный кодек для речи/музыки). Без F22 плагин отказался бы передавать файлы Opus даже на устройства, которые их обрабатывают.


F23 - Поддержка исходных файлов Monkey Audio (APE)

Что: распознает файлы .ape как допустимый исходный кодек для потоковой передачи/транскодирования.

Почему: APE - это формат без потерь с нишевой, но лояльной базой пользователей. Добавление его стоит недорого и открывает библиотеку для этих пользователей.


F24 - Откат MIME-типа AAC / ALAC

Что: если устройство поддерживает AAC или ALAC, но явно не рекламирует их в своем описании службы UPnP, плагин все равно предлагает их в качестве запасного варианта.

Почему: несколько устройств, которые хорошо обрабатывают AAC, забыли указать его в своем XML-файле возможностей. Без F24 плагин даже не попытается, принудительно транскодируя. С F24 плагин пытается и позволяет устройству обрабатывать его нативно, если оно может.


F25 - Флаг типа DLNA для нативных и закодированных потоков WAV

Что: флаг типа DLNA (идентификатор профиля, такой как LPCM, WAVE, MP3) должен соответствовать тому, что получает устройство. F25 гарантирует, что нативные потоки и закодированные потоки WAV правильно помечены.

Почему: несоответствующий тип DLNA приводит к тому, что некоторые устройства полностью отказываются от воспроизведения или применяют неправильный декодер.


F26 - Заголовок DLNA для файлов FLAC

Что: потоки FLAC получают правильный идентификатор профиля DLNA в своих заголовках.

Почему: без него некоторые устройства, поддерживающие FLAC, не распознают поток как таковой.


F27 - Исправление расчета битрейта в метаданных

Что: битрейт res@bitrate непрерывного потока вычислялся как (sampleRate * channels * bitsPerSample) / 1000 - кбит/с, что отличалось примерно в 125 раз от спецификации UPnP DIDL, которая определяет атрибут как байты в секунду. Теперь делится на 8 вместо 1000.

Почему: неправильное отображение битрейта на устройстве - косметическое на большинстве рендереров, но некоторые выделяют буферы потока из этого значения и заикаются на потоках, которые выглядят примерно в 125 раз меньше, чем они есть. Путь исходного файла, не являющегося непрерывным, уже был правильным ((bitrate_kbps * 1000) \ 8 = байты/сек); неправильным был только путь непрерывного потока.


F28 - Исправление формата времени метаданных (Marantz)

Что: res@duration в DIDL форматировался как H:MM:SS (например, 0:03:42). Спецификация UPnP DIDL определяет формат как H+:MM:SS[.F+] - строго с необязательными, но рекомендуемыми долями секунды; некоторые устройства Marantz считают голую форму недействительной и оставляют свой дисплей длительности пустым. Теперь форматируется как H:MM:SS.fff (например, 0:03:42.000).

Почему: проблема отображения, специфичная для бренда; соответствующий ISO-стиль формата с долями секунды исправляет ее, не затрагивая другие устройства. Применено на обоих сайтах выдачи DIDL (путь исходного файла + путь закодированного потока в WriteAudioFileDIDL).

Дополнительное исправление в том же проходе: pv:addedTime и pv:lastPlayedTime использовали hh (12-часовой формат) в своих строках формата DateTime вместо HH (24-часовой). Любой трек, добавленный или воспроизведенный между 13:00 и 23:59, отображался бы с неправильным часом (например, 17:42 → "05:42") на устройствах, которые отображают это поле. Теперь используется HH.


F29 - Поддержка перемотки закодированного MP3 (CBR)

Что: транскодированные потоки MP3 теперь рекламируют DLNA.ORG_OP=11 (поиск по байтам и по времени) вместо DLNA.ORG_OP=10 (только по байтам). Устройства, которые ранее отказывались от поиска по времени в транскодированном MP3, теперь могут нормально управлять своей полосой прокрутки/интерфейсом поиска.

Почему: транскодер MusicBee производит MP3 с постоянным битрейтом при предустановке HighQuality, поэтому сопоставление байт ↔ время является линейным - устройство может самостоятельно преобразовать запрос поиска по времени в HTTP Range поиск по байтам без какой-либо поддержки со стороны кодировщика. Реклама OP=11 разблокирует этот пользовательский интерфейс на устройстве. Без F29 пользователи, ищущие внутри транскодированного MP3, либо игнорировали поиск молча, либо возвращались к началу трека.

Реализация: перестроена GetEncodeFeature в ItemManager.vb, чтобы разбить встроенный If на читаемую цепочку If/ElseIf/Else. MP3 явно получает OP=11; другие не-PCM кодеки сохраняют OP=10. Без изменений для AAC/FLAC/и т.д. - для них потребуется проверка CBR-ности, специфичная для кодека, которую MusicBee не гарантирует.


F30 - Обработка расширения файла .mpeg

Что: файлы с расширением .mpeg (и еще более редким .mpe) теперь распознаются как FileCodec.Mp3 в GetCodec. До F30 они возвращали FileCodec.Unknown и молча отклонялись из библиотеки / не могли быть источниками транскодирования.

Почему: старые архивы MPEG-1 Layer 3 иногда использовали .mpeg вместо .mp3 (спецификация допускает оба). Несколько файлов в библиотеке из 300 тысяч достаточно, чтобы почувствовать "MusicBee их показывает, а плагин нет" - что сбивает с толку пользователя.


Поведение воспроизведения

F31 - Радиопотоки автоматически используют непрерывный режим

Что: WriteAudioFileDIDL теперь исследует свойство Kind исходного URL-адреса через Library_GetFileProperty и обрабатывает любой файл, чей Kind заканчивается на "Stream" (MusicBee сообщает "MP3 Stream", "Internet Stream" и т. д. для радио), как непрерывный, независимо от глобального переключателя Settings.ContinuousOutput. Используется ветвь DIDL непрерывного потока (Заголовок: "Непрерывный поток", id="continuousstream", фиксированный вывод PCM/Wave); устройство видит один поток бесконечного типа.

Почему: радиопотоки не имеют границ треков, фиксированной длины, поиска. Обработка их как дискретных файлов в DIDL приводила к тому, что плагин рекламировал диапазоны байтов и длительности, которых не существует. Автоматическое переключение, когда MusicBee уже сообщил нам "это поток", устраняет проблему, о которой пользователь не должен думать.

Область применения: применяется только тогда, когда MusicBee управляет воспроизведением (musicBeePlayToMode). Путь получения библиотеки (просмотр клиента UPnP) остается неизменным - URL-адреса радио там редки, и поведение, видимое пользователю, не должно меняться без явного тестирования.


F32 - Откат рекламы кодеков

Что: если устройство не рекламирует определенные кодеки (или плагин не может разобрать XML-файл возможностей устройства), плагин не сразу отклоняет поток. Вместо этого он пытается его передать и позволяет устройству решить.

Почему: многие устройства имеют неполный или нечитаемый XML-файл возможностей, но на самом деле хорошо обрабатывают кодек. F32 обменивает небольшую "лучшую попытку" на полный отказ.


F33 - Улучшение синхронизации полосы прокрутки

Что: позиция между опросами уже экстраполируется по времени от одной привязки (currentPlayStartTicks), поэтому полоса прокрутки обновляется плавно с субсекундной частотой. Оставшимся источником дрожания была начальная привязка для только что запущенного трека: предыдущий код предполагал position=0 в момент, когда таймер состояния впервые заметил, что состояние перешло в Playing, но к тому времени устройство могло воспроизводить 100-500 мс (один интервал опроса). Полоса прокрутки MusicBee начиналась бы с 0, затем прыгала вперед, когда реальность догоняла.

Исправление F33: при первом переходе в Playing на новом треке (currentPlayStartTimeEstimated=True) вызовите GetPlayPositionInformation(), чтобы получить фактическую текущую позицию устройства, затем привяжитесь к ней. UPnP сообщает только разрешение в 1 секунду, поэтому привязка все еще квантована, но она намного ближе к истине, чем предположение 0.

Почему: более плавное + более точное отображение прогресса, особенно сразу после смены трека. Нет способа обойти само разрешение отчетов UPnP в 1 секунду - это спецификация.


F34 - Дрожание полосы прокрутки после смены трека

Что: когда PlayToDevice вызывается для нового трека, плагин раньше оставлял currentPlayPositionMs и currentPlayStartTicks на их значениях предыдущего трека в течение ~100 мс между SOAP-Play и первым опросом таймера состояния, обнаруживающим новое состояние Playing. Полоса прокрутки MusicBee кратко показывала конец предыдущего трека, затем возвращалась к 0, затем поднималась. F34 обнуляет оба при входе в PlayToDevice - в момент, когда мы знаем, что происходит смена трека, до начала любой работы SOAP.

Почему: визуальный сбой при быстром пропуске (ручной переход или бесшовный переход). Теперь первый запрос PlayPositionMs MusicBee после Play чисто возвращает 0, затем GetPlayPositionInformation F33 уточняет его до фактической позиции устройства при первом тике изменения состояния.

Реализация: четыре строки в начале PlayToDevice, в паре с точной привязкой F33 по времени перехода.


F35 - Ошибка "Принудительное транскодирование"

Что: принудительное транскодирование все еще могло пропускать транскодирование в определенных комбинациях. После переработки F04 для каждого профиля были закрыты два конкретных пробела:

  1. Приоритет с ForceNativeStream. Когда оба были True (что может произойти при миграции схемы или частичном файле настроек), ForceTranscoding теперь выигрывает полностью (If streamingProfile.ForceTranscoding Then forceEncode = True ElseIf streamingProfile.ForceNativeStream Then forceEncode = False). Взаимоисключение пользовательского интерфейса предотвращает установку обоих флажков пользователем, но защита времени выполнения обрабатывает любое состояние, которое загрузилось непоследовательно с диска.
  2. Логика bypassTranscodeDecision. Ранее: streamingProfile.ForceNativeStream AndAlso Not Settings.ForceTranscoding. Теперь: streamingProfile.ForceNativeStream AndAlso Not streamingProfile.ForceTranscoding - то же правило приоритета, но в той же области действия для каждого профиля.

Почему: "принудительно" должно означать принудительно. Если пользователь явно включил ForceTranscoding для устройства, плагин никогда не должен молча переходить к нативной потоковой передаче, независимо от того, как сочетаются другие флаги.


F36 - Исключение "Рендерер закрыт"

Что: обернул Plugin.ReceiveNotification в Try/Catch верхнего уровня, который регистрирует любое необработанное исключение вместо того, чтобы позволять ему распространяться обратно в механизм уведомлений MusicBee.

Почему: уведомления от MusicBee (PlayStateChanged, VolumeMuteChanged и т. д.) отправляются в ControlPointManager, который общается с рендерером по SOAP. Отдельные места вызова уже имели Try/Catch вокруг своих вызовов SOAP, но достаточно странный случай синхронизации (например, рендерер умирает между двумя вызовами SOAP в одном и том же обработчике уведомлений) все еще мог ускользнуть. Обертка верхнего уровня является последней защитной сеткой, чтобы пользователь никогда не видел общее всплывающее окно "TargetInvocationException" от MusicBee.

Реализация: переименовал существующее тело в ReceiveNotificationInternal и добавил тонкую обертку ReceiveNotification, которая выполняет Try { ReceiveNotificationInternal(...) } Catch { LogError(...) }. Существующая инфраструктура Try/Catch для каждого метода внутри ControlPointManager (вокруг каждого вызова PostSoapRequest) остается - F36 - это ремень + подтяжки.


F37 - Поиск по длинному треку вызывает ложный переход

Что: поиск внутри длинного трека может вызвать кратковременный цикл Stopped→Playing на некоторых рендерерах. Без дискриминации ProcessNewPlayState.Stopped рассматривает это как естественное окончание трека и вызывает Player_PlayNextTrack, продвигая MusicBee, когда пользователь просто хотел перемотать. F37 устанавливает lastUserInitiatedSeek в Seek() и добавляет 5-секундную защиту в обработчике Stopped (отражая существующее окно lastUserInitiatedStop).

Почему: бесшумный пропуск к следующему треку во время поиска - одна из тех ошибок, причину которых никто не может угадать - пользователь думает "странно, я пытался перемотать вперед, а теперь играет следующая песня". Исправление механическое: тот же шаблон, что и уже существующая дискриминация пользовательской остановки.


F38 - Улучшенная обработка поиска для кодеков, склонных к сбоям

Что: сбой BubbleUPnP при поиске MP3 был каноническим симптомом. После аудита текущий код поиска yaiol уже делает правильные вещи - нативный путь правильно обрабатывает HTTP Range (206, Content-Range, AcceptRanges), закодированный путь рекламирует X-AvailableSeekRange и анализирует входящие заголовки timeSeekRange.dlna.org / npt, флаги DLNA.ORG_OP отражают фактические возможности потока (с опцией DisablePcmTimeSeek для проблемных устройств Platinum). Протестировано пользователями на текущем BubbleUPnP 4.6.4: сбоев не наблюдалось.

Почему: сбой BubbleUPnP при поиске MP3 был зарегистрирован примерно в 2024 году, и с тех пор приложение получило около 16 месяцев исправлений. F29 (закодированный MP3 OP=11) был новой переменной, которая могла бы снова его выявить; не выявляет на протестированных версиях.

Если сбой вернется: форма исправления будет переключателем "ограниченный поиск" для каждого профиля, который принудительно устанавливает DLNA.ORG_OP=10 (только по байтам) для помеченных кодеков - отражая то, как DisablePcmTimeSeek уже работает для PCM. Добавить тогда, а не заранее.


Пользовательский интерфейс и логирование

F39 - Кнопка "Добавить" выбирает новый профиль

Что: нажатие "Добавить" в списке профилей устройств создает новый профиль И автоматически выбирает его, чтобы пользователь мог немедленно редактировать поля. Наша реорганизация разделенного диалогового окна уже делает это - как прямой путь "Добавить", так и путь "из шаблона" заканчиваются Me.activeStreamingProfiles.SelectedIndex = Me.activeStreamingProfiles.Items.Count - 1. Проверка подтвердила, что наш форк уже обрабатывает это - ничего менять не нужно.

Почему: небольшая проблема UX, которая оказалась уже не проблемой здесь.


F40 - Больше максимальных подключений + предупреждение в журнале

Что: ограничение одновременных потоков плагина (SemaphoreSlim вокруг Sockets_Stream_File / Sockets_Encoder_Start) было жестко задано как 4. F40 делает его настраиваемым пользователем на странице общих настроек (по умолчанию 16, диапазон 1-256), добавляет строку журнала MaxConnections, когда запрос должен ждать слота, И отображает красный значок ⚠ Max Conn в левом нижнем углу диалогового окна настроек, если ограничение было достигнуто хотя бы один раз с момента запуска MusicBee.

Почему: когда устройство отправляет параллельные запросы (некоторые Marantz/Linn во время сканирования обложек, зонды метаданных BubbleUPnP наряду с активным воспроизведением), дополнительные запросы блокировались молча за семафором - пользователь видел "устройство медленное" без видимой причины. Строка журнала хороша для технической отладки, но нетехнические пользователи никогда не читают журналы. Видимый значок в диалоговом окне настроек делает условие достижения ограничения обнаруживаемым для любого, кто открывает настройки плагина.

Реализация:

  • Централизовано ожидание в WaitOnSendBarrier(logTag) в MusicBeeUpnp.vb; оба места вызова (MediaServerDevice.GetFile, Encoder.StartEncode) используют его.
  • Settings.MaxConnections сохраняется в v8 схемы настроек.
  • Plugin.MaxConnectionsHit - это постоянный флаг сессии, устанавливаемый внутри WaitOnSendBarrier; сбрасывается только при перезапуске MusicBee.
  • SettingsDialog.maxConnectionsBadge - это красная жирная метка в (16, 410), которая отображается только тогда, когда Plugin.MaxConnectionsHit истинно. Имеет всплывающую подсказку, объясняющую причину и способ устранения.
  • Семафор инициализируется один раз при загрузке типа, поэтому изменение настройки требует перезапуска MusicBee (отмечено в метке поля).

F41 - Запись в журнал "кодирование из-за ReplayGain/DSP"

Что: вместо отдельных строк журнала "кодирование для RG" / "кодирование для DSP", одна строка StreamDecision из F42 включает MB-DSP/EQ, MB-ReplayGain, Profile-DSP/EQ, Profile-ReplayGain как накопленные причины. Та же диагностическая ценность, меньше шума.

Почему: пользователи видят все причины транскодирования для данного трека в одной строке журнала, а не разбросанные. См. F42 для полной информации.


F42 - Запись в журнал "рендерер не поддерживает исходный кодек"

Что: добавлена строка журнала StreamDecision для каждого трека, воспроизводимого на устройстве, которая сообщает либо "нативный CODEC", либо "транскодировать CODEC→CODEC причина=…". Поле причины накапливает каждое условие, которое вызвало транскодирование: MB-DSP/EQ, MB-ReplayGain, Profile-DSP/EQ, Profile-ReplayGain, WebFile, VirtualFile, ForceTranscoding(global), SampleRate<min/SampleRate>max, DownmixToStereo, DeviceLacksCodec(X), BandwidthConstrained.

Почему: пользователи были сбиты с толку неожиданными скачками загрузки ЦП на файлах, которые они ожидали передавать нативно. Одна строка журнала для каждого трека точно сообщает им, какое условие вызвало транскодирование - и если поле показывает DeviceLacksCodec(Flac), они сразу понимают, что информация о протоколе устройства была неполной, и могут захотеть, чтобы сработал откат F32.

Реализация: одна строка-аккумулятор, постепенно создаваемая по цепочке принятия решений; регистрируется один раз в конце. Ограничено Settings.LogDebugInfo, чтобы избежать шума в журнале в производственной среде.


F43 - Журнал SetNextAVTransport показывает URL источника

Что: записи журнала QueueNext теперь включают source=<путь к библиотеке MusicBee> наряду с stream=<URL потоковой передачи HTTP>. То же изменение применено к пути успеха и пути сбоя (QueueNext:Failed).

Почему: при отладке проблемы с треком в очереди URL потоковой передачи (/encode/aabbccdd0.flac) сам по себе непрозрачен - одинаков для каждого трека. URL источника - это удобочитаемый путь к библиотеке, который точно сообщает, какой файл MusicBee пытался поставить в очередь.


F44 - Улучшенное логирование ошибок MIME-типов

Что: две новые записи в журнале во время Activate:

  • Activate:MimeUnverified - срабатывает для каждой некорректной записи в ответе GetProtocolInfo устройства, указывая, какая запись не могла быть разобрана (чтобы пользователь мог видеть, например, "Marantz вернул http-get:*::* для какого-то кодека - возможность не проверена, откат F32 будет угадывать").
  • Activate:NoSinkInfo - срабатывает один раз, если устройство вообще не вернуло элемент <Sink>. Это означает, что SupportedMimeTypes остается Nothing, а IsCodecSupported деградирует до "предполагать, что все работает" - полезный контекст, когда позже появляются ошибки "устройство отказало в потоке".

Почему: до F44 эти молчаливые откаты возможностей оставляли пользователей гадать, почему их треки либо транскодировались вопреки ожиданиям, либо отклонялись устройством. Теперь один поиск по Activate: показывает, была ли информация о возможностях устройства пригодна для использования.


F45 - Улучшенное логирование ошибок метаданных

Что: журнал исключений Browse в ContentDirectoryService.vb уже был обогащен в более ранней работе yaiol (сессия исправления ошибок Alia Vox) с ObjectID и трассировкой стека. F45 расширяет его далее с BrowseFlag (метаданные против дочерних элементов), Filter (какие атрибуты запросил клиент), sortCriteria и partialResultLength (сколько байтов DIDL было произведено до сбоя - указывает, насколько далеко в пакете находится плохой трек).

Почему: когда что-то идет не так в середине DIDL, значение partial-length сообщает вам, произошел ли сбой на первом треке пакета (partial=0) или в середине (partial=N) - в сочетании с startingIndex пакета вы можете определить индекс offending track. Filter и BrowseFlag объясняют, какой вид просмотра хотел клиент; иногда просмотр только метаданных завершается сбоем, тогда как просмотр дочерних элементов для того же ID успешен.


Сеть

F46 - Автоматический режим объявляет только на адаптерах реальной сети

Что: в режиме интерфейса Автоматический плагин раньше объявлял себя (SSDP) на каждом работающем адаптере IPv4. На машине, где также работает VPN-туннель (NordLynx) или виртуальный коммутатор (Hyper-V / WSL / Docker), та же библиотека объявлялась и на каждом из этих адаптеров, поэтому точка управления, с которой вы транслируете, обнаруживала server два или три раза и показывала библиотеку как дублирующиеся копии. Автоматический режим теперь оставляет только адаптеры с реальным шлюзом IPv4 по умолчанию (HasIPv4Gateway) — которого у адаптеров туннелей и виртуальных коммутаторов нет — поэтому они исключаются из списка объявлений. Закрепленный пользователем адрес по-прежнему имеет безусловный приоритет (объявление только на этом интерфейсе), а если ни один адаптер не сообщает о шлюзе, выбор возвращается ко всем адаптерам, поэтому список объявляемых адресов никогда не пуст и плагин не может стать невидимым.

Почему: дубликат возникает не из-за того, что «вы в VPN» — он возникает из-за одновременного объявления на адаптере LAN и на туннельном/виртуальном адаптере, поэтому одна точка управления видит один и тот же server по двум адресам. Потребительский VPN (NordVPN/NordLynx) туннелирует только трафик, идущий в интернет; рендерер DLNA живет в LAN, а трафик локальной подсети обходит туннель, поэтому туннельный адаптер всё равно никогда не достигает рендерера — его исключение убирает фантомную копию, но никогда — рабочий путь. Проверка шлюза — это дешевый, надежный сигнал, отличающий реальный адаптер LAN/Wi-Fi от туннеля или виртуального коммутатора. Дополняет N05 (который исправил, как объявления отправляются по таким каналам — multicast вместо broadcast); F46 определяет, на каких адаптерах вообще объявлять.

Известное ограничение: mesh-VPN / VPN удаленного доступа (Tailscale, ZeroTier, WireGuard до дома), рендереры которого действительно живут по ту сторону туннеля, обычно представляет адаптер без шлюза по умолчанию, поэтому автоматический режим отбрасывает и его. Такие пользователи вместо этого закрепляют адрес VPN, который имеет приоритет над фильтром шлюза.