Astra TV 给电视做一个简单的 IPTV 播放器

在此前的文章湖北电信 IPTV 机顶盒与局域网组播共存中,笔者介绍了如何保留原装机顶盒,同时把 IPTV 直播送进家庭局域网。通过 rtp2httpd 将组播转换为 HTTP 单播以后,电视、电脑和手机就可以使用普通播放器收看这些频道。

网络打通之后,电视这一端还需要一个顺手的播放器。

最初笔者使用 ProgTV 播放直播。它的功能和设置很丰富,对专业用户很有用,不过放到家里日常看电视的场景中,逐渐遇到了几个让人不太舒服的问题:

  1. 设置太多,家人用不上,反而增加了操作负担。 家里主要就是选台和看直播,大部分专业选项配置一次之后就不会再碰。对于不熟悉这些设置的家人来说,菜单越多,越容易不知道该选什么,误操作后也不容易恢复。
  2. 免费版有额外的启动延时。 笔者使用的版本会在启动时显示免费版延时提示。每次只是想看一会儿电视,却要先等这段限制结束,确实不太爽。
  3. 冷启动本身也偏慢。 在出现免费版延时提示之前,电视上就已经有一段蓝屏加载。笔者使用的 2.93.8 版本包含 .NET/Mono 运行时,运行时初始化也是很大的性能开销,再叠上免费版的额外等待,就很糟心了。

家里的需求其实很简单:打开应用后恢复上次看的频道,按遥控器上下键换台,展开列表时能知道现在正在播什么,需要时可以修改直播源。笔者因此想自己做一个播放器,把常用操作保留下来,减少需要家人理解的设置,也去掉额外插入的启动等待。

于是有了 Astra TV:一个使用 Kotlin 编写的 Android / Android TV IPTV 播放器。桌面入口显示为「电视直播」,方便家人找到。没有广告、账号或人为启动倒计时,直播源和节目单由用户自己配置。

项目地址:astra-sec/astra-tv

Astra TV 的首次配置、全屏播放、频道列表和设置界面

先把遥控器操作做好

在手机上,一个按钮只要能点到,通常就能用。在电视上,用户坐在几米外,手里只有方向键和确定键,按钮是否容易找到、焦点能否顺着预期移动,往往比按钮的数量更重要。

Astra TV 的全屏操作比较直接:上键增加频道,下键减少频道,按直播源中的列表顺序循环。确定键、左键和菜单键都可以展开菜单;有数字键的遥控器,也可以直接输入来源里的频道号选台。

频道列表打开以后,左侧只保留「频道」和「设置」两个入口。焦点移动到「设置」时,右侧直接换成设置选项;回到「频道」,右侧就恢复频道列表。这样,即使列表很长,也不需要一直滚到末尾才能找到设置,更不需要再打开一个额外的设置弹窗。

左、右键在侧栏和内容之间移动,返回键关闭菜单;全屏状态连续按两次返回即可退出。

设置里放的是日常确实会用到的选项:视频解码器、画面比例、刷新频道列表,以及直播源和节目单地址。应用会保存这些选择和上次播放的频道,下次打开时继续使用。

换台后,底栏会显示频道名称、当前节目和播放状态,播放恢复后计时隐藏。网络缓冲和自动重连也使用同一套状态显示。

直播源和节目单分开配置

首次打开 Astra TV,需要填写一个 HTTP 或 HTTPS 的 M3U 频道列表地址。直播源默认留空,安装包不附带频道,也没有绑定某个地区或运营商。此前家庭 IPTV 的场景只是开发起点,播放器本身可以使用其他符合格式的公开列表。

这里有两个不同的地址:

  • 直播源地址:M3U 列表,提供频道名称、播放地址,以及可能的频道号和请求头。
  • 节目单地址:XMLTV 文件,提供各频道的节目名称和播出时间,支持 XML 和 XML.gz。

节目单可以留空关闭,不影响直播播放。当前代码预填的是 51zmt 的 http://epg.51zmt.top:8000/e.xml.gz,也可以换成其他 XMLTV 来源。应用始终只读取用户设置的一个节目单源,便于确认显示的数据来自哪里。

地址保存以后,频道列表和节目单都会缓存到本地。后续启动可以先恢复已经保存的列表,再在后台更新;最后一次观看的频道也会一起恢复。网络暂时不可用时,有效缓存仍然可以帮助保留界面和节目资料,不过实际视频能否继续播放仍取决于直播连接。

节目单目前主要回答一个问题:这个频道现在正在播什么? 它显示在频道列表的第二行和换台底栏中,附带北京时间的播出时段,并在节目边界更新。

Android 13 模拟器中的频道列表与当前节目显示

上图是一次节目单配置测试中的模拟器截图,CGTN 纪录频道显示了「16:00–16:30 人文地理」。节目名称来自配置的 XMLTV,没有写死在应用里。

频道匹配会使用 M3U 中的 tvg-id、tvg-name 和频道名称,并做一些保守的别名处理。直播列表和节目单并不一定使用同一套命名,4K、国际版本或其他变体也不能随意合并。匹配不到或排期已经过期时,界面会显示「暂无节目单」,而不是猜一个节目填进去。

它还没有完整的时间轴节目表、回看或录制功能。现在优先做的是把正在播放的节目显示清楚,同时让节目单下载、解析和更新都留在后台,不打断选台操作。

视频解码和音频兼容

Astra TV 使用 AndroidX Media3 播放视频,当前支持 HTTP/HTTPS MPEG-TS 和 HLS。视频通过 Android 的 MediaCodec 解码,优先使用设备提供的硬件解码器,也允许在设置里选择固件报告的其他视频解码器。

这里处理的是「解码」:把收到的压缩视频还原成画面。应用没有重新编码或转码直播,也没有把 FFmpeg 当作软件视频播放器。HEVC、4K、10-bit 等内容能否播放,仍然取决于电视或机顶盒的解码能力。

FFmpeg 主要用来补足音频兼容性。有些 IPTV 使用 MP2 音频,设备的系统音频解码器未必支持,就可能出现有画面却没有声音的情况。因此,播放器保留了一个精简的 FFmpeg 音频扩展,用于 MP2、AAC、AC3、EAC3 等格式的兼容处理。

目前包含这些功能的 APK 约 5.2 MiB,原生扩展集中在需要补足的音频格式上。

最近加入的 HLS 支持则扩大了可用来源的范围。互联网上常见的 .m3u8 频道往往包含主清单、不同码率的媒体清单和连续的视频分片,需要按 HLS 的方式处理。频道指定的 User-Agent 和 Referer 也会用于这些请求。

使用时需要区分:M3U 频道列表和单频道 HLS 清单是两件事。 一个列表可以包含许多 .m3u8 播放地址;某份频道列表自身也可以叫 .m3u8。扩展名不能单独决定它属于哪一种,直播源设置需要的是频道列表。

当前 HLS 识别依据播放地址路径中的 .m3u8 扩展名。没有这个扩展名的 HLS,以及 DASH、RTSP、直接 UDP/RTP 等来源,暂时没有专用播放路径。家庭 IPTV 若使用组播,可以继续在网络侧通过 rtp2httpd 转成 HTTP,再交给 Astra TV 播放。

安装和使用

写本文时,Astra TV 的版本是 0.4.2,最低要求 Android 5.0,也就是 API 21;当前 APK 包含 ARMv7 和 ARM64 的原生音频库。这个最低版本是构建要求,实际使用还需要结合设备固件和解码能力判断。

项目源码已经放在 GitHub,可以用 Android Studio 打开,也可以通过命令行构建。需要准备 JDK 17 或 21、Android SDK platform 35 和对应的 build-tools:

1
2
3
git clone --recurse-submodules https://github.com/astra-sec/astra-tv.git
cd astra-tv
./gradlew :app:assembleRelease

生成的 APK 位于 app/build/outputs/apk/release/app-release.apk,可以通过设备允许的侧载方式安装。普通 APK 构建可以使用仓库中已有的原生音频库;重新编译 FFmpeg 的步骤和参数另见项目文档。

安装后,填写自己的 M3U 地址和可选的 XMLTV 地址,保存即可开始加载频道。

应用代码使用 Apache 2.0 许可,FFmpeg 音频部分遵循 LGPL 2.1 or later。对应的 FFmpeg 源码通过固定版本子模块提供,原生库构建脚本也保留在仓库中。