新功能
2.0.2 - 2026-07-20
- 当任何节点跟随某个模板时,该模板将无法再被删除——删除按钮将保持禁用状态,因此节点永远不会成为孤立节点。每个模板行现在都会显示其跟随者的实时 (n) 计数,这可以一目了然地解释删除按钮为何被禁用;删除未使用的模板现在也会 生效,而不是在下次加载时默认设置悄然重新出现。
- 视图树旁边新增了一个 漏斗 按钮,可以准确显示哪些节点跟随某个模板:它会过滤树,只显示所选模板的跟随者,在您选择其他模板时重新过滤,并在关闭时恢复完整的树。
- 广播 和 播客 节点现在永久与其类别的模板配对——在“路径”选项卡上重塑模板,节点将自行跟随;无需应用任何更改,因此“应用”按钮对它们禁用。模板列表通过两个区域反映了这一点:标准(所有新模板都在此处创建)和 保留(广播 + 播客)。
2.0.1 - 2026-07-20
- 路径模板现在已与使用它们的节点实时链接。应用模板会使节点遵循它:稍后编辑模板,每个遵循它的节点都会立即重塑——无需逐个查找并重新应用。视图树会在每个节点名称后显示它遵循的模板,重命名会立即显示在那里,删除模板时会先告诉您有多少节点遵循它(它们会保留当前布局并停止遵循任何内容)。
- 将模板应用于隐藏节点也会使其再次可见——应用是“向我显示这个,形状像那个”的手势,而隐藏则保留在“可见”复选框中。它所取代的保留“隐藏”模板已消失。
- 视图树不再丢失您的位置:勾选、展开的文件夹和滚动位置都可以在应用模板和其他刷新后保留。
2.0.0 - 2026-06-16
这是 MusicBee UPnP 插件的开源 yaiol 分支的首次公开发布。它分为两部分:此分支新增的所有内容,以及对原始插件进行的修复和改进。每个条目都保留了项目内部功能目录的 What / Why 形式,因此每个更改背后的原因都显示在页面上,而不仅仅是更改本身。
此分支中的新增功能
MediaRenderer - 播放到 MusicBee
N01 - MusicBee 作为播放目标渲染器
内容: 通常,此插件以一种方式工作:手机或其他设备浏览 MusicBee 的库并在其自身上播放音乐。此功能添加了相反的方向 - 它让 MusicBee 成为 播放器。通过手机上的控制器应用程序(例如 BubbleUPnP),您可以选择桌面 MusicBee 作为播放设备,然后用手控制它:播放、暂停、停止、快进或快退、跳到曲目中的某个点,以及更改音量或静音。
原因: 它将您的手机变成您 PC 上已有音乐的遥控器。坐在沙发上,在手机上浏览您的音乐库,点击一首曲目,它就会从连接到您桌面的扬声器中播放出来 - 您可以从您所在的位置进行完全控制。原始插件从未将此功能作为可用的功能发布。
开启方式: 默认情况下它是关闭的,因为开启它会允许您家庭网络上的任何设备在您的 PC 上开始播放。您可以通过在设置对话框的“常规”选项卡上勾选一个复选框来启用它。插件的三个角色在那里都有自己的复选框 - 共享我的库(服务器)、允许他人播放到我(渲染器)和播放到其他设备(控制点) - 并且对话框只显示您开启的角色实际需要的设置选项卡,因此您永远不会遇到不适用于您的选项。
区分您的机器: 您可以给渲染器起任何您喜欢的名称(它最初是“MusicBee (yaiol)”)。该名称会出现在您手机的播放目标列表中,因此当多台 PC 运行 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 FLAC 时使 5.1 功能的渲染器失效。现在第二个子句排除了 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 端口之前。在一个真实的库(5 万多首曲目,5400 集播客,数百个电台)上,这需要几分钟的冷启动,并且树会永远留在 RAM 中,包括客户端从未打开过的分支。此分支不预先构建任何内容:根目录为每个端点公开一个 L: 前缀的占位符(L:music、L:podcast、L:filter:…);每个级别仅在客户端浏览到它时才计算(LazyBrowse → EnsureLazyEndpointInMemory → 每级缓存),并且库更改通知会清除缓存(SetLibraryDirty)。
原因: 冷启动基本上是即时的 - MusicBee 完成插件初始化时 HTTP 端口已打开 - 并且内存与已浏览的内容成比例,而不是与库大小成比例。权衡:首次浏览到端点会支付其加载成本;重新进入会缓存直到下一次库更改。这是其他一切所依赖的基础。完整说明:FIXES.md。
网络和健壮性
强化 HTTP 服务器的绑定路径。原始插件在其端口不可用时会静默死亡。
N04 - 自愈 HTTP 端口绑定
内容: 插件的 HTTP 服务器在其配置的端口不可用时不再崩溃。三个相关更改:
- 绑定失败时自动回退。
HttpServer.Start尝试配置的端口,并在SocketException时向上扫描最多 20 个端口以查找第一个空闲端口。实际绑定的端口记录在一个新的Plugin.boundServerPort中,并且所有宣传服务器的内容 - SSDPLOCATIONURL(NOTIFY + M-SEARCH 响应)、设备 URL(PrimaryHostUrl)、路由器端口转发以及 SSDP/控制点自过滤器 - 现在都读取boundServerPort而不是Settings.ServerPort。UPnP 客户端通过 SSDP 发现真实端口,因此移动的端口对渲染器是透明的。 - 用户通知。 当发生回退时(保存的端口不是正在使用的端口),本地化的
MessageBox(WarnPortInUse)会告诉用户哪个端口正在实际提供服务,并且设备仍然会找到它 - 因为插件是无头运行的,对话框中的消息只有已经怀疑有问题的人才能看到。 - 重启恢复。
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 之后,端口冲突会自愈 - 服务器继续在下一个可用端口上运行,用户会收到通知,并且客户端会重新发现它 - 而不是使整个插件崩溃。
实现:
- 默认端口从
49382移至9779(低于动态范围,因此 Windows 永远不会自动保留它;不是已知的媒体服务器默认值),在所有三个ServerPort声明 + 设置解析失败回退中。 Plugin.boundServerPort(新的共享字段)保存实时监听端口;activeServerPort保持配置的 快照,因此“需要重启”徽章逻辑不会在回退时误触发。HttpServer.PortScanRange = 20;扫描在第一次成功的TcpListener.Start()时停止,并且只有在所有尝试都失败时才抛出最后一个异常。- 新的 EN 资源键
WarnPortInUse(翻译遵循发布时区域设置传递)。
N05 - 通过多播组(VPN / 点对点)进行 SSDP 公告
内容: SSDP 公告发送到 UPnP 多播组(239.255.255.250),而不是 IP 广播地址。当 SSDP 搜索响应与服务器重启竞争时记录的无害“无法访问已处置对象”错误也被抑制。
原因: 在点对点 / VPN 网络适配器上,IP 广播不适用 - 旧的广播发送失败并出现“无效参数”,并且公告被遗漏,因此插件对这些链接上的客户端不可见。向正确的多播组公告可以修复这些适配器上的发现问题。
库导航
这些在此分支中发布,原始插件中没有。它们来自实际使用真实 UPnP 客户端浏览插件自己的输出。
N06 - 基于过滤器的库公开
内容: MusicBee 的过滤器选项卡(用户 MusicBee 文件夹中的 .xautopf 文件)成为插件库中的 UPnP 根容器。然后,每个过滤器的曲目都可以在 AlbumArtistSort → Album → Tracks 的层次结构中浏览。
原因: 拥有精心策划的 MusicBee 过滤器(例如“5 星曲目”、“最近添加”、“古典 → 巴洛克”)的用户希望在使用 UPnP 客户端浏览插件时找到它们。原始插件只公开了原始库树。
N07 - SortAlbumArtist 字段连接
内容: 插件现在读取 MusicBee 的 MetaDataType 165(排序专辑艺术家)并将其用于浏览视图中的艺术家分组/排序。
原因: 高保真浏览器和发烧友使用排序艺术家名称(“贝多芬,路德维希·范”而不是“路德维希·范·贝多芬”)来组织库。这是严肃听众的标准期望。两个上游都缺少。
N08 - 多值 AlbumArtist 处理
内容: 当专辑的 AlbumArtist 字段包含多个艺术家,并用 "; " 分隔时(例如 "yaiol; Ars Ricercata"),曲目现在会出现在浏览视图中每个 艺术家下,而不是出现在一个结合了所有名称的“弗兰肯斯坦”艺术家下。
原因: 协作专辑和合辑需要在每个协作者下显示。没有这个,查找专辑的一半搜索路径都会中断。
N09 - 专辑容器封面 (upnp:albumArtURI)
内容: DIDL 浏览响应中的专辑容器节点现在包含一个指向专辑封面的 upnp:albumArtURI 元素。
原因: 没有这个,UPnP 客户端浏览视图中的每个专辑都会显示一个通用图标而不是专辑封面。这是导航的视觉提示;每个现代高保真浏览器都期望如此。
N10 - 过滤器专辑内的曲目排序
内容: 过滤器公开的专辑内的曲目现在按碟片编号,然后按曲目编号排序。
原因: 标准专辑顺序。如果没有明确排序,曲目会以过滤器碰巧返回的任何顺序返回 - 通常看起来是随机的。
N11 - 播放列表文件夹树修复
内容: LoadLibraryPlaylists 函数(最初由 Steven Mayall 编写,约 2014 年)未能深入到新创建的播放列表文件夹中。每个文件夹中的第一个播放列表以及任何子文件夹最终都会孤立在根级别。
原因: 原始插件中存在了十一年。在打开 BubbleUPnP 并点击播放列表后 30 秒内可见。通过在树构建期间正确递归到新创建的文件夹中,在 yaiol 中修复。
N12 - XML 非法控制字符净化
内容: 任何包含 C0 控制字符(例如,来自错误编码传递的 0x19 - UTF-8 → Latin-1 → 截断 0x99 为 0x19)的标签的曲目都会导致整个浏览响应失败并显示 Action Failed,一旦该错误曲目进入分页批次。
原因: XML 1.0 禁止大多数 C0 控制字符,并且 XmlWriter 在被要求写入任何此类字符时会抛出异常。原始插件中存在。通过 XmlConvert.IsXmlChar 在每个 Library_GetFileTags 退出点剥离无效字符来修复。
N13 - 分页浏览中电台列表的确定性
内容: 浏览电台容器时,它会进入通用文件列表分支,该分支在每次调用时都会调用 files.Sort(AlbumFileComparer)。电台条目具有空的专辑/碟片/曲目标签,因此每次比较都返回 0 - List(Of T).Sort 是不稳定的,每次调用都会产生不同的顺序。UPnP 控制点会分页(BubbleUPnP 获取 0..15,然后是 16..end);在两次调用之间,列表会重新洗牌,因此一些电台会出现在两个页面中(重复),一些则都没有(缺失) - 每次刷新都看起来是随机的。
原因: 原始插件中存在(其作者从不通过 UPnP 浏览电台)。此处通过 Browse 中的专用 ContainerCategory.Radio 分支修复,没有每次调用排序;radioFiles 在加载时按标题排序一次(稳定)。分页浏览现在看到确定性顺序;页面 1 和页面 2 是不相交的。
N14 - UPnP 搜索专辑类返回专辑容器
内容: UPnP 搜索专辑类查询(upnp:class = "object.container.album.musicAlbum",例如 BubbleUPnP 的“随机专辑”)返回完整的曲目列表而不是专辑容器,因此客户端显示零专辑。原始处理程序只解析带括号的条件,然后无论请求的类如何都转储所有曲目。
原因: 在此处修复 - 专辑类查询现在枚举不同的专辑(按专辑艺术家+专辑分组),并将每个专辑作为适当的 musicAlbum 容器发出,带有封面艺术,可通过 Salb<idx> 虚拟 ID 空间寻址,以便客户端可以深入到结果并播放它。
N15 - 工作、范围感知的 UPnP 搜索,带点击穿透
内容: 原始版本没有宣传搜索功能(GetSearchCapabilities 返回空),因此客户端甚至拒绝发送搜索;旧的后端从 musicFiles 读取,在惰性树时代永久为空。此分支宣传真实可搜索的属性,针对惰性库实现按标题搜索曲目和按标题搜索专辑(HandleLazySearch),当发送真实容器 ID 时将查询范围限定到客户端当前分支(否则替换 L:music,以便顶部栏搜索不会拖入播客/电台/有声读物噪音),并通过合成的 Ssrch_alb_* ID 使专辑结果可点击,Browse 的早期分支将这些 ID 映射回专辑的曲目。(专辑类结果作为容器的部分是 N14。)
原因: BubbleUPnP 中的搜索从“库不支持搜索”变为返回有用、范围限定、可播放的结果。完整设计 + 拒绝的方法:SEARCH.md。
N16 - UPnP 缓存失效 (SystemUpdateID)
内容: 原始版本返回常量 SystemUpdateID=0 - UPnP ContentDirectory 缓存失效约定 - 因此符合规范的客户端(BubbleUPnP)将库视为永不更改:过时的浏览结果、URL 方案更改后的 404 缩略图,以及“重启 MusicBee 两次才能看到更改”的舞蹈。此分支在加载时从纪元秒数中获取 SystemUpdateID(因此每次重启都严格领先于上次),并在每次库变异和设置更改时递增它(SetLibraryDirty / ResetCache → BumpSystemUpdateId)。
原因: 客户端在下次浏览时可靠地获取编辑、新文件和设置更改。已知限制:订阅客户端不会通过 GENA 主动重新推送新值(作为未来工作搁置);它们仍然在下次浏览时看到它。
N17 - 播客订阅封面
内容: 播客磁贴不显示图像 - 每个 /PodcastThumbnail/ 请求都返回 404。两个堆叠的错误:解析链从未检查 MusicBee 的实际封面缓存(%LocalAppData%\MusicBee\InternalCache\Subscriptions\<name>.jpg,桌面 UI 从中加载),并且 HTTP 层的 unescape+lowercase 损坏了 feed-URL 路由键,直到其最后一个路径段。此分支从 MB 的 InternalCache 解析封面,并通过一个 URL 安全的 slug 路由查找,该 slug 在 HTTP 层保持完整(PodcastSlug / podcastSubIdBySlug)。
原因: 订阅封面现在在浏览视图中呈现(所有 22 个以前返回 404 的请求都已解决)。
N18 - 分层(带分隔符)标签浏览
内容: 任何字段都可以在“库选项”选项卡上标记为分层,并给定一个单字符分隔符(一个字段选择器 + 带添加/删除的分隔符框,保存在插件设置中)。将 Grouping 设置为 /,并将值设置为 Jazz/Cool Jazz,然后浏览时显示为 Jazz › Cool Jazz,而不是一个平面条目。精确标记在分支上的曲目(仅 Jazz)会获得自己的 [Jazz] 节点,因此没有任何内容被隐藏,只有一个子项的分支会自动折叠,并且 ; 被拒绝作为分隔符,因为它本身是 MusicBee 的多值分隔符。
原因: 用户已在单个字段中编码的深层标签分类(流派树、心情层次结构、“古典/巴洛克/协奏曲”)最终可以作为标签描述的树进行浏览,而不是用户必须从头到尾阅读的平面斜杠分隔字符串墙。
N19 - 按分组字段标记的单个根路径
内容: 根目录下的单个浏览路径按其分组字段(例如“流派”)标记,而不是其完整的短路径,与合并的第一个字段组的命名方式匹配。
原因: 浏览树读取一致 - 无论是单独的根条目还是与同级合并的根条目(N20),都采用相同的命名规则 - 而不是单独的根条目显示冗长的内部路径,而其合并的邻居显示干净的字段名称。
N20 - 合并共享第一个字段的浏览路径
内容: 共享相同第一个字段的两个浏览路径 - “流派 / 排序专辑艺术家”和“流派 / 播客人物” - 合并为一个流派根文件夹,该文件夹首先列出流派值,然后分成两个视图,而不是在根目录并排显示两个几乎重复的“流派 / …”条目。
原因: 拥有几个嵌套在公共字段下的相关视图的用户会看到根目录被几乎相同的顶级条目弄乱。合并它们可以保持根目录的可读性,并将相关视图分组到它们所属的位置 - 在它们的共享字段下。
N21 - 分类类型浏览路径(标准 / 电台 / 播客)
内容: 每个浏览路径都按类别分类 - 标准、电台或播客。模板列表分为这三个部分,每个模板的字段选择器只提供该类别数据实际可以提供的字段,并且模板只能应用于视图树中匹配的节点(不兼容的节点会变灰且无法选中)。保留的电台和播客模板无法删除,因此它们的类别部分永远不会消失。
原因: 如果没有类型化,用户可能会构建一个默默地为空的布局 - 电台没有“专辑”,播客剧集没有“专辑艺术家” - 并且只有通过 UPnP 客户端浏览到死文件夹才能发现它。将字段菜单和应用目标限制为类别的真实数据,使得无法构建空的布局。
N22 - 按发布年份对播客进行分组
内容: 读取每个播客剧集的发布日期,因此带有年份级别的播客浏览路径会按年份对剧集进行分类,而不是将它们折叠到单个“未知”下。
原因: 大型播客订阅可以像库的其余部分一样按年份导航,而不是因为插件从未查看过每集发布日期而导致每集都落在一个未注明日期的堆中。
N23 - 折叠单结果分组级别
内容: 解析为单个值的分组级别 - 例如,一个唱片类型级别只显示“LP”,对于只制作 LP 的艺术家,或者一个字母级别只有一个字母 - 会自动跳过,直接将用户带入其内容。
原因: 浏览一个只包含一个文件夹的文件夹纯粹是摩擦。折叠单选级别可以消除不必要的点击,而不会改变用户可以访问的内容。
N24 - 针对 MusicBee 日期字段的年份分组/搜索
内容: 年份条件不再使用裸四位数查询 MusicBee 的完整日期“年份”字段,并且硬编码的年份字段别名已删除,因此现在每个按组字段都从路径定义中通用解析。
原因: 对于其年份标签包含完整日期的库,按年份分组或搜索以前不会返回任何内容 - 四位数查询永远不会匹配完整日期字段。查询正确的字段可以使年份分组和搜索再次找到曲目。
N25 - 独立的“年份”和“年份 (yyyy)”分组字段
内容: 专辑分组和浏览路径现在公开 MusicBee 自己的两个年份字段 - 年份(完整日期标签)和年份 (yyyy)(仅四位数年份) - 因此用户在定义专辑分组或浏览路径时可以选择其中一个。
原因: 这两个字段在 MusicBee 中含义不同,将它们折叠会失去这种区别。同时显示两者可以让用户根据自己的意图将一年的所有发行版(yyyy)收集在一起,或者保持精确的日期顺序(完整年份标签)。
N32 - 固定的筛选器和播放列表在根目录按类型分组
内容: 固定的筛选器现在直接显示在浏览根目录的筛选器文件夹下方,固定的播放列表直接显示在播放列表文件夹下方,而不是所有固定项都聚集在根目录末尾的一堆中。每个固定的快捷方式都与其同类放在一起。
原因: 随着用户固定的快捷方式越来越多,末尾那一堆混杂的筛选器和播放列表会越来越难以浏览,并且把每个快捷方式与其所属的文件夹分隔开。将固定项归入各自的类别之下,可以保持根目录清晰易读,并让每个快捷方式紧挨着它所属的内容。
设置对话框和打包
N26 - 分区设置对话框
内容: “偏好设置”页面采用了左侧导航布局,包含以下部分:常规 / 播放 / 库 / 设备配置文件 / 诊断。
原因: 原始版本是一个单一的、冗长的平面列表,列出了所有设置 - 对于构建它的开发人员来说没问题,但对于其他人来说却令人困惑。分区将相关选项分组,使对话框感觉更像现代应用程序设置。
程序集 + 插件重命名(无 F-id - 打包说明)
内容: 编译后的 DLL 命名为 mb_UPnP_yaiol.dll,插件报告自身为“MusicBee UPnP (yaiol)”。与原始的 mb_Upnp.dll 不同。
原因: 用户可以安装 yaiol 和原始插件,并并排比较行为。
徽章系统 - 显示运行时状态(F40 背后的机制)
内容: 一种通用的 UI 模式,用于将重要的运行时条件显示为设置对话框中可见的彩色徽章。当前实例:
- ⚠ 最大连接数 (N04) - 当 MusicBee 启动以来至少达到一次最大连接数限制时触发。粘性会话标志
Plugin.MaxConnectionsHit。在没有可用插槽时在WaitOnSendBarrier内部设置。 - ⚠ 需要重启 - 当保存的设置需要 MusicBee 重启才能生效时触发。粘性会话标志
Plugin.RestartRequired。在对话框的保存处理程序中设置,当新的持久化值与运行时快照(Plugin.activeMaxConnections、Plugin.activeServerPort、Plugin.activeIpAddress)不同时。需要重启的设置仅限于那些确实无法热重载的设置 - HTTP 服务器绑定参数和在 Initialise 时构建的 SemaphoreSlim。
原因: 插件的日志文件对于技术用户调试来说很好,但非技术用户盯着“设备声音不对”或“播放缓慢”时永远不会打开 诊断 → 查看日志。徽章捕获了用户需要知道发生了什么的情况,并在他们下次打开插件时显示出来 - 无需阅读任何内容即可发现。
未来可重用:
- 检测到配置文件不匹配(设备用户代理从未匹配任何配置文件,回退到通用)。
- 触发 NextURI 退避(F13 - 对于不稳定的设备,会话期间禁用无缝播放)。
- 库扫描失败/部分。
- 会话中渲染器连接丢失。
- 任何其他“发生过一次,用户应该知道”比“在 1000 行其他日志中静默记录”更好的情况。
实现约定:
- 徽章标签位于对话框级别(不在任何面板内),因此无论用户在哪个部分,它们都可见。
- 放置在底部行,靠近保存/取消(当前:y=410 水平堆叠)。
- 每个徽章在
Plugin中都有一个相应的粘性会话标志,当条件发生时翻转为 True,并且仅在 MusicBee 重启时重置。 - 资源:
<Condition>Badge(标签文本,前缀为 ⚠)+<Condition>BadgeTip(解释原因+补救措施的工具提示)。 - 对于“保存的设置需要重启”徽章,在
Plugin.Initialise()时获取运行时快照,并在对话框保存处理程序中Settings.SaveSettings()后与Settings.*进行比较。
N27 - 取消会丢弃路径/模板编辑
内容: 在设置对话框中对路径和模板进行的编辑现在在用户点击“取消”时会被丢弃,而不是悄悄地保持应用,并且在会话期间删除的任何保留模板都会重新创建。(模板在编辑时会实时保存 - “路径”选项卡上没有单独的“保存”按钮。)
原因: “取消”应该意味着取消。以前,用户尝试更改路径/模板并退出后,发现更改已提交,除了手动重做每个更改之外,没有办法撤消它们。
N28 - 字段选择器菜单中的字面 & 符号
内容: 名称中包含“&”的字段 - 例如“Mood & Context” - 在字段选择器菜单中会字面呈现该 & 符号,而不是将其吞噬为 Alt-助记符前缀。
原因: 带有 & 符号的字段名称显示错误(字符消失,下一个字母成为加速器),导致菜单条目难以识别。
N29 - 稳定、未翻译的设置窗口标题
内容: 设置窗口标题固定为品牌字符串“MusicBee UPnP Plugin”,不再随界面语言而变化;每个区域设置包中都删除了每种语言的 DialogTitle 字符串。
原因: 随语言变化的窗口标题是一个可翻译的表面,但没有任何好处 - 标题是一个品牌标志。固定它可以使其在任何地方都稳定和一致。
本地化
原始插件仅支持英语。此分支完全可本地化 - 每个面向用户的字符串都通过资源包流转,并且插件会自动检测 MusicBee 自己的 UI 语言。
N30 - 多语言 UI(翻译待定)
内容: 本地化机制已完成并发布。Localisation.vb 从 MusicBee3Settings.ini 中读取 MusicBee 选择的语言(<SystemLanguage> 内名)并将匹配的 .NET 文化应用于线程,因此 My.Resources.Resources.* 返回本地化字符串。每个面向用户的标签/按钮/消息都连接到资源键(设计器控件通过 ApplyDesignerExtras + sync-en-locale.js;WarnPortInUse 等运行时字符串手动添加)。尚未完成的是实际翻译:只有英文源包(Resources.resx)存在 - 其他语言的卫星包将在插件功能完整时一次性生成(在字符串仍在变化时零碎翻译会浪费精力)。
目标语言(MusicBee 本身提供的集合,通过 endonymToCulture 1:1 匹配,因此插件会自动遵循 MusicBee 的语言):
阿拉伯语 (ar) |
捷克语 (cs) |
德语 (de) |
希腊语 (el) |
西班牙语 (es) |
法语 (fr) |
匈牙利语 (hu) |
意大利语 (it) |
韩语 (ko) |
荷兰语 (nl) |
挪威语 (nb) |
波兰语 (pl) |
葡萄牙语(巴西) (pt-BR) |
葡萄牙语(葡萄牙) (pt-PT) |
瑞典语 (sv) |
土耳其语 (tr) |
乌克兰语 (uk) |
俄语 (ru) |
日语 (ja) |
简体中文 (zh-CN) |
繁体中文 (zh-TW) |
英语 (en, 源) |
变体策略(根据工作区区域设置规则):PT 和 ZH 分为不同的包,因为词汇/脚本确实存在差异(pt-BR/pt-PT,zh-CN/zh-TW)。EN 是一个单一的包 - MusicBee 的“English(US)”(en-US)通过 .NET 的文化链回退到 en,因此不生成单独的美国包。ES 和 FR 同样是单一区域设置。
原因: UPnP 插件的设置(“不使用原始 PCM”、“强制小端 PCM”、端口回退警告)在母语中已经足够神秘。遵循 MusicBee 自己的 UI 语言 - 而不是强制使用英语 - 是非英语用户能否配置工具的区别。两个上游都没有尝试过这一点。
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 帧)。
原因: 根据论坛证词,这是最大的播放质量提升。高保真用户购买昂贵的渲染器明确需要位完美输出;任何 DSP 触碰都会破坏目的。默认开启,因为大多数现代设备都能处理用户扔给它们的任何编解码器,并且 ReplayGain/EQ 应该是可选的。它是每个配置文件独立的,因此您可以为旧的 Xbox 保留转码,同时将原生流发送到高保真 DAC。
F04 - “强制转码”每配置文件
内容: 每个配置文件覆盖,强制将每个流通过转码器发送到此设备,无论原生编解码器支持如何。与 F03(强制原生流)相反。与 F03 互斥 - 当其中一个开启时,UI 会自动取消勾选另一个。
原因: 单个全局切换将与每个配置文件的强制原生流(F03)相矛盾。实际情况:设备 A 是一个高保真 DAC,需要位完美的原生流;设备 B 是一个旧的 AV 接收器,无法处理 FLAC。如果使用全局切换,用户必须选择 - 以牺牲另一个设备为代价。使用每个配置文件,每个设备都能得到正确的答案。
实现:
StreamingProfile.ForceTranscoding As Boolean = False。- 持久化模式升级到 v9。v9 之前的文件会加载旧的全局值一次并将其复制到所有配置文件中,从而在升级后保留旧的行为。
- UI:从诊断面板中移除,添加到设备配置文件部分,与强制原生流并列。双向互斥处理程序(每个
CheckedChanged在翻转之前取消订阅另一个,以避免无限循环)。 - 决策点:
Settings.ForceTranscoding→streamingProfile.ForceTranscoding在WriteAudioFileDIDL中。
F05 - “强制小端 PCM”每配置文件
内容: PCM 流(L16/L24 mime 类型)按规范是大端。有些设备错误地期望小端,并在收到正确的大端数据时播放白噪声。F05 切换每个配置文件的字节顺序。
原因: 没有这个,某些设备会输出一堵静态噪音。症状是戏剧性的,原因在不了解 PCM 编码的情况下是不可见的 - 切换为用户提供了一个猜测和检查的修复。
F06 - “不使用原始 PCM”每配置文件
内容: 当设备声称支持原始 PCM 时,插件会使用它。有些设备撒谎 - 它们接受 SOAP 握手,但会损坏实际的原始 PCM 数据,同时正确处理封装在 WAVE 容器中的 PCM。F06 强制 PCM-over-Wave,无论设备宣传什么。
原因: 某些 Marantz 型号尤其如此 - 它们宣传原始 PCM,但只有 WAVE 有效。没有这个,原始 PCM 流会失真,并且没有错误消息指向问题。
F07 - “内容长度”每配置文件
内容: 在 HTTP Content-Length 头部中发送的值。四个选项:
- 默认 - 已知时为实际字节数,未知时省略。
- 无 - 从不发送头部(仅分块编码)。
- 仅 PCM - 仅发送原始 PCM;其他所有内容都省略。
- 固定 - 发送
UInt32.MaxValue - 8192(表示“巨大未知长度”的哨兵)。
原因: UPnP/DLNA 设备对 Content-Length 的反应差异很大。有些需要精确的数字,有些讨厌流上的 Content-Length,有些需要一个哨兵“非常大”的值来保持缓冲。这后来从仅 PCM 扩展到所有输出格式,因为同样的问题出现在转码的 MP3/AAC 流中。
F08 - “不清除 NextURI”每配置文件
内容: 通常,当队列清空时,插件会清除设备的排队 NextURI(发送带有空白 URL 的 SetNextAVTransportURI)。有些设备(特别是 Denon)将空白 NextURI 解释为“停止一切”并立即停止播放。F08 阻止插件清除它。
原因: 没有这个,Denon 用户会遇到设备在队列清空时中断播放的情况。勾选 F08 后,设备会将过时的 NextURI 保留在内存中(无害 - 它只会在下次排队时被覆盖)。
F09 - FLAC 作为转码输出格式
内容: 设备配置文件中的转码格式下拉菜单现在提供 FLAC 以及 PCM 16/24、MP3、AAC、Ogg。选择它会将 BASS 编码器通过 MusicBee 的标准 FLAC 转换命令行(与 MP3/AAC/Ogg 已经使用的机制相同)。
原因: 对于能够很好处理 FLAC 但无法解码源编解码器(例如 Eversolo 接收 MusicBee 的 WMA 库转换为 FLAC)的设备,这可以保留无损质量,而 MP3/AAC 会丢弃音频数据。解锁 N02(5.1 下混控制),如果没有无损转码选项,N02 无法解决。
实现: 在 Encoder.StartEncode 的 Select Case Codec 块中添加一行 - FLAC 加入 MP3/AAC/Ogg 的命令行驱动分支。UI 下拉菜单获得“FLAC”作为第 6 个选项。SettingsDialog 中的加载/保存映射扩展以识别 FileCodec.Flac ↔ SelectedIndex = 5。Mime、DLNA 类型和编码功能已在 ItemManager.GetMimes / GetDlnaType / GetEncodeFeature 中通过早期工作(F21、F26)连接。
无缝播放 (SetNextAVTransportURI)
F10 - SetNextAVTransportURI / NextURI 核心
内容: 真正的无缝播放。当设备在其 UPnP 服务描述中宣传支持 SetNextAVTransportURI 时,插件会在当前曲目结束前预先将下一曲目排队到设备上。设备在内部进行转换,曲目之间没有可听见的间隙 - 就像您在 CD 播放器上听到的那样。这不是“连续流”技巧(它将所有内容连接成一个长流并丢失每曲元数据)。
原因: 旗舰级 Tier-2 功能。作为连续现场表演录制的专辑(现场唱片、古典乐章、DJ 混音)在曲目之间有半秒的静音时听起来不对劲。正确解决这个问题是一个旗舰功能,现在已在此分支中。
注意: 排队的音频通过插件的 HTTP 服务器使用 streamHandle=0(库获取模式)提供服务,这意味着 MusicBee 的音频引擎不参与排队的曲目。权衡:ReplayGain/DSP/EQ 效果不适用于下一曲目。当“强制原生流”开启时(默认),这是可以接受的。
F11 - “禁用 NextURI 支持”每配置文件
内容: 即使设备宣传 SetNextAVTransportURI,此复选框也会强制插件忽略该广告并回退到一次播放一首曲目。
原因: 某些设备宣传 NextURI 但其实现存在错误(崩溃、半转换、挂起)。与其逆向工程每个损坏的设备,不如让用户获得一个“在这里关闭它”的切换。
F12 - 播放列表上的 NextURI 生命周期
内容: 当 MusicBee 触发 NowPlayingListChanged 时,插件会重新评估应为无缝过渡排队的内容。它通过 NowPlayingList_GetNextIndex(1) + NowPlayingList_GetListFileUrl 向 MusicBee 请求新的“下一首”曲目,与设备上当前排队的内容进行比较(通过新的 nextPlaySourceUrl 字段跟踪),如果发生更改则重新排队(如果 MusicBee 说没有下一首曲目,则清除队列)。
原因: 没有 F12,当用户移除/重新排序排队的曲目时,设备会继续播放过时的 NextURI。这在历史上经历了多次迭代,因为每次列表变异都需要不同的处理 - 我们通过信任 NowPlayingList_GetNextIndex(它已经尊重随机播放和全部重复循环)来简化,因此所有变体都通过相同的比较进行。
实现:
- 新的
nextPlaySourceUrl字段存储排队的 MusicBee 库 URL(带有句柄后缀的流式 URL 与库路径不可比较)。 MediaRendererDevice上新的Public Sub RefreshQueuedNextUri()。三种结果:没有 NextURI 排队 → 无操作;排队与新的“下一首”匹配 → 无操作;排队不同 → 使用新的 URL 调用QueueNext(或QueueNext("")清除 - 这尊重 F08 DoNotClearNextUri)。- 在
Plugin.ReceiveNotification下的NotificationType.NowPlayingListChanged中连接。
F13 - NextURI 失败退避
内容: 在同一设备上连续 4 次 SetNextAVTransportURI 失败后,插件会禁用该设备的无缝播放,直到 MusicBee 重启。
原因: 如果设备确实因 NextURI 而损坏(间歇性 SOAP 故障、网络故障),插件否则会在每首曲目上不断重试。F13 停止噪音并静默回退到一次播放一首曲目。
F14 - 重复模式 + NextURI 集成
内容: F14 分为两种情况,在 OnAvTransportStatusCheck 中的 F15 转换检测器处处理:
- 全部重复:MusicBee 将正确的“循环”URL(列表末尾的第 1 首曲目)传递给
Plugin.QueueNext。不需要特殊的插件逻辑 - 设备会转换到它,F15 检测器像往常一样调用Player_PlayNextTrack,它会将 MusicBee 的 NPL 索引循环回 0。 - 单曲重复:MusicBee 将相同的曲目 URL 传递给
Plugin.QueueNext。设备会转换到它(新的流句柄,相同的源)。F15 检测器现在查询Player_GetRepeat()- 如果是RepeatMode.One,它会跳过Player_PlayNextTrack调用,这样 MusicBee 就不会将 NPL 索引从循环曲目中前进。
原因: 如果没有单曲重复跳过,在无缝过渡时调用 Player_PlayNextTrack 会将 MusicBee 前进到列表中的下一首曲目(单曲重复仅影响播放器 UI 中曲目结束自动前进行为 - 下一曲 总是向前移动),这与单曲重复的含义相矛盾。
播放次数注意事项: 在单曲重复模式下,播放次数的增加取决于 MusicBee 3.7.9563+ 是否注意到循环播放。较旧的 MusicBee 版本会正确播放无缝重复,但会错过播放次数的增加。已记录;不阻止。
F15 - 曲目过渡检测状态机
内容: 当设备内部从当前曲目过渡到 NextURI 时,插件需要注意到并告诉 MusicBee 推进其正在播放的索引。否则 MusicBee 会认为它仍在上一曲目上,并且播放次数/UI/Scrobbling 会不同步。
实现: 在每个状态计时器滴答时轮询 GetPositionInfo.TrackURI。当报告的 URI 与我们通过 NextURI 排队的 URI 匹配时,我们会在 MusicBee 上调用 Player_PlayNextTrack 并设置 suppressNextSoapCall,以便生成的 PlayToDevice 不会重新发送 SetAVTransportURI(这会中断无缝播放)。
原因: 没有 F15,设备会播放下一曲目,但 MusicBee 的 UI 会显示它仍在上一曲目上。令人困惑,破坏 Scrobbling,破坏播放次数跟踪。曲目过渡检测需要长时间、每个渲染器的迭代,因为每个渲染器品牌在何时报告 URI 更改方面都有自己的怪癖(有些首先报告 TRANSITIONING,有些直接跳到 PLAYING 并带有新 URI,有些在两者之间有一个短暂的 STOPPED)。
注意: 我们的第一个版本适用于 BubbleUPnP 渲染器。每个设备的边缘情况保留在 B6 中。
F16 - 无缝过渡时的爆音修复
内容: 当排队曲目的源格式(采样率/通道/编解码器)与当前播放曲目不同时,会发生爆音,迫使设备的 DAC 在过渡时重新锁定。F16 添加了一个 NextUri:FormatChange 诊断,在排队时只要格式不同就会触发,并命名双方 - 因此听到爆音的用户可以进行关联。
诊断还指出了缓解措施:勾选设备配置文件上的强制转码。这会将每首曲目统一为单个转码编解码器/采样率/位深,从而完全消除源格式差异。
为什么推迟实际的转码匹配修复: 结构性修复(转码排队曲目以匹配正在播放曲目的格式)需要更改插件的 HTTP 服务器 URL 方案 - 当前 /encode/{id}0.{ext} 原生提供排队文件。F16 的未来 v2 将添加每个格式的 /encode/{id}0_{rate}_{depth}.{ext} 路由并通过编码器连接它们。如果实际设备在强制转码不足以解决问题后仍出现爆音,那么这是一个值得进行的更大架构更改。
今天的实现:
lastSourceUrl字段跟踪当前播放的源 URL。QueueNext读取当前和排队曲目的FilePropertyType.SampleRate/Channels/Kind并在不匹配时记录NextUri:FormatChange。
F17 - 搜索后进度条重新同步
内容: Seek() 函数在成功的 Seek SOAP 后已经调用了 GetPlayPositionInformation(),这修复了“完全不重新同步”的情况。F17 解决了 UPnP 1 秒 RelTime 量化导致的剩余长达 1 秒的漂移:当设备报告的位置四舍五入到用户请求的目标的 1 秒以内时,插件现在信任用户的亚秒级精确值,而不是设备的截断。只有当设备报告的内容明显不同(>1 秒偏差)时,我们才使用其值(搜索落在请求位置之外,例如某些编解码器上的关键帧对齐)。
原因: 没有这个,以 2:30.500 为锚点,根据设备的“2:30”报告进行搜索,会导致进度条显示比实际慢约 500 毫秒。在 F17 之后,进度条与用户在常见曲目内拖动情况下的意图匹配,并且仍然尊重设备报告的关键帧对齐异常情况。
F18 - 连续流 / NextURI 互锁
内容: 现在有两个互锁:
- 运行时: 当
Settings.ContinuousOutput开启时,QueueNext会提前返回False。连续流是其自身的无缝机制(一个长的连接流);在其之上发送SetNextAVTransportURI会使设备混淆每个曲目是离散 URI 还是连续流的一部分。 - UI: 当用户勾选全局连续流复选框时,当前显示的配置文件的
forceNativeStream会自动取消勾选。连续流总是转码,因此强制原生流在这种组合中毫无意义。
原因: 防止用户同时启用两种冲突的无缝机制。没有 F18,设备将同时接收连续流 URI 和每个后续曲目的 NextURI,其行为取决于渲染器。
F19 - 忽略空白 NextURI 错误
内容: 当使用空白 URL 调用 SetNextAVTransportURI 时(例如列表中的最后一首曲目),某些设备会返回 SOAP 故障。F19 会静默吞噬这些故障 - 记录但不作为错误传播。
原因: “没有下一首曲目”是正常情况,而不是错误。将其视为致命错误会污染日志,并在某些流程中触发重试风暴。
Mime 类型和 DLNA 元数据
F20 - MP3 mime → audio/mpeg
内容: 符合标准的 MP3 mime 类型是 audio/mpeg,而不是 audio/mp3。后者是大多数设备都能容忍的常见错误名称,但更严格的渲染器会拒绝它。
原因: 静默修复了遵循标准的更严格设备上的播放问题。yaiol 代码库已经正确处理了这一点;无需更改。
F21 - Mime 类型顺序:非 x- 变体优先
内容: 当设备同时宣传 audio/flac 和 audio/x-flac 时,插件会首先返回非 x- 变体。对于任何同时具有标准和实验性 mime 的编解码器也是如此。
原因: x- 前缀标记实验性/非官方 mime。一些渲染器在标准形式下表现更好。微小的重新排序,实际影响。
F22 - Opus mime 类型支持
内容: 识别 Opus 为可流式传输的音频编解码器;在提供 Opus 曲目时发送 audio/opus mime。
原因: Opus 现在很常见(现代语音/音乐折衷编解码器)。没有 F22,插件将拒绝流式传输 Opus 文件,即使是支持它们的设备。
F23 - Monkey Audio (APE) 源文件支持
内容: 识别 .ape 文件为有效的流式传输/转码源编解码器。
原因: APE 是一种无损格式,拥有小众但忠实的用户群。添加它成本很低,并为这些用户解锁了库。
F24 - AAC / ALAC mime 回退
内容: 如果设备支持 AAC 或 ALAC 但未在其 UPnP 服务描述中明确宣传它们,插件仍会将其作为回退提供。
原因: 许多设备确实可以很好地处理 AAC,但忘记在其功能 XML 中列出它。没有 F24,插件甚至不会尝试,强制转码。有了 F24,插件会尝试并让设备在可能的情况下原生处理它。
F25 - 原生 + 编码 WAV 流的 DLNA 类型标志
内容: DLNA 类型标志(例如 LPCM、WAVE、MP3 等配置文件标识符)需要与设备接收的内容匹配。F25 确保原生流和编码 WAV 流被正确标记。
原因: DLNA 类型不匹配会导致某些设备完全拒绝播放或应用错误的解码器。
F26 - FLAC 文件的 DLNA 头部
内容: FLAC 流在其头部中获得正确的 DLNA 配置文件标识符。
原因: 没有它,一些支持 FLAC 的设备无法识别流。
F27 - 元数据中的比特率计算修复
内容: 连续流 res@bitrate 之前计算为 (sampleRate * channels * bitsPerSample) / 1000 - kbps,与 UPnP DIDL 规范定义的属性为每秒字节数相差约 125 倍。现在除以 8 而不是 1000。
原因: 设备上比特率显示错误 - 在大多数渲染器上是外观问题,但有些渲染器会根据该值分配流缓冲区,并在看起来比实际小约 125 倍的流上出现卡顿。非连续源文件路径已经正确处理了这一点((bitrate_kbps * 1000) \ 8 = 字节/秒);只有连续流路径是错误的。
F28 - 元数据时间格式修复 (Marantz)
内容: DIDL 中的 res@duration 格式为 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 在其 DateTime 格式字符串中使用了 hh(12 小时制)而不是 HH(24 小时制)。任何在 13:00 到 23:59 之间添加或播放的曲目都会在显示该字段的设备上以错误的小时数呈现(例如 17:42 → “05:42”)。现在使用 HH。
F29 - 编码 MP3 搜索支持 (CBR)
内容: 转码的 MP3 流现在宣传 DLNA.ORG_OP=11(字节和时间搜索),而不是 DLNA.ORG_OP=10(仅字节)。以前拒绝转码 MP3 时间搜索的设备现在可以正常驱动其进度条/搜索 UI。
原因: MusicBee 的转码器以 HighQuality 预设生成恒定比特率 MP3,因此字节 ↔ 时间映射是线性的 - 设备可以自己将时间搜索请求转换为 HTTP Range 字节搜索,而无需编码器端支持。宣传 OP=11 解锁了设备上的该 UI。没有 F29,用户在转码 MP3 中搜索时,要么搜索被静默忽略,要么被丢到曲目开头。
实现: 重构了 ItemManager.vb 中的 GetEncodeFeature,将内联 If 分解为可读的 If/ElseIf/Else 链。MP3 明确获得 OP=11;其他非 PCM 编解码器保留 OP=10。AAC/FLAC 等没有变化 - 这些需要编解码器特定的 CBR 验证,而 MusicBee 不保证。
F30 - 处理 .mpeg 文件扩展名
内容: 扩展名为 .mpeg(甚至更罕见的 .mpe)的文件现在在 GetCodec 中被识别为 FileCodec.Mp3。在 F30 之前,它们返回 FileCodec.Unknown 并被静默地从库中拒绝 / 无法作为转码源。
原因: 旧的 MPEG-1 Layer 3 存档有时使用 .mpeg 而不是 .mp3(规范允许两者)。30 万个库中的少数文件足以让人觉得“MusicBee 显示它们但插件不显示” - 这让用户感到困惑。
播放行为
F31 - 电台流自动使用连续模式
内容: WriteAudioFileDIDL 现在通过 Library_GetFileProperty 探测源 URL 的 Kind 属性,并将任何 Kind 以“Stream”结尾的文件(MusicBee 报告电台为“MP3 Stream”、“Internet Stream”等)视为连续流,无论全局 Settings.ContinuousOutput 切换如何。使用连续流 DIDL 分支(标题:“Continuous Stream”,id="continuousstream",固定 PCM/Wave 输出);设备看到一个单一的无限风格流。
原因: 电台流没有曲目边界,没有固定长度,没有搜索。在 DIDL 中将它们视为离散文件会导致插件宣传不存在的字节范围和持续时间。当 MusicBee 已经告诉我们“这是一个流”时自动切换,消除了用户不应该考虑的陷阱。
范围: 仅当 MusicBee 驱动播放时适用(musicBeePlayToMode)。库获取路径(UPnP 客户端浏览)不变 - 那里的电台 URL 很少见,用户可见的行为不应在没有明确测试的情况下改变。
F32 - 编解码器广告回退
内容: 如果设备不宣传某些编解码器(或者插件无法解析设备的 capability XML),插件不会立即拒绝流。相反,它会尝试提供流并让设备决定。
原因: 许多设备的 capability XML 不完整或不可读,但实际上可以很好地处理编解码器。F32 用一个小小的“最佳猜测并尝试”来代替彻底拒绝。
F33 - 进度条同步改进
内容: 轮询之间的位置已经从单个锚点(currentPlayStartTicks)进行挂钟外推,因此进度条以亚秒级速率平滑更新。剩余的抖动源是新启动曲目的初始锚点:以前的代码假定在状态计时器首次注意到状态变为播放时 position=0,但那时设备可能已经播放了 100-500 毫秒(一个轮询间隔)。MusicBee 的进度条会从 0 开始,然后当实际情况赶上时向前跳。
F33 修复:当新曲目首次过渡到播放状态时(currentPlayStartTimeEstimated=True),调用 GetPlayPositionInformation() 获取设备的实际当前位置,然后以此为锚点。UPnP 只报告 1 秒分辨率,因此锚点仍然是量化的,但它比假设 0 更接近真实情况。
原因: 更平滑 + 更准确的进度显示,尤其是在曲目更改后。无法绕过 1 秒 UPnP 报告分辨率本身 - 这是规范。
F34 - 曲目更改后进度条抖动
内容: 当为新曲目调用 PlayToDevice 时,插件过去会在 SOAP-Play 和第一个状态计时器轮询检测到新播放状态之间的约 100 毫秒窗口内,将 currentPlayPositionMs 和 currentPlayStartTicks 保留在其上一曲目的值。MusicBee 的进度条会短暂显示上一曲目的结尾,然后跳回 0,然后上升。F34 在 PlayToDevice 入口处将两者归零 - 在我们知道曲目更改正在发生的那一刻,在任何 SOAP 工作之前。
原因: 快速跳过用例(手动下一曲或无缝过渡)中的视觉故障。现在 MusicBee 的首次 PlayPositionMs 查询在 Play 后干净地返回 0,然后 F33 的 GetPlayPositionInformation 在第一次状态更改滴答时将其细化为设备的实际位置。
实现: PlayToDevice 顶部的四行,与 F33 的过渡时间精确锚定配对。
F35 - “强制转码”错误
内容: 强制转码在某些组合下仍然可能跳过转码。在 F04 每个配置文件重做之后,两个特定的漏洞被弥补:
- 与 ForceNativeStream 的优先级。 当两者都为 True 时(这可能发生在模式迁移或部分设置文件之间),ForceTranscoding 现在完全胜出(
If streamingProfile.ForceTranscoding Then forceEncode = True ElseIf streamingProfile.ForceNativeStream Then forceEncode = False)。UI 互斥阻止用户同时勾选两者,但运行时保护处理从磁盘加载不一致的任何状态。 - bypassTranscodeDecision 逻辑。 以前:
streamingProfile.ForceNativeStream AndAlso Not Settings.ForceTranscoding。现在:streamingProfile.ForceNativeStream AndAlso Not streamingProfile.ForceTranscoding- 相同的优先级规则,但在相同的每个配置文件范围。
原因: “强制”应该意味着强制。如果用户明确为设备启用了强制转码,插件绝不能静默地回退到原生流式传输,无论其他标志如何组合。
F36 - 渲染器关闭异常
内容: 将 Plugin.ReceiveNotification 包装在一个顶层 Try/Catch 中,该 Try/Catch 记录任何未捕获的异常,而不是让它传播回 MusicBee 的通知泵。
原因: 来自 MusicBee 的通知(PlayStateChanged、VolumeMuteChanged 等)分派到 ControlPointManager,该管理器通过 SOAP 与渲染器通信。各个调用站点已经在其 SOAP 调用周围有 Try/Catch,但足够奇怪的时序情况(例如,渲染器在同一通知处理程序中的两个 SOAP 调用之间死亡)仍然可能逃逸。顶层包装器是最终的安全网,因此用户永远不会看到 MusicBee 弹出的通用“TargetInvocationException”。
实现: 将现有主体重命名为 ReceiveNotificationInternal,并添加一个薄包装器 ReceiveNotification,它执行 Try { ReceiveNotificationInternal(...) } Catch { LogError(...) }。ControlPointManager 内部(围绕每个 PostSoapRequest 调用)预先存在的每个方法 Try/Catch 基础设施保持不变 - F36 是双重保险。
F37 - 长曲目搜索触发错误过渡
内容: 在某些渲染器上,在长曲目中搜索可能会产生短暂的停止→播放循环。如果没有区分,ProcessNewPlayState.Stopped 会将其视为曲目自然结束并调用 Player_PlayNextTrack,在用户只想拖动时推进 MusicBee。F37 在 Seek() 中标记 lastUserInitiatedSeek,并在停止处理程序中添加一个 5 秒的保护(镜像现有的 lastUserInitiatedStop 窗口)。
原因: 搜索期间静默跳到下一曲是那种没人能猜到原因的错误之一 - 用户会想“奇怪,我尝试快进,现在它正在播放下一首歌”。修复是机械的:与已有的用户停止区分模式相同。
F38 - 改进了对易崩溃编解码器的搜索处理
内容: BubbleUPnP 在 MP3 搜索时崩溃是典型的症状。经过审计,当前的 yaiol 搜索代码已经做了正确的事情 - 原生路径正确处理 HTTP Range(206、Content-Range、AcceptRanges),编码路径宣传 X-AvailableSeekRange 并解析传入的 timeSeekRange.dlna.org / npt 头部,DLNA.ORG_OP 标志反映实际流功能(带有针对问题 Platinum 设备的 DisablePcmTimeSeek 选项)。在当前的 BubbleUPnP 4.6.4 上进行了用户测试:未观察到崩溃。
原因: BubbleUPnP MP3 搜索崩溃报告于 2024 年左右,此后该应用程序已进行了约 16 个月的修复。F29(编码 MP3 OP=11)是可能重新暴露它的新变量;在测试版本上没有。
如果崩溃再次发生: 修复形式将是每个配置文件的“有限搜索”切换,强制在标记的编解码器上使用 DLNA.ORG_OP=10(仅字节) - 镜像 DisablePcmTimeSeek 对 PCM 的工作方式。届时再添加,而不是预先添加。
UI 和日志记录
F39 - “添加”按钮选择新配置文件
内容: 点击设备配置文件列表中的“添加”会创建一个新配置文件并自动选择它,以便用户可以立即编辑字段。我们的分区对话框重构已经这样做了 - 直接添加路径和从模板添加路径都以 Me.activeStreamingProfiles.SelectedIndex = Me.activeStreamingProfiles.Items.Count - 1 结束。检查确认我们的分支已经处理了这一点 - 无需更改。
原因: 小小的 UX 痛点,结果在这里已经不是痛点了。
F40 - 更大的最大连接数 + 警告日志
内容: 插件的并发流限制(SemaphoreSlim 围绕 Sockets_Stream_File / Sockets_Encoder_Start)硬编码为 4。F40 使其在“常规”设置页面中可由用户配置(默认 16,范围 1-256),在请求必须等待插槽时添加 MaxConnections 日志行,并且如果 MusicBee 启动以来至少达到一次限制,则在设置对话框左下角显示红色 ⚠ 最大连接数徽章。
原因: 当设备触发并行请求时(某些 Marantz/Linn 在封面扫描期间,BubbleUPnP 的元数据探测与活动播放同时进行),额外的请求会静默地阻塞在信号量后面 - 用户看到“设备缓慢”而没有可见原因。日志行对于技术调试很有用,但非技术用户从不阅读日志。设置对话框中可见的徽章使任何打开插件偏好设置的人都能发现达到限制的情况。
实现:
- 将等待集中在
MusicBeeUpnp.vb中的WaitOnSendBarrier(logTag)中;两个调用站点(MediaServerDevice.GetFile、Encoder.StartEncode)都使用它。 Settings.MaxConnections保存在设置模式的 v8 中。Plugin.MaxConnectionsHit是一个粘性会话标志,在WaitOnSendBarrier内部设置;仅在 MusicBee 重启时重置。SettingsDialog.maxConnectionsBadge是一个红色粗体标签,位于(16, 410),仅当Plugin.MaxConnectionsHit为 True 时显示。有一个工具提示解释原因和补救措施。- 信号量在类型加载时初始化一次,因此更改设置需要 MusicBee 重启(在字段标签中注明)。
F41 - 记录“因 ReplayGain/DSP 而编码”
内容: 不再是单独的“为 RG 编码”/“为 DSP 编码”日志行,F42 的单个 StreamDecision 行包含 MB-DSP/EQ、MB-ReplayGain、Profile-DSP/EQ、Profile-ReplayGain 作为累积原因。诊断价值相同,噪音更少。
原因: 用户可以在一个日志行上看到给定曲目发生转码的所有原因,而不是分散的。有关完整详细信息,请参阅 F42。
F42 - 记录“渲染器不支持源编解码器”
内容: 为每个播放到设备的曲目添加了一个 StreamDecision 日志行,说明是“原生 CODEC”还是“转码 CODEC→CODEC reason=…”。原因字段累积了触发转码的每个条件:MB-DSP/EQ、MB-ReplayGain、Profile-DSP/EQ、Profile-ReplayGain、WebFile、VirtualFile、ForceTranscoding(global)、SampleRate<min/SampleRate>max、DownmixToStereo、DeviceLacksCodec(X)、BandwidthConstrained。
原因: 用户对他们期望原生流式传输的文件上意外的 CPU 峰值感到困惑。每个曲目的一行日志会准确告诉他们哪个条件导致了转码 - 如果该字段显示 DeviceLacksCodec(Flac),他们会立即知道设备的协议信息不完整,并且可能希望 F32 的回退生效。
实现: 单个累加器字符串在决策链中逐步构建;在末尾记录一次。受 Settings.LogDebugInfo 控制,以避免在生产环境中产生日志噪音。
F43 - SetNextAVTransport 日志显示源 URL
内容: QueueNext 日志条目现在包含 source=<MusicBee library path> 以及 stream=<HTTP streaming URL>。相同的更改应用于成功路径和失败路径(QueueNext:Failed)。
原因: 在调试排队曲目问题时,流式 URL(/encode/aabbccdd0.flac)本身是不透明的 - 每首曲目都相同。源 URL 是人类可 grep 的库路径,它准确告诉您 MusicBee 尝试排队的是哪个文件。
F44 - 更好的 mime 类型错误日志记录
内容: 在 Activate 期间有两个新的日志条目:
Activate:MimeUnverified- 针对设备GetProtocolInfo响应中每个格式错误的条目触发,命名哪个条目无法解析(因此用户可以看到例如“Marantz 为某个编解码器返回了http-get:*::*- 功能未验证,F32 的回退将猜测”)。Activate:NoSinkInfo- 如果设备根本没有返回<Sink>元素,则触发一次。这意味着SupportedMimeTypes保持 Nothing,并且IsCodecSupported降级为“假设一切正常” - 当稍后出现“设备拒绝流”错误时,这是有用的上下文。
原因: 在 F44 之前,这些静默的功能回退让用户猜测为什么他们的曲目要么被转码超出预期,要么被设备拒绝。现在,对 Activate: 的一次 grep 可以显示设备的 capability 信息是否可用。
F45 - 更好的元数据错误日志记录
内容: ContentDirectoryService.vb 中的 Browse 异常日志已在早期的 yaiol 工作(Alia Vox 错误会话)中通过 ObjectID 和堆栈跟踪进行了丰富。F45 进一步扩展了它,包括 BrowseFlag(元数据与子项)、Filter(客户端请求了哪些属性)、sortCriteria 和 partialResultLength(在失败之前生成了多少字节的 DIDL - 指示坏曲目在批次中的位置)。
原因: 当 DIDL 中途出现问题时,partial-length 值会告诉您失败是在批次的第一个曲目(partial=0)还是中途(partial=N) - 结合批次的 startingIndex,您可以识别出有问题的曲目索引。Filter 和 BrowseFlag 解释了客户端想要哪种类型的浏览;有时仅元数据浏览失败,而相同 ID 的子项浏览成功。
网络
F46 - 自动模式仅在真实网络适配器上广播
内容: 在自动接口模式下,插件以前会在每个正常工作的 IPv4 适配器上广播自身(SSDP)。在同时运行 VPN 隧道(NordLynx)或虚拟交换机(Hyper-V / WSL / Docker)的机器上,同一个媒体库也会在这些适配器上被广播,因此您投射所用的控制点会发现服务器两到三次,并将媒体库列为重复副本。自动模式现在仅保留拥有真实 IPv4 默认网关的适配器(HasIPv4Gateway)——隧道和虚拟交换机适配器没有默认网关——因此它们会从广播列表中剔除。用户固定的地址仍然绝对优先(仅在该接口上广播),如果没有任何适配器报告网关,选择器会回退到所有适配器,因此广播地址列表永远不会为空,插件也不会变得不可见。
原因: 重复并不是因为"使用了 VPN"——而是因为同时在 LAN 适配器和隧道/虚拟适配器上广播,导致一个控制点在两个地址上看到同一个服务器。消费级 VPN(NordVPN/NordLynx)只隧道传输发往互联网的流量;DLNA 渲染器位于局域网中,本地子网流量绕过隧道,因此隧道适配器无论如何都不会到达渲染器——剔除它只会移除一个幻影副本,绝不会移除可用路径。网关检测是区分真实 LAN/Wi-Fi 适配器与隧道或虚拟交换机的廉价而可靠的信号。它与 N05 互补(N05 修复了此类链路上广播如何发送——用组播代替广播);F46 则决定究竟在哪些适配器上广播。
已知限制: 网状/远程访问 VPN(Tailscale、ZeroTier、连回家中的 WireGuard)的渲染器确实位于隧道另一端,其适配器通常没有默认网关,因此自动模式也会将其剔除。这类用户应改为固定 VPN 地址,固定地址优先于网关过滤。