文 / 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北京大会上详细介绍实现过程。
FFmpeg的1-in-N-out流水线。为什么它无法处理前面讨论的技术问题?
FFmpeg如何以编程方式处理需要单个输入来生成多个转码和(或)转封装输出的实例? 我们可以通过直接剖析FFmpeg最新3.3版的源代码,来了解其线程模型和转码流水线。
在顶层ffmpeg.c文件中,transcode()函数(第4544行)不断循环并重复调用transcode_step()函数(第4478行),直到其输入信息被完全处理,或用户中断执行为止。Transcode_step()函数封装了主要的流水线,并在许多其他即时步骤之间编排诸如文件I / O、过滤、解码和编码等动作。
在初始设置阶段,init_input_threads()(第4020行)函数被调用,并将根据输入文件的数量,产生一些新的线程来处理这些输入。
代码语言:javascript复制if (nb_input_files == 1) {
return 0;
}
for (i = 0; i < nb_input_files; i ) {
...
ret = av_thread_message_queue_alloc(&f->in_thread_queue, f->thread_queue_size, sizeof(AVPacket)); // line 4033
}
在第4033行中(如上所示),我们看到产生的线程数量完全由输入的数量决定。也就是说,这意味着FFmpeg将只使用一个线程来处理1-in-N-out的场景。
在get_input_packet()函数(第4055行)中,只有当输入文件的数量大于1时,才会调用多线程伴随函数get_input_packet_mt()(第4047行)。get_input_packet_mt()函数可以以非阻塞的方式从消息队列中读取输入帧。否则的话,我们需要使用av_read_frame()(第4072行)来每次读取并处理一个帧。
代码语言:javascript复制#if HAVE_PTHREADS
if (nb_input_files > 1) {
get_input_packet_mt(f, pkt);
}
#endif
return av_read_frame(f->ctx, pkt);
如果我们跟踪帧数据一直到流水线结束,我们发现它进入到process_input_packet()函数(行2591)中,该函数对帧数据进行解码并通过所有适用的过滤器进行处理。时间戳校准和字幕处理的工作也在这个函数中进行。最后,在函数返回之前,已解码的帧被复制到每个相关的输出流。
代码语言:javascript复制for (i = 0; pkt && i < nb_output_streams; i ) {
... // check constraints
do_streamcopy(ist, ost, pkt); // line 2756
}
最后,transcode_step()函数调用reap_filters()函数(第1424行)来循环遍历每个输出流。reap_filters()函数的for循环负责收集缓冲区中待处理的帧,并将这些帧进行解码,然后封装到一个输出文件中。
代码语言:javascript复制// reap_filters line 1423
for (i = 0; i < nb_output_streams; i ) { // loop through all output streams
... // initialize contexts and files
OutputStream *ost = output_streams[i];
AVFilterContext *filter = ost->filter->filter;
AVFrame filtered_frame = ost->filtered_frame;
while (1) { // process the video/audio frame for one output stream
... // frame is not already complete
ret = av_buffersink_get_frame_flags(filter, filtered_frame, …);
if (ret < 0) {
... // handle errors and logs
break;
}
switch (av_buffersink_get_type(filter)) {
case AVMEDIA_TYPE_VIDEO:
do_video_out(of, ost, filtered_frame, float_pts);
case AVMEDIA_TYPE_AUDIO:
do_audio_out(of, ost, filtered_frame);
}
...
}
通过跟踪这条流水线,我们知道这些帧是如何通过单个线程的上下文顺序进行处理的,从中我们能看到一些冗余。我们可以得出结论,既然1-in-N-out的转码流模型对我们来说是最有价值的,那么FFmpeg仅使用单线程来输出结果则可能并不理想。FFmpeg文档也建议我们在实际用例中,并行地启动多个FFmpeg实例或将更有意义。在这里,我们关键的一点认识是,既然此工具(FFmpeg)没有提供多线程功能,它就无法满足Twitch流媒体服务的严格需求,那么我们就无法随心所欲地使用它。
基准测试
TwitchTranscoder是我们为解决前面讨论的技术问题而开发的内部软件。它已被广泛运用于我们的生产中,每天24小时地处理数万个并发直播流。
为了确定TwitchTranscoder每天在转码任务上的表现是否会优于FFmpeg,我们进行了一系列基本的基准测试。在我们的测试中,我们对两个工具使用相同的Twitch直播流以及有相同预设、配置文件、比特率和其他标志的1080p60视频文件。每个视频源都被转码成我们通常使用的典型的720p60,720p30,480p30,360p30和160p30。
我们的假设是,FFmpeg对于输入文件的转码速度比TwitchTranscoder要慢,甚至可能无法跟上直播的速度。
图9,10和11中的结果比较了TwitchTranscoder与FFmpeg的执行时间。实验表明,即使在我们处理相同及更多(除了上面指定的栈之外,还提供仅音频转码,缩略图生成等等)任务的情况下,我们的转码器对于离线转码一直有绝对优势。
对于输出单个版本的720p60,FFmpeg稍快,这是因为TwitchTranscoder要处理如上所述的更多任务。当版本的数量增加时,TwitchTranscoder的多线程模型表现出更大的优势,这些优势帮助它超越了FFmpeg。观察Twitch完整的ABR梯度,与FFmpeg相比,TwitchTranscoder节省了65%的执行时间。
图9:TwitchTranscoder与FFmpeg转码时间比较,实验1
图10:TwitchTranscoder与FFmpeg转码时间比较,实验2
图11:TwitchTranscoder与FFmpeg转码时间比较,实验2
我们通过比较在出问题前,一台机器上最多能够运行多少个FFmpeg的并行实例来进行实时流转码测试。这里可能发生的问题包括帧丢失、视频伪影等。在我们的生产服务器中,我们能够支持多个通道同时进行转码,同时,更多的通道被转封装。不幸的是,运行多个FFmpeg实例会导致一系列影响转码输出的错误,并且需要更高的CPU利用率(请参见图12中的屏幕截图)。
图12:FFmpeg运行多个实例时的错误消息
结论
在本文中,我们将FFmpeg作为实时流RTMP- to-HLS的转码器进行了研究,并提供了有关如何操作该工具的信息。该解决方案部署起来很简单,但有一些技术问题值得注意,比如段错位、不必要的性能损失,以及缺乏支持我们产品功能的灵活性等。因此,我们实现了自己内部的转码器软件栈TwitchTranscoder,它运行在一个定制的线程模型中,并可以在一个进程中输出N个处理版本。
LiveVideoStack招募全职技术编辑和社区编辑
LiveVideoStack是专注在音视频、多媒体开发的技术社区,通过传播最新技术探索与应用实践,帮助技术人员成长,解决企业应用场景中的技术难题。如果你有意为音视频、多媒体开发领域发展做出贡献,欢迎成为LiveVideoStack社区编辑的一员。你可以翻译、投稿、采访、提供内容线索等。