88

唱吧直播硬件播放器及其演进

 6 years ago
source link: https://mp.weixin.qq.com/s/hUwCVjuPlDD64LYjFtlLIQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

唱吧从 2015 年开始进入直播领域,从刚开始的 PC 秀场直播产品「唱吧直播间」,到后面的手机直播产品「火星直播」,在直播领域持续发力,技术上也有了一定的积累。直播移动端技术有两块比较核心的模块,一块是直播推流,一块是直播拉流,就是播放直播。唱吧直播技术团队一点一点摸索过来,踩了很多坑,也积累了很多技术经验。今天主要分享一下唱吧自研的硬件解码播放器以及演进过程。

为什么要做硬件解码播放器?

在研发硬件解码播放器之前,我们使用了一款叫做 KxMovie 的开源播放器,作者已经停止维护,最近的一次 commit 是四年前。我们直播技术团队在 2015 年的时候开始使用的 KxMovie,为了满足我们的需求,在其基础上做了大量的修改。KxMovie 是基于 FFmpeg 的,音视频解码都是使用 FFMPEG 内置的软件解码器,缺点是占用 CPU 较高,解码效率不高。

随着直播产品功能的逐渐丰富,对性能的要求越来越高,播放过程中当有大量的聊天消息和礼物特效时,CPU 就被占满,经常用用户反馈看到的画面不流畅,交互也不流畅,手机发热也比较严重。为了解决这个问题,我们放弃了 KxMovie,研发了硬件解码播放器,CBPlayer。CBPlayer 基于 FFmpeg 和 iOS 系统自带的 VideoToolbox framework,VideoToolbox 使用 iOS 设备内置的编解码芯片进行解码,解码效率更高,占用 CPU 更低,支持 iOS 8 及以上的系统。

如下图所示,和 KxMovie 相比,CBPlayer 少占用了 74% 的 CPU。

Image

CBPlayer 的工作流程

在介绍 CBPlayer 工作流程之前,先简单的介绍一下唱吧直播客户端的推拉流采用的协议和封装格式。我们推流采用的 RTMP 协议(基于 TCP 协议和 FLV 音视频封装格式),FLV 里视频格式用的是 H.264 编码,音频是 AAC 编码。播放端采用的是 HTTP 协议和 FLV 封装格式,同样视频是 H.264 编码,音频是 AAC 编码。都是业内常用的协议和格式。

Image

1. CBPlayer 使用 FFmpeg 从 CDN 节点拉取 FLV 流,然后 FFmpeg 会 demux 出来 AAC 编码的 Audio Packet 和 H.264 编码的 Video Packet(在 FFmpeg 里都被称为 AVPacket)。

2. 获得 AVPacket 之后,下一步交给解码器。 Audio Packet 通过音频解码器解码出 PCM 的数据,存入 Audio Frame Queue 队列。Video Packet 交给硬件解码器解码出 CVImageBufferRef(= CVPixelBufferRef) 数据,CVImageBufferRef 里就是 H.264 视频数据,并存入 Video Frame Queue 队列。音视频的解码器都工作在独立的线程中,互相独立,互不影响。

3. 现在有了 PCM 数据,CBPlayer 使用 AUGraph 来播放这些 PCM 数据,同时也支持在 AUGraph 中串入额外的 AudioUnit,来提供更多的音效,以及对音频更细粒度的控制和更好的扩展性。

AUGraph 支持推和拉模式,这里我们使用的是拉数据的模式。之所以使用拉模式的主要原因是,CBPlayer 的音视频对齐方式是视频向音频对齐。人的耳朵比眼睛要敏感的多,视频跳几帧人眼不太能感觉得到,但是音频如果跳帧,耳朵很容易听出来,所以直播场景下,大多采用视频向音频对齐。在拉模式下,AUGraph 会不断的调用回调函数要音频数据,每次回调,player 从 Audio Packet Queue 里拿一次数据给 AUGraph,同时把相应时间的视频帧渲染出来,就实现了视频向音频对齐。这么做比较讨巧,也比较简单。当然也可以自己实现时钟,灵活的设置音视频的对齐方式,这个后面如果有类似的需求,也会继续完善。

4. 说完音频,再说说视频。CBPlayer 除了支持基于 VideoToolbox 的硬件解码之外,也支持基于 FFmepg 的软件解码。如果 app 要求支持 iOS 8 以下的设备,会 fallback 到软件解码。拿到解码后的 CVImageBufferRef 之后,使用 OpenGL 渲染到 CAEAGLLayer 上。在 OpenGL 层还可以加一些处理,比如我们之前为了提升清晰度,在 OpenGL 渲染之前,加上了自动对比度调节的处理。

硬件解码除了手动的用 VideoToolbox 做解码之外,系统也提供了原生支持硬件解码的组件。比如 AVSampleBufferDisplayLayer,这个 layer 是 CALayer 的子类,它支持直接通过 -(void)enqueueSampleBuffer:(CMSampleBufferRef)sampleBuffer 方法填充编码后的 CMSampleBuffer 数据,内部会自动使用 VideoToolbox 进行硬件解码。优点是比较方便,缺点是不好干涉内部的处理流程,扩展性差。

整个流程到这就结束了,这是一个简化后的模型,实际上要比这个复杂,要处理多线程间的同步,本地播放缓存的处理,以及很多种意外的情况等等。

我们踩过的那些坑

在实际场景中,我们遇到了很多和播放相关的问题,不局限于硬件解码方面,我挑几个典型的跟大家分享一下。

  1. OpenGL 渲染操作不能运行在后台线程,否则会 crash。播放的时候,如果用户按 Home 键退到后台,这个时候需要及时的停掉 OpenGL 的渲染,并清空本地队列里的视频帧。对于没有执行完的命令,执行 glFinish()函数,强制执行完所有的 OpenGL 命令。

  2. 设置合理的 probe size 和 analyze duration。这两个参数是用来告诉 FFmpeg 使用多少时间来探测流信息,或者拉去多大 size 的流来探测流信息。值越小,用在探测流信息的时间越少,首屏时间越快,但是可能会探测到不完整的流信息。值越大,越能探测到完整的流信息,但是首屏时间也会延长。不同的码率,这个值的大小也不一样。最好根据自身的状况,反复尝试出一个合理的值,并做好重试的策略。对于 FLV 流来说,从 CDN 缓存出发也可以做一些优化。首先,最好音视频的 metadata 都放在 FLV 流的头里面,这样拿到 FLV 的头就拿到了音视频的 metadata。或者优化音视频帧的顺序,因为视频帧比音频帧大很多,如果能让先拿到音频帧,再拿到视频帧,就可以减小 probe size。我们遇到的问题是,偶尔拿到了视频的 metadata,但是没有获得音频的 metadata,导致了使用错误的 sample rate 播放音频,播放出来的效果也是不对的。研究了一下发现,是先拿到了比较大的视频帧,后拿到音频帧,由于 probe size < FLV 头 + 视频帧的大小 + 音频帧头大小,导致没有获取到完成的音频帧的 metadata。

  3. 做好重试策略。可能因为网络或者参数等各类原因导致打开流或者获取流信息失败。做好判断和重试策略,提升播放成功率。

  4. 处理好 FFmpeg 的 interrupt 的情况。这个是遇到很多次也很纠结的问题。FFmpeg 是靠传入一个 interrupt 的函数指针来做中断的。什么意思呢,就是一旦开始 FFmpeg 的工作流,就不能主动让它马上停止了,只能被动的等待 FFmpeg 回调传入的 interrupt 函数,在这个函数里返回 YES,它才会停止。比如 avformatopeninput 或者 avreadfream 函数是阻塞的,弱网情况下,回调函数的调用也比较慢,时间也不确定,如果这个时候用户关闭了播放页,需要及时的让 interrupt 函数返回 YES,妥善的协调好 FFmpeg 的 AVFormatContext 以及 interrupt 函数里引用的对象的生命周期的关系,否认可能就会 crash. 

  5. 本地 buffer 队列大小的问题。本地 buffer queue 越大,抵抗网络抖动越好,观看延迟就越大,反之,本地 buffer queue 越小,抵抗网络抖动能力越差,观看延迟就越小。

CBPlayer 的开源计划正在准备中,敬请期待。

另外欢迎热爱流媒体研发的的你,任性地把你 real 的简历发到的 qiaoxueshi#changba.com,我们一起 homie bro peace yo!

That's all, thank you!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK