构建自己的流媒体电视台

导航至

最近一位同事提到看到一位 YouTuber 在家中搭建了自己的有线电视频道。

这让我觉得是个不错的想法:我有时想让电视在背景播放(背景噪音有助于狗狗安静),但经常难以从众多选项中挑选(其中包括我们大多数 DVD 的复制品)。

直播电视和 Prime Video 并不是真正的选择:目的是有一些背景娱乐,而不是被那些我永远不会购买的产品的烦人广告所打扰。

拥有一个无广告且只播放我喜欢的内容的频道,会让选择变得容易得多;我通常并不打算观看它。

我们家里有各种各样的设备,所以我希望我的实现使用常见的协议(HLS 和/或 RTMP),并且最好能在我的 Kubernetes 集群上运行。

在这篇文章中,我将讨论我构建的容器镜像的一些更有趣的方面,该镜像用于使用我的 DVD 收藏运行自己的流媒体电视台。

概述

该系统只是一个 docker 容器,它会随机从本地库中选择媒体,并通过 RTMP 和 HLS 使其可供播放器使用。

就像一个电视频道一样,它播放自己的日程。

不可避免地,我有些过于兴奋,所以功能集还包括

  • 媒体允许和阻止列表:限制频道可以流式传输的内容
  • 设置广播窗口的能力:限制频道可以流式传输的时间
  • 动态广播触发:如果没有人实际观看,则减少计算的使用
  • 将播放历史和其他统计信息写入 InfluxDB
  • 一个 API 端点,用于跳转到下一集(以防播放问题或“我看过这个”)

我选择使用InfluxDB而不是(例如)编写文本日志,因为将数据写入时序数据库可以更轻松地探索和分析播放历史。它还允许轻松地将历史与[性能和用法统计](例如,以确定某些电视剧是否会导致帧率降低)相关联。

InfluxDB的其他功能(例如支持数据保留期)也允许轻松管理数据的生命周期(例如,通过在数据不再有用时将其过期)。

运行

通过docker调用很简单

不可避免地,在Kubernetes上运行的YAML有点冗长,但就K8s配置而言仍然相当简单。

YAMAL

RTMP & HLS

由于之前的一些工作,我在视频传输方面积累了大量的经验,包括RTMP。

事实上,在之前的一个职位中,我拥有并运营了一个nginx-rtmp的私有分支,为了满足极端多样化的客户需求,必须对其进行大量定制(换句话说,我对它做了一些无法言表的事情)。

这种经验是相关的,因为拥有、运营和支持自定义RTMP应用程序(尤其是在CDN规模上)带来的一个好处是,你只会想这样做一次(如果那样的话)。

因此,尽管nginx-rtmp是这个构建的核心,但我一点也没有定制它,而是围绕它构建了工具。

虽然我以前构建过HLS工具,但这次没有这个需求:nginx-rtmp已经内置了HLS支持(它也支持DASH,但我目前没有使用它)。

视频处理

该项目涉及读取任意视频文件,因此,显然,我选择了视频转换的瑞士军刀:FFmpeg

使用ffmpeg将文件流式传输到RTMP服务器是一个已经解决的问题。

image 2这个命令中的关键标志是-re,它指示ffmpeg以文件的原生帧率读取文件,确保文件以实时方式传输,而不是一次性传输。

发布脚本

我用BASH编写了自动化发布。

在RTMP堆栈的工作中,我有位同事偶尔会开玩笑地在shell脚本上留下代码审查评论:“行数表明这应该有一个.py扩展名。”

我想发布脚本也会吸引类似的审查,而且……他是对的。当我意识到我需要迭代文件中的行并对其执行类似于'|'.join()的操作时,我开始后悔选择BASH。

但我已经投入了……也许我会在某个时候重写它。

视频选择

发布脚本会随机选择一个系列和一集,并通过从阻止列表构建的正则表达式对其进行过滤。

过滤器在两个阶段都应用,这样我就可以包含针对剧集以及系列的过滤器。

例如,如果我想确保不包含特别节目,我可能会过滤掉包含字符串 s00e 的任何文件。

动态发布触发器

容器第一次构建相当简单。

  • 启动 nginx-rtmp
  • 循环,将随机剧集发布到 nginx-rtmp

结果是始终在线的IPTV频道。

但是……它始终是开着的。即使我不在观看,ffmpeg 也会尽职尽责地忙碌,占用宝贵的核心并消耗电力。

image 3

因此,我决定修改它,使得发布脚本只有在有玩家连接时才启动新的剧集。[链接](https://projects.bentasker.co.uk/gils_projects/issue/project-management-only/home-tv-station/11.html)。

nginx-rtmp 模块有一系列事件钩子,包括 play 事件。每个钩子都会向任意端点发送一个简单的HTTP请求。

所以,通过一行配置

Nginx 将播放请求的详细信息发送到目的地

image 4

我[组合](https://projects.bentasker.co.uk/gils_projects/issue/project-management-only/home-tv-station/11.html#comment7570)了一个小型控制服务器(用Python编写——我不想重复入口点时的错误),允许实现一个有些简陋但功能性的流程

  • 玩家连接到 nginx-rtmp
  • nginx-rtmpplay 事件发送到控制服务器
  • 控制服务器将 play(以及玩家数量)写入控制文件
  • 发布脚本检测到状态变化并启动 ffmpeg
  • 播放开始

如果玩家稍后断开连接,也有类似的流程

  • 玩家断开连接
  • nginx-rtmpplay_done 事件发送到控制服务器
  • 控制服务器从玩家数量中减去1
  • 如果玩家数量小于1,则写入 stop 状态
  • ffmpeg 继续当前流,但在状态为 stop 的情况下不会启动下一流

我决定不在断开连接时停止 ffmpeg,因为我想再次连接(可能是换房间的结果)。

在日志中,它看起来像这样

技术上,这个过程延长了视频启动时间,但为了确保流畅播放,RTMP播放器通常会先坐下来填充缓冲区,所以额外的延迟并不那么明显。

引入这一点带来了几个好处,包括

  • CPU 和电力不会被浪费在处理没有人在看的东西上
  • 我不会错过剧集的开始,因为系统不会在有人观看之前开始流

唯一的真正缺点是它只适用于RTMP流。HLS是在标准HTTP连接上提供的,所以没有简单的等效方法(当然有绕过它的方法,但它们都有我不想做的权衡)。

广播窗口

老实说,实现这一点更多的是关于怀旧而不是效率。

广播窗口表示系统应该在设定的小时内广播视频。

在这些时间之外,而不是启动新剧集,玩家将收到旧的BBC测试卡。

image 5

不过,它有一点与原始版本不同,即卡片是以静音流式传输的。怀旧只走这么远,我并不真的喜欢重温被蜂鸣声吵醒的经历。

避免剧集间的冻结

系统设计意味着 ffmpeg 必须消费一系列任意视频文件。由于在多年的时间里被抓取,它们之间有很大的差异,包括

  • 视频和音频编解码器
  • 帧率
  • 分辨率
  • 像素格式(较少见,但仍然存在)

nginx-rtmp基本上只是将输入管道中的内容转发出去,因此当一集结束时,下游播放器可能会接收到与前几集完全不同的帧。

一些播放器(如ffplay)可以很好地处理这种情况。但是,大多数播放器在响应时会冻结。

为了避免这些意外的变化,我调整了ffmpeg的调用,使其输出标准化

为了确保分辨率一致,使用了缩放过滤器,以720p为目标。如果输入文件的分辨率较低,则使用信封式缩放。

经过这次更改后,集与集之间的转换变得更加可靠。

我还实现了一个可选机制,可以在集变换时将播放器从流中(再回到)重定向。然而,Kodi的简单IPTV播放器插件似乎对此不太喜欢。

播放历史和统计

多年来,我们积累了大量的剧集,有时我甚至可能认不出我们在看什么(或者也许我真的变老了)。

最初,我实现了一个小的API端点,可以调用以检查当前播放的内容。

但如果我们完全沉浸在其中并忘记检查直到结束,那我们就永远不会知道那部精彩的剧集了!

因此,我决定让容器将播放历史写入InfluxDB。

image 6

结论

这本质上只是一个有趣的项目,可以让我玩一玩,让自己几天不用烦恼。

但是,它也非常好地满足了其目标使用案例,即使它并不总是成功地作为背景运行:那天晚上我坐下,打开它,迎接我的是一集Bucky O’Hare

最好的是,没有广告间隔。诚然,我考虑过添加一个,这样我就可以起诉Musk“扣留数十亿美元的广告收入”(这是它现在的运作方式,对吧?)

流媒体的需求因流媒体剧集的不同而略有不同:处理高分辨率视频显然比低分辨率视频需要更多的资源。我目前将pod限制在2核,这对大多数媒体来说都很合适。

我不会添加那么多播放器,但大部分计算开销在初始发布上——实际的流传输非常便宜(我可能会在网络拥塞之前遇到问题,而不是CPU成为问题)。