构建自己的流媒体电视台
作者:Ben Tasker / 开发者
2024年10月08日
导航至
最近一位同事提到看到一位 YouTuber 在家中搭建了自己的有线电视频道。
这让我觉得是个不错的想法:我有时想让电视在背景播放(背景噪音有助于狗狗安静),但经常难以从众多选项中挑选(其中包括我们大多数 DVD 的复制品)。
直播电视和 Prime Video 并不是真正的选择:目的是有一些背景娱乐,而不是被那些我永远不会购买的产品的烦人广告所打扰。
拥有一个无广告且只播放我喜欢的内容的频道,会让选择变得容易得多;我通常并不打算观看它。
我们家里有各种各样的设备,所以我希望我的实现使用常见的协议(HLS 和/或 RTMP),并且最好能在我的 Kubernetes 集群上运行。
在这篇文章中,我将讨论我构建的容器镜像的一些更有趣的方面,该镜像用于使用我的 DVD 收藏运行自己的流媒体电视台。
概述
该系统只是一个 docker 容器,它会随机从本地库中选择媒体,并通过 RTMP 和 HLS 使其可供播放器使用。
就像一个电视频道一样,它播放自己的日程。
不可避免地,我有些过于兴奋,所以功能集还包括
- 媒体允许和阻止列表:限制频道可以流式传输的内容
- 设置广播窗口的能力:限制频道可以流式传输的时间
- 动态广播触发:如果没有人实际观看,则减少计算的使用
- 将播放历史和其他统计信息写入 InfluxDB
- 一个 API 端点,用于跳转到下一集(以防播放问题或“我看过这个”)
我选择使用InfluxDB而不是(例如)编写文本日志,因为将数据写入时序数据库可以更轻松地探索和分析播放历史。它还允许轻松地将历史与[性能和用法统计](例如,以确定某些电视剧是否会导致帧率降低)相关联。
InfluxDB的其他功能(例如支持数据保留期)也允许轻松管理数据的生命周期(例如,通过在数据不再有用时将其过期)。
运行
通过docker调用很简单
不可避免地,在Kubernetes上运行的YAML有点冗长,但就K8s配置而言仍然相当简单。
RTMP & HLS
由于之前的一些工作,我在视频传输方面积累了大量的经验,包括RTMP。
事实上,在之前的一个职位中,我拥有并运营了一个nginx-rtmp的私有分支,为了满足极端多样化的客户需求,必须对其进行大量定制(换句话说,我对它做了一些无法言表的事情)。
这种经验是相关的,因为拥有、运营和支持自定义RTMP应用程序(尤其是在CDN规模上)带来的一个好处是,你只会想这样做一次(如果那样的话)。
因此,尽管nginx-rtmp
是这个构建的核心,但我一点也没有定制它,而是围绕它构建了工具。
虽然我以前构建过HLS工具,但这次没有这个需求:nginx-rtmp
已经内置了HLS支持(它也支持DASH,但我目前没有使用它)。
视频处理
该项目涉及读取任意视频文件,因此,显然,我选择了视频转换的瑞士军刀:FFmpeg
使用ffmpeg
将文件流式传输到RTMP服务器是一个已经解决的问题。
这个命令中的关键标志是-re
,它指示ffmpeg
以文件的原生帧率读取文件,确保文件以实时方式传输,而不是一次性传输。
发布脚本
我用BASH编写了自动化发布。
在RTMP堆栈的工作中,我有位同事偶尔会开玩笑地在shell脚本上留下代码审查评论:“行数表明这应该有一个.py
扩展名。”
我想发布脚本也会吸引类似的审查,而且……他是对的。当我意识到我需要迭代文件中的行并对其执行类似于'|'.join()
的操作时,我开始后悔选择BASH。
但我已经投入了……也许我会在某个时候重写它。
视频选择
发布脚本会随机选择一个系列和一集,并通过从阻止列表构建的正则表达式对其进行过滤。
过滤器在两个阶段都应用,这样我就可以包含针对剧集以及系列的过滤器。
例如,如果我想确保不包含特别节目,我可能会过滤掉包含字符串 s00e
的任何文件。
动态发布触发器
容器第一次构建相当简单。
- 启动
nginx-rtmp
- 循环,将随机剧集发布到
nginx-rtmp
结果是始终在线的IPTV频道。
但是……它始终是开着的。即使我不在观看,ffmpeg
也会尽职尽责地忙碌,占用宝贵的核心并消耗电力。
因此,我决定修改它,使得发布脚本只有在有玩家连接时才启动新的剧集。[链接](https://projects.bentasker.co.uk/gils_projects/issue/project-management-only/home-tv-station/11.html)。
nginx-rtmp
模块有一系列事件钩子,包括 play
事件。每个钩子都会向任意端点发送一个简单的HTTP请求。
所以,通过一行配置
Nginx 将播放请求的详细信息发送到目的地
我[组合](https://projects.bentasker.co.uk/gils_projects/issue/project-management-only/home-tv-station/11.html#comment7570)了一个小型控制服务器(用Python编写——我不想重复入口点时的错误),允许实现一个有些简陋但功能性的流程
- 玩家连接到
nginx-rtmp
nginx-rtmp
将play
事件发送到控制服务器- 控制服务器将
play
(以及玩家数量)写入控制文件 - 发布脚本检测到状态变化并启动
ffmpeg
- 播放开始
如果玩家稍后断开连接,也有类似的流程
- 玩家断开连接
nginx-rtmp
将play_done
事件发送到控制服务器- 控制服务器从玩家数量中减去1
- 如果玩家数量小于1,则写入
stop
状态 ffmpeg
继续当前流,但在状态为stop
的情况下不会启动下一流
我决定不在断开连接时停止 ffmpeg
,因为我想再次连接(可能是换房间的结果)。
在日志中,它看起来像这样
技术上,这个过程延长了视频启动时间,但为了确保流畅播放,RTMP播放器通常会先坐下来填充缓冲区,所以额外的延迟并不那么明显。
引入这一点带来了几个好处,包括
- CPU 和电力不会被浪费在处理没有人在看的东西上
- 我不会错过剧集的开始,因为系统不会在有人观看之前开始流
唯一的真正缺点是它只适用于RTMP流。HLS是在标准HTTP连接上提供的,所以没有简单的等效方法(当然有绕过它的方法,但它们都有我不想做的权衡)。
广播窗口
老实说,实现这一点更多的是关于怀旧而不是效率。
广播窗口表示系统应该在设定的小时内广播视频。
在这些时间之外,而不是启动新剧集,玩家将收到旧的BBC测试卡。
不过,它有一点与原始版本不同,即卡片是以静音流式传输的。怀旧只走这么远,我并不真的喜欢重温被蜂鸣声吵醒的经历。
避免剧集间的冻结
系统设计意味着 ffmpeg
必须消费一系列任意视频文件。由于在多年的时间里被抓取,它们之间有很大的差异,包括
- 视频和音频编解码器
- 帧率
- 分辨率
- 像素格式(较少见,但仍然存在)
nginx-rtmp
基本上只是将输入管道中的内容转发出去,因此当一集结束时,下游播放器可能会接收到与前几集完全不同的帧。
一些播放器(如ffplay
)可以很好地处理这种情况。但是,大多数播放器在响应时会冻结。
为了避免这些意外的变化,我调整了ffmpeg
的调用,使其输出标准化
为了确保分辨率一致,使用了缩放过滤器,以720p为目标。如果输入文件的分辨率较低,则使用信封式缩放。
经过这次更改后,集与集之间的转换变得更加可靠。
我还实现了一个可选机制,可以在集变换时将播放器从流中(再回到)重定向。然而,Kodi的简单IPTV播放器插件似乎对此不太喜欢。
播放历史和统计
多年来,我们积累了大量的剧集,有时我甚至可能认不出我们在看什么(或者也许我真的变老了)。
最初,我实现了一个小的API端点,可以调用以检查当前播放的内容。
但如果我们完全沉浸在其中并忘记检查直到结束,那我们就永远不会知道那部精彩的剧集了!
因此,我决定让容器将播放历史写入InfluxDB。
结论
这本质上只是一个有趣的项目,可以让我玩一玩,让自己几天不用烦恼。
但是,它也非常好地满足了其目标使用案例,即使它并不总是成功地作为背景运行:那天晚上我坐下,打开它,迎接我的是一集Bucky O’Hare!
最好的是,没有广告间隔。诚然,我考虑过添加一个,这样我就可以起诉Musk“扣留数十亿美元的广告收入”(这是它现在的运作方式,对吧?)
流媒体的需求因流媒体剧集的不同而略有不同:处理高分辨率视频显然比低分辨率视频需要更多的资源。我目前将pod限制在2核,这对大多数媒体来说都很合适。
我不会添加那么多播放器,但大部分计算开销在初始发布上——实际的流传输非常便宜(我可能会在网络拥塞之前遇到问题,而不是CPU成为问题)。