给电视装上 NAS 音乐播放器:一个开源项目如何让客厅变成私人音乐厅

给电视装上 NAS 音乐播放器:一个开源项目如何让客厅变成私人音乐厅

给电视装上 NAS 音乐播放器:一个开源项目如何让客厅变成私人音乐厅

当你的 NAS 里躺着几万首无损音乐,电视却只能放爱优腾——这合理吗?

一、先说说痛点

先问一个问题:你的电视都在干什么?

对大多数人来说,电视=流媒体播放器。爱奇艺、优酷、腾讯视频、B站……充完这家充那家,每年会员费上千,看的广告比正片还多。

但如果你是个音乐爱好者,情况更尴尬。

你或许攒了一台 NAS,装上了 Jellyfin 或 Navidrome,把辛辛苦苦从 CD 抓轨、从各路渠道收集的无损音乐整整齐齐地码好。手机上有 VLC、有 Plexamp,电脑上有 Foobar2000——一切都很完美。

但电视呢?

电视那块大屏幕、那套音响系统——明明是最适合听音乐的场景之一——却几乎找不到一个好用的音乐播放器。Android TV 的应用商店里翻来覆去就是那几个流媒体 App,偶尔有几个本地播放器,UI 还停留在十年前的设计水平。更别提歌词显示了——在电视上看歌词?不存在的。

这就是 NASMusicTV 要解决的问题。

一个开源的 Android TV 音乐播放器,连接你的 NAS(Jellyfin/Navidrome),把几万首音乐搬到电视上,还带逐字卡拉 OK 歌词


界面截图

二、它到底能做什么?

我先把功能表列出来,但请耐心看完——每一条背后都有些值得讲的故事。

🎵 连接你的 NAS

支持 Jellyfin 和 Navidrome 两种后端。填上服务器地址、账号密码,电视自动扫描你的音乐库。专辑、艺术家、歌曲、流派、年代——该有的浏览方式全都有。

这里有个数字:在 v2.2.0 之前,它最多只能加载 1000 首歌曲。因为 Jellyfin API 默认返回 Limit=1000,超过的部分直接截断。如果你的音乐库超过 1000 首(大多数人都超过),后面的歌就凭空消失了。

修复方式?分页。每页 200 首循环请求,上限放开到 50000 首。现在几万首歌的音乐库,一次加载完,显示「已加载 3,247 / 共 3,247 首」,那种感觉还挺爽的。

📜 歌词系统——这是最大的亮点

老实说,这是我当初做这个项目最大的动力。

Android TV 上能找到的音乐播放器,几乎没有一个是好好做了歌词显示的。有的干脆不显示,有的只显示一个静态文本,有的号称支持歌词但实际根本解析不了 LRC 文件。

NASMusicTV 的歌词系统有这几个层次:

  1. MP3 内嵌歌词:直接从 ID3 标签里提取
  2. 本地 LRC 文件:扫描歌曲同目录的 .lrc 文件
  3. NAS 后端歌词 API:Jellyfin 10.8+ 的歌词端点
  4. 网络匹配:用歌曲名+艺术家自动搜索酷狗、网易云的在线歌词,匹配成功后本地缓存

加载失败自动 fallback 到下一个来源。来源切换在播放页一键完成,当前歌词来源有标签显示(内嵌/本地/网络/在线)。

但真正让我兴奋的是 v2.4.1 加入的逐字高亮(卡拉 OK 模式)。

逐行滚动歌词已经不错了,逐字变色才是真正的沉浸感。当前唱到哪个字,那个字就变色——50ms 刷新频率,过渡平滑到几乎看不出卡顿。实现方式也挺有意思:一个独立的 20fps 高频时钟,基于每秒进度锚点 + 流逝时间插值估算当前字位置,完全不依赖 ExoPlayer 的回调。

你说电视大屏有什么用?播一首《加州旅馆》,左侧专辑封面,右侧歌词逐字变色——这是我见过最美的音乐播放体验之一。

🔍 拼音搜索——”zjl”找到周杰伦

中文音乐搜索有个天然的坑:遥控器。

你用电视遥控器搜「周杰伦」,需要按多少次方向键?用拼音首字母「zjl」直接定位——这才是电视该有的交互方式。

实现上用了 TinyPinyin 库,因为最低兼容 API 22(Android 5.1),而 Android 自带的 ICU Transliterator 需要 API 26+。这其实是个挺典型的 Android 兼容性问题:你总得支持那些还在运行的低版本设备,尤其是在电视这个品类上。

现在你可以输入「zjl」搜到周杰伦、「wbq」搜到王宝强、「gyy」搜到高耀太——不只是艺术家,专辑名、歌曲名同样支持拼音首字母匹配。

🌐 网络音乐——没有 NAS 也能用

这个功能最初是 v2.4.0 加的,后来独立成一个顶级 Tab。

它通过 Meting-API(一个开源的网易云/QQ/酷狗 API 代理)实现在线搜索歌曲。也就是说,即使你的 NAS 没开机,电视也能播网络音乐。

Meting-API 端点有 3 个预设(Mikus、Redcha、Qijieya),当前端点失败或超时自动 fallback 到下一个。三个全挂了?界面会显示红色提示。用户也可以在设置里配置自定义端点。

网络歌曲的播放链接是 302 重定向的,需要实时解析真实 mp3 URL,不做持久缓存。Coil 图片库自动跟随重定向,封面显示几乎不需要额外代码。

不过老电视盒子有个坑:很多 Android TV 设备还是 Android 5.1(API 22),出厂就没有 Let’s Encrypt 的根证书。Meting-API 的 HTTPS 连接直接 SSL 握手失败,搜索功能全部瘫痪。最终的解决方案是配置了一个信任所有证书的 TrustManager——一种务实的妥协,因为网络音乐搜索本质上是公开服务,不是敏感数据传输。

还有一个细节:网络歌曲的来源标识——播放页标题下方会显示一个 NET 标签,一眼就能区分当前是在播 NAS 的还是网络的。

说到推荐歌单,有 20 多个预置歌单(热歌榜、新歌榜、欧美流行、抖音热门等),按日期做每日轮换,每天推荐的内容都不一样。歌单展示也从最初的简单列表改成了双列卡片网格,每张卡片附带封面轮播 + 「换一批」按钮。

🌤️ 天气电台——根据窗外天气推荐音乐

这是 v2.6.0 加的一个「浪漫」功能。

获取当前位置的天气(通过 OpenWeatherMap),然后根据天气「心情」自动匹配歌单:

  • ☀️ 晴天 → 轻快、阳光的歌
  • 🌧️ 雨天 → 适合沉思的旋律
  • ❄️ 雪天 → 温暖舒缓的调子
  • 💨 大风天 → 激昂澎湃
  • ☁️ 阴天 → 安静的氛围音乐
  • 🌙 夜晚 → 静谧的睡前曲

歌曲来源既可以是 NAS 曲库,也可以是网络搜索结果,混合编排成一个电台队列。

实际上这背后是 OpenWeatherMap 的 One Call API,获取实时天气数据 + 5 日预报,然后按天气类型映射到预设的「心情关键词」列表,再去搜索匹配的歌曲。技术实现不算复杂,但效果意外地好——特别是外面下着雨,电视自动播起一首和雨天氛围契合的歌时。

🎚️ 均衡器 + 歌词字体缩放 + 封面滤镜

这些是 v2.4.0 之后陆续加入的细节功能。

均衡器有几种预设方案(流行、古典、摇滚、爵士、舞曲等),也可以手动调节 10 个频段增益。歌词字体大小从 0.7x 到 1.6x 可调,设置永久保存。封面滤镜支持高斯模糊(0-25dp)和暗色遮罩(0-100%),营造沉浸式的播放背景。

说实话,均衡器这个功能在手机上很常见,但在电视上——用遥控器调均衡器——体验还挺奇特的。但有人就是需要这个,所以做了。


三、一些有趣的技术细节

编码问题的噩梦

如果你用过 NAS 管理中文音乐,你一定遇到过乱码。

很多中文 MP3 的 ID3 标签使用了 GBK/GB2312 编码,但某些工具(或者 Jellyfin 的某些版本)会把它当作 Latin-1 或 UTF-8 来解码,结果就变成了「ä½ å¥½」这样的天书。

NASMusicTV 的解决方案是 EncodingUtils.fixEncoding()

  1. 先尝试正常 UTF-8 解码
  2. 检查字符串中是否存在 U+FFFD(替换字符)——这是不可恢复乱码的标志
  3. 如果有,用 GBK 再解码一次
  4. 如果还没有,检查是否包含希腊/西里尔字母——有的话就不回退,因为合法的希腊/西里尔元数据不应该被破坏

这个过程在 v2.4.3 里还修过一回:原来只要出现希腊字母就触发 GBK 回退,结果是合法的希腊音乐专辑名全乱了。修正后只对 U+FFFD 回退。

这个 bug 修了多次,从 v2.2.0 到 v2.4.3 跨了 4 个版本——没有更好的办法,因为问题本质上是不可能完全在客户端解决的(如果字符已经被错误地转换为 Unicode 码点,原始编码信息就已经丢失了)。

播放队列持久化:又一个序列化陷阱

播放队列持久化听起来简单:退出时保存歌曲列表,启动时恢复。

但网络歌曲的播放链接有时效性(302 重定向链接几分钟后过期),所以持久化时要把 streamUrl 字段置空。启动恢复时,NAS 歌曲通过 adapter.getSongsByIds() 刷新链接,网络歌曲在用户按播放时实时解析。

然后是那个经典的 ProGuard 崩溃:v2.5.1 发布后,用户一启动就闪退。

原因?Gson 在 R8 混淆后泛型信息被剥离了。AppPreferences$LastQueueData.songs: List<Song> 反序列化出来不是 Song 对象,而是 LinkedTreeMap——一种内部的 JSON 对象表示。代码拿到 LinkedTreeMap 后当 Song 用,自然就崩溃了。

修复方式:在 proguard-rules.pro 里加了一行 -keep class com.nasmusic.tv.data.prefs.**,保留下持久化数据类的泛型签名。

这种 bug 在 Android 开发里太典型了:本地测试时一切正常(debug 构建不优化),Release 构建一混淆就出事。

SSE 与「网络歌词」的奇怪联动

v2.4.1 加入了一个功能:NAS 歌曲切换到「在线歌词」来源时,自动搜索网络封面,加入轮播候选队列。

为什么?因为很多 NAS 里的音乐文件没有内嵌封面图,或者是低分辨率的老封面。从网络搜索到的封面通常更新、更清晰。

但是有一个微妙的副作用:当歌词来源切回「内嵌」时,网络封面会被清除。这个设计是为了保持语义一致性——你用的是「内嵌歌词」,那封面也应该用内嵌的。但如果用户只是切回内嵌但还想保留网络封面?那就需要在 UI 上再加一个独立开关了——功能太多,暂时没做。

D-Pad 导航:被低估的工程挑战

这个项目大概是你能找到的 D-Pad 导航最密集的代码库之一。

Android TV 只有方向键 + OK 键。你不能「点」一个按钮,只能「导航到」它。这在简单列表里还好,但一旦遇到歌词按钮、队列移动按钮、均衡器滑块、字体缩放按钮……焦点管理就变成了噩梦。

有几个典型问题:

  • 歌词高亮模式跨页面丢失:用户在播放页选了逐字高亮,切换到设置页再回来,发现变回逐行了。原因是 Compose 的 rememberSaveablewhen(currentScreen) 切换时整个页面离开 Composition,状态被 GC 了。修复:把 lyricsHighlightMode 提升到 ViewModel 的 StateFlow。
  • 设置页左侧导航栏无法滚动:模拟器上设置页有 6 个分区,遥控器翻到第 4 个就翻不下去了。原因是 Column 没有加 .verticalScroll(),超过屏幕高度的焦点项被裁切。加了可滚动之后,还要用 Compose 的 BringIntoView 确保焦点项自动滚入可视区域。
  • 搜索输入框被列表覆盖:网络搜索时,输入法弹出来却被底部的歌曲列表挡住了。修复:把搜索输入框放到系统级 Dialog 里。
  • 队列按钮无法聚焦:每个 SongRow 右侧的删除/移动按钮,放在 FocusableSurface 内部时焦点导航会被外层拦截。修复:移出到兄弟级,用 Box(focusGroup) + 独立 focusable 节点。

每个问题单看都不大,但累积起来就是几十次代码审查和迭代。

还有一处被反复打磨的细节是 BACK 键的三级响应:第一下关闭弹窗或对话框 → 第二下回到播放页 → 第三下弹出退出确认。这种层级在手机上很自然,但在电视上需要处理更多边缘情况——比如当前在设置页、歌词弹窗打开时,按 BACK 应该先关弹窗还是先退出设置页?

👥 多艺术家拆分

中文音乐里有大量的「张三 feat. 李四」「王五 & 赵六」「甲某 × 乙某」这类合作曲目。NASMusicTV 的 ArtistSplitter 支持 feat.ft.with&//×vs 多种分隔符解析,拆分结果作为独立艺术家展示。

这意味着如果你搜「李四」,不仅能看到李四自己的专辑,还能看到他参与的所有合作曲目。详情页里每首歌的 artist 字段也做了拆分,「播放全部」时会自动去重。


四、版本的故事

从 v1.0.0 到现在的 v2.6.2,这个项目走了将近——从 CHANGELOG 来看——实际上是大概 6 周的密集开发(从 6 月 21 日到 7 月 3 日版本号到了 2.6.2)。等等,6 周从 1.0.0 到 2.6.2?这频率有点吓人。

事实上,这个项目从一个初始可用的版本开始,经历了几乎每天发版的阶段:

  • v1.0.0:Jellyfin/Navidrome 连接 + ExoPlayer 播放 + LRC 歌词。能用。
  • v2.0.0-v2.1.0:UI 大改版、详情页、流派/年代浏览、卡拉 OK 逐字高亮。
  • v2.2.0:编码修复、分页加载、DI 容器重构、密码加密存储、GitHub Actions CI。工程化完善。
  • v2.3.0:拼音搜索、回归测试框架。
  • v2.4.0-v2.4.4:网络音乐搜索、播放队列持久化、逐字歌词高频刷新、多封面轮播、4 轮 Code Review 修复。
  • v2.5.0-v2.5.1:网络音乐独立 Tab、推荐歌单、端点 429 限流修复。
  • v2.6.0-v2.6.2:天气电台、歌词字体缩放、封面滤镜、OpenCodeReview 全量审查。

CHANGELOG 全文 430 行,记录了每一次 bug 修复和功能新增。如果去翻 git log,能看到大量的「修复: xxx 在某种条件下崩溃」这类提交。

这其实挺诚实的——不是那种「我们一次就做对了」的叙事,而是真实开发中不断发现问题、不断修复的日常。


五、技术栈一览

语言        Kotlin
UI          Jetpack Compose for TV (tv-material alpha)
播放引擎    Media3 / ExoPlayer
网络层      OkHttp
图片加载    Coil
数据持久化  DataStore (Preferences) + Gson
拼音        TinyPinyin (兼容 API 22+)
最低 SDK    API 22 (Android 5.1)
目标 SDK    API 34 (Android 14)
架构模式    MVVM(手动 DI,无框架)
CI          GitHub Actions
测试        JUnit 4 + Robolectric + Mockito
协议        GPL v3

没有用 Hilt/Dagger(手动 DI),没有用 Retrofit(直接用 OkHttp),没有用 Jetpack Navigation(手动 when(currentScreen))。如果按照「现代 Android 开发最佳实践」的标准,这算「不够主流」。但它的优势是依赖少、包体小、更容易理解和调试——对于一个个人开源项目来说,这可能是更务实的选择。

而且这个手动 DI 其实也经历过改造。v2.2.0 之前用的是静态 companion object 的单例模式,到处 NasMusicApp.getInstance()。后来重构为 NasMusicApp Application 类直接持有所有管理器实例,其他组件通过 context.applicationContext as NasMusicApp 获取。算不上优雅,但胜在简单直观——整个 DI 逻辑就在一个文件里,不用翻半天 Dagger 模块图。


六、一些数字

  • 代码路径backend/impl/backend/network/backend/weather/lyrics/player/ui/screens/util/data/model/
  • CI:Push/PR 自动构建 Debug APK
  • 版本号:当前 v2.6.2,versionCode 16
  • 支持的 ABI:arm64-v8a、armeabi-v7a、x86_64
  • Release 构建:启用 ProGuard 混淆 + 资源压缩
  • 测试:Robolectric 单元测试覆盖核心工具类(PinyinUtils、ArtistSplitter、LrcParser)

七、写在最后

老实说,这个项目不是什么划时代的作品。

它没有用最前沿的技术,没有 AI,没有区块链,没有云原生。它就是一个很务实的工具——把 NAS 里的音乐搬到电视上,做好歌词显示,支持遥控器操作,然后就够了。

但就是这种「务实」,在 Android TV 生态里反而成了稀缺品。电视端的应用开发一直是个小众领域:开发者少、用户少、赚不到钱。大的流媒体平台有自己的 App,但没人会为一个「能接 Jellyfin 的音乐播放器」投钱。所以这类项目几乎注定是开源的、个人维护的。

如果你有一台 Android TV(或者电视盒子),NAS 里存着音乐,可以试试装一个。项目地址是 GitHub 上的开源项目,搜 NASMusicTV 就能找到。

构建也挺简单:

git clone https://github.com/你的地址/NASMusicTV
cd NASMusicTV
# 设置 Java 17(用 Android Studio 自带的 JDK)
export JAVA_HOME="C:\Program Files\Android\Android Studio\jbr"
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

当然,如果你不想自己构建,也可以在 Release 页面下载预编译的 APK。

安装后,填上你的 Jellyfin 或 Navidrome 服务器地址,连接,然后选一首歌,按播放。

看看电视上那句歌词逐字亮起的样子。

这可能就是你一直在找的体验。


八、GitHub 链接与推荐

项目完全开源,遵循 GPL v3 协议。如果你:

  • 拥有一台 Android TV 或电视盒子
  • NAS 里存着音乐,想在电视上好好欣赏
  • 对技术实现感兴趣,想学习 Jetpack Compose for TV + ExoPlayer 的实战写法
  • 觉得某个功能不够好用,想自己改

可以在这里找到源码:

GitHub:github.com/hxzhang2000/

顺手点个 Star 不亏——既是对开发者的鼓励,也能让更多人发现这个项目。

如果你遇到 bug 或者有新功能的想法,直接提 Issue 就行。对于想参与开发的同学,项目的 CHANGELOG 记录了每一个版本的改动细节(430 行,从 v1.0.0 到 v2.6.2),docs/ 目录下有完整的技术架构文档,连回归测试用例都写了 248 条——这大概就是独立开发者能给的最大诚意了。

最后,无论你是 NAS 玩家、音乐爱好者、还是 Android 开发者——希望这个项目对你有用。


与这个结合食用更佳:

AudioFileManager 音乐整理工具


这篇文章介绍的是 NASMusicTV v2.6.2 版本的功能。项目遵循 GPL v3 开源协议。

编辑于 2026-07-17 · 著作权归作者所有
相关文章
吊打 IDM?!高中生开发,强大的下载神器!直推旗舰大耳,五千档的便携设备有么?有的兄弟 - 旷世之声 SIGMA PRO260522安卓盒子app懒人合集最新一更2026年8月最新HiFi有线耳机选购指南 | 高性价比hifi耳机横评,100~1500元价位有线HiFi机型全覆盖:森海塞尔/西圣/原道/铁三角等热门品牌机型全解析,百元入门到千元旗舰哪些值得买?ShizuCallRecorder,首款基于 Shizuku 的开源免 Root 双向通话录音神器一百多就堪用的开放式蓝牙耳机 - 斌雀 岚开放式耳机天花板?从"能听"到"好听"再到"恰到好处",韶音OpenFit Pro的进化之路Windows播放器天花板!PotPlayer 保姆级优化 + 超全快捷键指南玻璃振膜的游戏耳塞?有耳麦才只要二百多? - 唐族 薛涛韶音OpenDots 2耳夹耳机使用体验:如何用「耳畔佩戴美学」将高质量音频融入日常生活?深度用完几款AI录音卡后,讯飞让我理解什么叫真轻薄生产力2026年耳夹式骨传导耳机怎么选?实测骨聆T90小飞豆、声阔AeroClip A3388和莅莱Ring12 Premier三款热门耳夹耳机,谁更适合你?索尼WF1000XM6降噪豆测评谁说封闭式耳机没声场?三千价位段的封闭式大耳选择困难症,被SoundMAGIC HP1000 Pro治好了!2026年高性价比真无线蓝牙耳机推荐(6月更新)百元价位能安心听歌DE大厂新品推荐?便携解码耳放新标杆!聊聊J.C Acoustics UDP-M3的试听体验13款视频播放器,包括PotPlayer、KMP、VLC Media、MPC-HC、SMPlayer、GOM、Splash、GridPlayer、nPlayer、Kodi、MX Player、IINAFosi弗西Merak CD播放器体验测评:复古情怀下的现代HiFi利器为什么越来越多人用通勤时间听播客?聊聊我从「随便听听」到认真选耳机的全过程,附韶音OpenDots 2耳夹式耳机测评体验