引子
博主第一次收到光华之声是在小学的时候,在那遥远的 2013 年,当时光华之声的 9745 kHz 还没有被挂掉,每天夜里,伴随着民乐火龙和隔壁 NHK 的强力轰炸,博主只能凭借想象力,艰难地猜测节目在讲什么。
- 欢迎收听 … 我是黄萱
- … 两岸 … 大不同
- 在中国,没有 50 万以上 … 几乎免谈
- … 台湾好行征文活动 …
- … 我们的电邮地址是 lili329@ms45.hinet.net …
就这样,博主陆陆续续听到 2017 年,但是在 2017 年的某一天,9745 kHz 突然从短波频率上消失了,他走得如此地突然,没有通知,没有人谈论,博主一度怀疑是自己设备出了问题。
在此后的几年里,博主只在中波频率中收到过两三次光华之声的信号。作为一个反贼后浪,光华之声无疑是博主的启蒙导师,他用快、准、狠的手段,将一个祖国未来的小韭菜扼杀在了摇篮中,对于博主来说,实在是意义重大。
一个偶然的机会,去年在网际互联网冲浪时,发现了光华之声主持人陈彦的 Facebook 帐号,在打过招呼过后,博主和她聊了起来,重拾了曾经的回忆。
正值光华网站改版,博主也顺便向她提了建议,希望光华能够自建流媒体服务器,来让中国听友能够顺利地线上收听,陈彦也答应向博主会向电台主管转达建议。
前几日博主无意中打开光华之声网站,发现光华之声网站已经加上自家的直播了。但美中不足的是,直播流的 HLS 链接被加上了 Token,还被限制了有效期,这让博主很难受。
要是能拿到一个永久固定的,不需要 Token 就能用的 HLS 地址,那该多好啊。
分析
光华的直播网站位于 audio.voh.com.tw/KwongWah/m3u8.aspx,网站语言为 ASP.NET,HLS 直播地址是由后端分配好 Token 后,嵌入到 <source>
标签中的。
换句话说,用户请求 audio.voh.com.tw/KwongWah/m3u8.aspx 后,服务器会返回整个渲染好的 HTML 页面。由于 Token 构造不是在前端完成的,这就让用户没有了自行构造 Token 的机会。
正因为没有自行构造 Token 的机会,所以只能透过解析 HTML 的方式,来获取带有 Token 的 HLS 地址。
最后,博主决定用 Golang 的 Goquery 包来获取带 Token 的 HLS 地址。Goquery 类似 jQuery,它是 jQuery 的 Go 版本实现,使用它,可以很方便的对 HTML 进行处理,同时,鉴于 Golang 的 WORA 特性(Write Once Run Anywhere), 也极大方便部署。
在获取到 HLS 地址过后,博主打算调用 FFmpeg 自动做转发,并将 TS 分片输出到指定位置,由于光华之声的 HLS 流地址具有时效性,所以还需要计算出 HLS 地址的可用时长,然后在 Token 将要过期时获取新地址,重启 FFmpeg 进程,开始新一轮的转发。
需求
综上所述,需要实现如下功能。
- 透过 Goquery 解析网站,取到
<source>
标签中src
的属性,并在 HTTP 请求失败时重试,直到成功为止(一定程度上解决 GFW 干扰) - 解析获取到的 HLS 流的 URL 所带的参数,取到
expires
参数的值,并同当前时间比较求差值,得到 HLS 地址的有效时长 - 透过循环来调用 FFmpeg,在 HLS 流快要过期时获取新 URL,并重启 FFmpeg
- 若 FFmpeg 输出文件夹不存在,则自动创建一个
实现
保存数据
创建一个全局结构体,用于保存数据。
1 | var config mediaInfo |
获取 HLS 地址
从 <source>
标签中获取 src
的属性值,若失败,则始终重试。
1 | func getLink() { |
解析 URL 参数
从 URL 提取参数,取到过期时间。
例如 https://vohradiow-hichannel.cdn.hinet.net/live/RA000077/playlist.m3u8?token=JKAVpqhl4YsInEsqFkFI_g&expires=1652028980
,则取到过期时间为 1652028980(Unix 时间戳)。
1 | func urlPraser(myUrl string, urlParam string) int64 { |
计算可用时长
获取直播流可用时长,并提前一小时。
1 | func validTime(uTime int64) int64 { |
运行 FFmpeg
在成功获取相关资讯后,调用系统 FFmpeg,开始转发,并将 TS 分片输出到指定位置。
1 | // 运行 FFmpeg |
主函数
引入 flag 包,获取用户指定的命令行参数,这里设置 -p
和 -o
两个选项,分别对应 FFmpeg 程序路径和 FFmpeg 转发的流输出路径。
默认情况下,默认 FFmpeg 路径为 /usr/bin/ffmpeg
,FFmpeg 转发的流输出路径为 /www/khmusic
。
1 | func main() { |
HTTP 服务器
最后,还需要一个 HTTP 服务来向用户提供串流服务。博主这里使用 Python 自带的 http.server
模块来实现。
启动 Python 自带的 HTTP 服务非常简单,只需要一行命令即可,其中,/www
是 FFmpeg 转发的流输出路径,8080
是监听的 HTTP 端口,::
代表监听了所有地址(包含 IPv6)。
1 | python3 -m http.server -d /www 8080 --bind :: |
部署
博主已将仓库开源至 GitHub,欢迎 Star。
点击 README.md 中的部署按钮,可以直接部署到 Heroku 上。
博主部署了一个,用着还不错,配合 CloudFlare Workers + 自选 IP 应该会更爽。
- khmusic.herokuapp.com/khmusic/index.m3u8(暂未被屏蔽)
- khmusic.a1.workers.dev/khmusic/index.m3u8(2022 年 5 月 9 日,CloudFlare Workers 已被屏蔽)
如果是部署到自己的服务器上,则只需要将仓库拉取下来用 Docker 构建一下就行了,步骤也很简单。
1 | git clone https://github.com/bclswl0827/khmusic-forwarder |
部署应用时,如果服务器上已经运行有 HTTP 进程,可以直接将容器的 /www
目录挂载到既有的网站目录中,也可选择启动自带的 HTTP 服务,并开放对应端口。
例如直接部署应用到 80 端口,使用自带的 HTTP 服务,需要注意,指定运行自带 HTTP 服务的端口号和指定开放的需要保证相同。
1 | docker run -d \ |
又如利用既有的 HTTP 服务,将容器的 /www
目录挂载到既有的网站目录中(例如 /var/www/live
)。
1 | docker run -d \ |
后记
和光华的一位老听友聊天,才知道他以前已经反馈了很多次,希望光华能有自己的串流。没想到这么多年了才给安排上,想必光华的内部也是一堆鸽王吧。
同时也要感谢陈彦,在帮忙反馈听友建议之余,还帮博主联系到了已经离职的廖恒。