文 / Jeff Gong, Sahil Dhanju, Chih-Chiang Lu, Yueshi Shen
译 / 王鸿蒙
编者按:超过220万创作者在Twitch发布海量的视频,这对实时转码业务造成了巨大压力,Twitch团队通过优化多线程的转码服务以及Intel QuickSync的支持,实现了比FFmepg性能提升65%,并降低80%总体拥有成本。Twitch团队通过博客介绍了这一实现,LiveVideoStack对本文进行了摘译,点击『阅读原文』访问英文博客。同时,Yueshi Shen将在12月8-9日的ArchSummit 2017北京大会上详细介绍实现过程。
背景介绍
Twitch是全球领先的视频游戏、电子竞技和其他新兴创意内容的流媒体直播平台。每个月,超过220万独创内容创作者在我们的网站上直播或上传视频。高峰期,Twitch同时处理成千上万的并发直播视频流,并将其传送给世界各地的观众。
图1描述了我们的直播视频CDN架构,它为全球提供数以万计的并发直播流。
图一
与许多其他实时流服务一样,Twitch接收直播者通过RTMP上传的实时消息流。RTMP是一种用于在互联网上传输视频流和音频流的协议,主要用于点对点通信。为了将我们的直播内容触达无数观众,Twitch使用HTTP实时流媒体协议(HLS),HLS是一种基于HTTP的流媒体通信协议,现在大多数视频网站都使用该技术。
在实时流处理流水线内,转码模块负责将输入的RTMP流转换为具有多个版本(如1080p、720p等)的HLS格式。这些版本具有不同的码率,使得具有不同下载带宽的观众能够以尽可能最好的质量来获取实时视频流。图2描述了我们的实时视频CDN中的转码模块的输入和输出。
图2: 转码模块的输入和输出
在这篇文章中,我们将讨论:
- FFmpeg如何满足大部分实时转码要求
- FFmpeg无法提供哪些功能
- Twitch为什么建立自己的内部转码器软件栈
直接使用FFmpeg
FFmpeg是一个流行的开源软件项目,旨在记录、处理和流式传输视频和音频。它被云编码服务广泛部署,以用于文件转码,也可用于实时流转封装和转码。
假设我们正在使用RTMP协议以6mbps和1080p60(1920×1080,每秒60帧的帧速率)接收使用最广泛的H.264视频压缩标准文件。我们想要生成4个HLS版本:
- 1080p60 HLS / H.264
- 720p60 HLS / H.264
- 720p30 HLS / H.264
- 480p30 HLS / H.264
一个解决方案是运行4个独立的FFmpeg实例,每个实例都处理一个版本。在这里,我们将所有即时解码刷新(IDR)的时间间隔设置为2秒,并关闭场景变化检测,从而满足HLS标准所要求的所有版本输出的HLS片段完全时间对齐的要求。
FFmpeg 1-in-1-out示例命令:
代码语言:javascript复制ffmpeg -i <input file or RTMP stream> -c:v libx264 -x264opts keyint=<IDR interval>:no-scenecut -s <resolution> -r <fps> -b:v <target bitrate> -profile:v <profile> -c:a aac -sws_flags <scaler algorithm> -hls_list_size <number of playlist entries> <output file or playlist>.m3u8
注意:
- “<>”中的所有参数都来自用户的输入。其中一些在下面会更详细地描述
- c:v用来指定要使用的视频编解码器,我们的例子中使用的是libx264
- x264opts用来表示libx264的选项。在这里,IDR间隔应该是你想要的FPS的2倍,所以720p60会产生一个120的IDR间隔,而720p30需要一个60的IDR间隔。No-scenecut用来禁用场景变化检测
- s用来指定视频大小。可以是“宽x高”的形式或尺寸缩写的名称
- r用来指定FPS
- b:v用来指定目标视频比特率,当有带宽限制或要求时,该功能非常有用;另外,b:a用于音频
- profile是指H.264的配置文件
- sws_flags决定应该使用哪种缩放算法
- hls_list_size用于确定播放列表中的最大片段数(例如,等于6时表示实时流传输,将其设置为0则表示具有所有片段的播放列表)。段持续时间(可选的hls_timeflag)将与IDR间隔相同,在我们的例子中是2秒。
由于H.264是有损压缩标准,转码将不可避免地导致视频质量下降。而且,编码在计算上是非常“昂贵”的过程,特别是对于高分辨率和高帧速率的视频。考虑到这两个约束条件,相对理想的情况是对源RTMP中的最原始版本进行复合而不是转码,以节省计算能力并保持视频质量。
在上面的例子中,如果我们想要将输入的1080p60 RTMP源码复合成HLS,我们实际上可以使用上面的命令,而不用指定大小或目标FPS,以及编解码器的副本(避免对源码进行解码和重新编码):
代码语言:javascript复制ffmpeg -i <input file or RTMP stream> -c:v copy -c:a copy -hls_list_size <number of playlist entries> <output file or playlist>.m3u8
转封装源码流是一种有效的技术,但可能会导致输出HLS失去规范兼容性,从而在某些设备上无法正常播放。 我们将在下一节阐释该问题的性质及其影响。
另一方面,FFmpeg具有接收1个输入和产生N个输出的功能,我们用下面的FFmpeg命令来演示。
FFmpeg 1-in-N-out示例命令(使用主配置文件、x264快速预置和双线性缩放算法):
代码语言:javascript复制ffmpeg -i <input file or RTMP stream>
-c:v libx264 -x264opts keyint=120:no-scenecut -s 1920x1080 -r 60 -b:v <target bitrate> -profile:v main -preset veryfast -c:a libfdk_aac -sws_flags bilinear -hls_list_size <number of playlist entries> <output file or playlist>.m3u8
-c:v libx264 -x264opts keyint=120:no-scenecut -s 1280x720 -r 60 -b:v <target bitrate> -profile:v main -preset veryfast -c:a libfdk_aac -sws_flags bilinear -hls_list_size <number of playlist entries> <output file or playlist>.m3u8
-c:v libx264 -x264opts keyint=60:no-scenecut -s 1280x720 -r 30 -b:v <target bitrate> -profile:v main -preset veryfast -c:a libfdk_aac -sws_flags bilinear -hls_list_size <number of playlist entries> <output file or playlist>.m3u8
-c:v libx264 -x264opts keyint=60:no-scenecut -s 852x480 -r 30 -b:v <target bitrate> -profile:v main -preset veryfast -c:a libfdk_aac -sws_flags bilinear -hls_list_size <number of playlist entries> <output file or playlist>.m3u8
如果我们想复合最原始版本,而转码其余的版本,我们可以用先前指定的编解码器副本替换第一个输出配置:
代码语言:javascript复制
注意:
- 使用上面的命令,我们可以从一个输入文件转码出多个版本。 每个“”表示新的一行,我们可以指定一个不同的标志组合,以及一个唯一的输出名称。 每个命令都是相对独立的,可以使用任何其他的标志组合。
- 这里的每个命令的主要区别在于s和rflags,本文前面已经解释过了。
- 在单个FFmpeg实例中运行以下多个转码的一个替代方法是运行多个实例,即并行地为每个期望的输出运行一个实例。 1-in-N-out的FFmpeg是一个消耗计算资源较低的过程,我们将在稍后做出解释。
几个技术问题
上一节演示了如何使用FFmpeg为直播流生成HLS。虽然很有用,但是一些技术方面的问题使FFmpeg成为一个不太理想的解决方案。
复合和转码
图3: HLS版本和片段,对齐跨多个版本的片段
在HLS中,一个版本由一系列片段组成,每个片段以一个IDR帧开始。HLS规范要求版本的相应片段的IDR帧必须对齐,以便它们具有相同的演示时间戳(PTS)。只有这样,当观众的网络状况发生变化时,HLS自适应比特率(ABR)播放器才能在这些版本之间无缝地切换(见图3)。
图4:复合版本和转码版本的片段之间的不对齐
如果我们对源代码和其他版本进行转码,我们将得到完美的时间对齐的HLS片段,因为我们强制FFmpeg精确地每2秒编码一次IDR。 但是,我们无法控制源RTMP比特流中的IDR间隔,这完全由播放软件的配置决定。如果我们将源代码进行复合,那么复合的和转码的版本的片段就不能保证对齐(见图4)。这种不对齐可能会导致播放问题。例如,我们注意到,Chromecast在收到含有未对齐片段的HLS流时会不断显示播放暂停。
对于具有可变IDR间隔的源RTMP流,我们希望输出的HLS看起来如图5那样对齐:
图5:复合版本和转码版本的对齐分段
但是,在1-in-1-out 和1-in-N-out的FFmpeg实例中,与N个输出版本相对应的N个编码器是独立的。如上所述,除非所有的版本都被转码(即最原始的版本也将被转码而非复合),否则结果IDR不会被对齐(见图4)。
软件性能
如图2中所讨论的,我们的RTMP-HLS转码器接收1个流的输入并产生N个流的输出(N = HLS版本的数量,例如,图5中的N = 4)。实现这种输出的最简单方法是创建N个独立的1-in-1-out转码器,每个转码器产生1个输出流。上面介绍的FFmpeg解决方案使用了这个方法,生成了N个FFmpeg的实例。
在1-in-1-out转码器中有3个组件,即解码器,缩放器和编码器(见图6)。因此,对于N个FFmpeg实例,我们将对应有N个解码器,N个缩放器和N个编码器。
图6:剖析视频转码器
由于N个解码器都是相同的,所以理想情况下,转码器应消除冗余的N-1个解码器,并将来自这个唯一解码器的解码图像发送到N个下游缩放器和编码器(见图7)。
图7:仅有1个视频解码器的1-in-N-out视频转码器
上面我们谈到了解码器冗余。现在,我们来看看四个版本的例子。
- 1080p60 HLS/H.264
- 720p60 HLS/H.264
- 720p30 HLS/H.264
- 480p30 HLS/H.264
这个例子中的两个转码版本720p60和720p30可以共享一个缩放器。实验证明,缩放在转码过程中是一个计算量非常大的步骤。 避免不必要的重复缩放过程可以显著优化我们的转码器的性能。 图8描绘了整合720p60和720p30版本的缩放器的线程模型。
图8:共享缩放器的720p60和720p30的线程模型
除了解码器和缩放器共享之外,更重要的特性是使用多线程。由于编码器和缩放器在计算上都非常昂贵,因此利用现代多核CPU架构同时处理多个版本是非常重要的。从我们的实验中,我们发现多线程对于实现更高密度的工作非常有用,对于某些特定的应用程序(如4K)也非常重要。
自定义功能
FFmpeg是一种多功能的视频处理软件,支持标准的ABR转码工作流的各种视频/音频格式。但是,它不能处理Twitch操作中特有的一些技术要求。例如,
1)帧率下采样器
典型的传入比特流具有60fps(每秒帧数),我们将它们转码为30fps,以获得较低的比特率版本(例如,720p30,480p30等)。另一方面,由于Twitch是一个全球平台,我们经常收到50fps的流量,这些流量大部分来自PAL国家。在这种情况下,较低比特率的版本应该下采样到25fps,而不是30fps。
简单地删除每一个第二帧在这时并不是一个很好的解决方案。对于两种不同类型的传入比特流,我们的下采样器需要有不同的表现。一种具有低于60fps的固定帧率,另一种具有不规则帧丢失,平均帧率低于60fps。
2)元数据插入
某些信息需要被插入到HLS比特流中以增强用户体验。通过构建我们自己的转码器和播放器,Twitch可以控制完整的端到端摄取 - 转码 - CDN播放流水线。这允许我们将专有的元数据结构插入到转码器输出中,最终由我们的播放器进行解析,并用于产生Twitch特有的效果。
LiveVideoStack招募全职技术编辑和社区编辑
LiveVideoStack是专注在音视频、多媒体开发的技术社区,通过传播最新技术探索与应用实践,帮助技术人员成长,解决企业应用场景中的技术难题。如果你有意为音视频、多媒体开发领域发展做出贡献,欢迎成为LiveVideoStack社区编辑的一员。你可以翻译、投稿、采访、提供内容线索等。