搭建FFmpeg处理音视频

基于Java+FFmpeg搭建一个处理音视频的处理中台。

主要的架构设计分为两块内容。

image.png

一个对外提供服务的音视频处理任务中台,承接来自业务方的所有音视频处理需求,核心逻辑包括

  • 将音视频处理任务落库
  • 将音视频任务发送至消息队列
  • 消费来自消息队列的消息,将音视频处理结果更新到数据库
  • 通过回调任务,将结果回调给各个业务方

一个承接所有音视频任务的处理集群,核心逻辑包括

  • 消费来自消息队列的消息
  • 通过FFmpeg进行处理
  • 将处理好的文件上传至OSS服务
  • 将处理结果发送至消息队列

音视频处理任务中台

中台的考虑很简单,能承受住业务方的所有流量即可。内部需要的角色也较为简单。

分布式JOB

用于处理音视频回调通知,所有提交过来的任务都必须携带回调地址,比如Http接口。

分布式MQ

作为中台与逻辑处理集群的通信方式,最开始为了简单快捷起见,选择了使用Redis(lpush + bpop)作为消息中间件来处理,当下来看是一个错误的决定。后来统一完成了Kafka。

为何不能是Redis作为消息中间件

1、生产方与消费方的处理速率根本不再一个量级,当消费方处理速率比生产方低时,消息会大量积压,进而形成了一个Big key
2、Redis有丢失的风险,所以生产方需要做好一个合适的二次投递机制
3、缺少MQ部分功能,比如可靠的重试机制

音视频任务处理集群

该服务主要基于Java + FFmpeg来实现,因为业务特性的问题,FFmpeg是极其吃性能,特别是CPU,所以机器的性能会更好。

此处设计之初的目标是能快速的复制,当业务量突然进来时能通过加机器来抗住,所以节点必须是无状态,其次为了保证节点的稳定性,我们也考虑了进程内任务的一些排队处理方案。

思路

简单的描述一下当前的一些程序设计以及考量,并非是绝对的正确,因为很多方案选择的时候是需要考虑时间成本、人力成本的,只能慢慢通过演进的方式找到最佳实践。

为何要放在后端

如果客户端可以做,一定要放在客户端做。因为后端如果来处理这种大规模的音视频内容,所需要的成本太高了。放在客户端做,不管是从体验还是成本都是更优的方案。(但是也要考虑一些性能不是很好地机子去处理,防止程序卡死)

有兴趣的可以在本地的机子上体验一下FFmpeg,简直就是CPU猛兽。

部署

毫无疑问,必须是Docker。将FFmpeg的打包到镜像中,节点的复制是镜像级别的复制。纯jar包的形式手工运维很痛苦,加机器还要处理FFmpeg,包括下载,解压,配置环境变量一系列操作,即便写成脚本也是运维同学的一个心智负担。

Java CV

最开始我们并未注意到JavaCV,所以先通过Java Process+FFmpeg+Shell做了第一版功能的实现。

后来注意到Java CV,通过依赖POM的形式就能把FFmpeg集成到Java程序中,毫无疑问比起通过Java Process来调用命令行工具更让我们心动。

可是JavaCV的学习曲线看起来很陡,目前也没有找到比较合适的学习资料,传送门,虽然能够实现我们所需要的相关功能,但是唯恐后续的成本太高甚至有可能切会Process的方式,所以我们当下仅作为调研方案参考。

并行处理

我们在考虑要不要使用线程池来并行消费MQ。

维护要并行处理?因为要更好的利用CPU,但是熟知FFmepg的同学一定知道,它是非常吃CPU性能的,并且FFmpeg本身就做了并行处理的多线程技术方案,那还需要并行处理吗?

在CPU密集型的任务中,其实是不建议开启多线程,因为本身CPU就很忙了,多线程下多了一些CPU的上下文切换,反而造成了性能的损耗。

答案是要,但是要拆分来看。把整个任务处理分成多个段来看,首先是FFmpeg处理,处理成功后要把本地文件上传至OSS服务器上,然后发出MQ,最后将本地文件删除。

拆分后可以很好的将任务分成CPU密集型+IO密集型两段,所以我们可以把后续的IO密集型线程池化处理。

Process

想来这是服务端开发这个方向上,Process应该算是比较冷门的API。

  • waitFor(long timeout, TimeUnit unit) 而不是 waitFor(),小心因为子进程阻塞把父进程耗死。
  • getInputStream + getErrorStream + close,两个流的内容都打印,遇到问题好排查
  • waitFor timeout 后调用 destroyForcibly,小心子进程丢失在父进程的管控下
  • exitValue,0表示成功,其他表示失败,比如1表示主动destroy, 6表示process 被hangs, 137表示子进程被其他进程kill

CosClient 踩坑

在使用CosClient不当时,还遇到了连接池泄露的问题。

程序设计

我们的程序设计改了两三次。

第一次设计忙于调研+交付,没有足够的时间来做设计,也没有进行合理的抽象与规划,导致第二个需求进来时几乎涉及大量复制,代码重复明显,所以进行初步设计。第二次设计又因为业务交付时间紧急只做了一部分的抽象。第三次忍无可忍在周末重新做了一遍设计。

核心逻辑主要通过责任链来组织核心代码。

前文提到把任务处理分成多个段来看,把任务分成了主要的CPU密集型+IO密集型的方式,通过责任链可以把任务拆分成更多的节点,让程序的扩展性更好。

image.png

幂等

为啥要做幂等?主要考虑MQ重试,重复投递等。

令牌桶

为啥要做令牌桶?考虑到当下我们申请的服务器的核数申请的比较高,单个任务并没有把我们的CPU核数给彻底跑满,或者接近满,所以设计了一个小的线程池来提高并发(SynchronousQueue)。

令牌桶当下我们有2个选择,原生JDK中的Semaphore,以及自定义基于Redis LPush + LPop的命令实现的令牌桶。

最后选择基于Redis LPush + LPop 的方案。为何?性能并不是我们的第一指标,Semaphore不支持动态的调整令牌个数,想要实现动态的调整令牌个数需要自定义实现。而基于Redis的方案成本很低,通过配置中心或者暴露出来的运维接口就可以较好的动态调整(令牌减少需要跟业务线程竞争令牌)。

推荐动态的去调整线程池大小,而不是令牌桶方案,这也是我们下个迭代会考虑的方案,并且对我们而言改动很小。

CPU线程池

前文已经说过了,绝大多数的任务并没有把我们的CPU的核数给彻底跑满,甚至还有不少富余,所以适当的增加并发是有必要的。

IO线程池

毋庸置疑,IO线程池干的就是大量IO的操作,适当的增加线程池的大小

FFmpeg的线程数

尝试通过限制FFmpeg线程数+CPU线程池的方式来合理的提供并发,后来发现并无优势,而FFmpeg内部的并行方案没有看到相关的源代码并不好判断,所以现在的方案是去掉了这些参数,由FFmpeg自行动态的调整。

filter_threads
filter_complex_threads

转码效率

在很多时候,需要面临处理一些特别大的视频,为了提高效率,我们需要通过一些参数来优化,但同时可能会损耗原视频的质量,评估的核心还是损耗后的质量是否在可接受范围呢。

根据码率、分辨率等参数来判断视频的质量,如何获取码率等信息?建议使用ffprobe工具,能够提取到音视频很多信息并以Json的格式返回。

具体参数:preset,参考:https://trac.ffmpeg.org/wiki/Encode/H.264

你可能感兴趣的:(搭建FFmpeg处理音视频)