播放器主要流程分析
播放器的播放流程与推流过程类似,但是顺序相反。推流端先采集音频和视频,进行音视频编码和封装,并按照流媒体协议进行处理,最终得到输出流。而播放器则将输入流经过解析和解封装,得到音频包(如 AAC)和视频包(如 H.264、H.265),并进行解码以获取音频帧 PCM 和视频帧 YUV。最后,播放器需要经过音视频同步模块将视频和音频进行渲染播放,最终呈现给用户。
从线程模型上进行分析,播放引擎核心线程通常可以划分为“解封装线程(读线程)”、“音/视频解码线程”、“音/视频播放线程”,此外为了播放引擎和上层应用更好的配合,播放引擎一般还会维护一个“消息线程”,向上层报告如“播放失败错误码、视频/音频首帧播放时间、播放过程中的统计数据(如首帧耗时、音/视频码率、音视频接收帧数/秒、音视频解码帧数/秒、音视频播放帧数/秒、播放卡顿次数、播放卡顿时长、音视频同步时钟差等数据)”。
从线程模型上可以看出,播放引擎的核心线程之间是一个十分显著的生产者消费者模型。该模型可以高效地利用 CPU 多核并行的优势并且通过合理的媒体缓冲区管理可以在一定程度上抵抗网络和解码抖动,避免频繁卡顿。
播放过程中常见的问题主要集中在无法播放、无声音或无画面、画面花屏、卡顿和延迟高等问题,造成这类问题的原因可能涉及到播放端、服务器和推流端三个环节,但是往往问题排查链路都是由播放端倒推进行的,下面我们将结合我们在工作中积累的一些案例对直播流在播放过程中的一些常见问题来进行分析。
现象:客户反馈在直播过程中,画面突然卡住,声音正常。
分析:通过对播放日志以及视频流 Dump 分析我们发现问题发生时,播放端正在使用 Android 硬件解码,且拉到的视频流分辨率发生了变化(1080p->16p),通过查阅资料我们锁定了问题的根因,即 Android 的 MediaCodec 编解码对分辨率的支持范围是因设备而异的。可以通过查看设备中/vendor/etc/media_codecs_xxx.xml 查看,如设备“M2006C3LC”的 media_codecs_mediatek_video.xml,关于 H264 的解码描述如下图,该“OMX.MTK.VIDEO.DECODER.AVC”所支持的分辨率范围为(64*64 )- (2048*1088),故在解析分辨率为 16*16 的视频时可能会出现一些无法预知的问题。
实践经验:Android 硬件能力进行适配
1. Android 设备相比于 IOS 设备而言碎片化较为严重,针对不同机型的不同能力进行适配能够极大减少上述类似问题发生,好在 MediaCodec 接口中向开发者提供了相对较为全面的 Capability 查询接口,如 MediaCodecInfo.VideoCapabilities 中提供的部分接口:
public Range getBitrateRange ()
public Range getSupportedFrameRates ()
public Range getSupportedHeights ()
public boolean isSizeSupported (int width, int height)
...
MediaCodec 提供的这部分接口可以帮助我们在初始化编/解码器的时候更好的根据设备能力来选择使用硬解码器还是软解码器,尽可能的避免因为设备能力问题导致的解码失败、回退等问题。针对编码端主要有 MediaFormat 是否支持,Profile/Level 是否支持,编码分辨率是否支持,编码码率是否支持,编码帧率是否支持,编码实例数量是否超过最大支持范围等,解码端与编码端类似。
2. 维护一份黑名单:启用视频硬件编/解码可以一定程度上减轻 CPU 负担,提高视频表现,但是由于 Android 碎片化较为严重,MediaCodec 的编解码能力很多都是由设备制造商来实现的,难以达到表现的一致性,从我们的过往案例中就可发现部分机型的硬件编解能力是十分糟糕的,并且无法找出其糟糕的根本原因,所以针对这部分机型可以选择使用一份黑名单来进行维护。
3. 完善硬件解码回退软解机制:比较常见硬件解码问题如硬件解码耗时大,硬件解码 API 报错等,对于 MediaCodec 接口调用的报错一般是相对比较容易捕获的,可以通过对捕获的错误信息进行判别,对关键错误进行回退软件解码处理或者上报应用层进行处理,但是在接口调用并未报错但存在解码耗时大的问题仍然会十分影响用户体验,所以针对解码耗时过大的情况进行回退以及上报应用层也是十分必要的。
现象:在我们的项目测试过程中遇到了 pk 等场景硬件解码花屏的问题。
分析:经过我们测试发现用软解是不花屏的,硬件解码则会花屏,通过使用 ffmpeg dump 接收端的码流数据来分析,发现发生花屏的时候视频的 sps/pps 发生了变化,那问题就很明确了,是一个典型的未正确更新 sps/pps 导致的解码花屏问题。
实践经验:在一般情况下,稳定的单次推流时 sps/pps 通常都是不发生变化的,一般发生变化的情况主要就是视频分辨率发生变化的时候,原生的 ijk 播放引擎在硬件解码模块中支持分辨率切换时候的 sps/pps 更新。但是有些推流端的业务可能更加复杂,涉及到编码器的动态切换等也会导致分辨率未变时 sps/pps 发生变化。所以从通用性角度出发应该在每次接收到视频头信息时与当前视频头信息进行对比,发生变化时及时进行更新,避免出现花屏等问题。
现象:画面花屏,一段时间后恢复。
分析:通过对码流 Dump 分析我们发现这个码流没有任何问题,后来通过对日志的分析锁定了问题的根本原因:“参考帧丢失导致解码花屏”。
实践经验:花屏问题时常发生在关键帧缺失,丢失参考帧等场景中。在任何时候,编码后,解码前的视频帧都不是不建议丢弃的,否则就可能导致解码端花屏问题。在直播场景中,为了消除累计延时,有些播放器会选择丢弃帧缓冲区内的部分未解码帧,那么这种场景下比较合理的策略是丢弃一整个 GOP,或者不采用丢帧策略而采用倍速播放的方式来追赶延时,追赶到符合直播延时要求时再停止倍速策略。此外为了尽可能的避免这类问题导致的花屏问题,应在首播、断网重连等场景下进行关键帧检测,从关键帧开始进行解码。
现象:项目测试过程中遇到播放端音视频突然停止播放的情况。
分析:通过对接收端的码流进行分析我们发现,在直播的过程中音频媒体流突然停止,通过排查得知是推流端支持音频/视频媒体流的动态关闭功能。该功能会导致接收端音频或视频的媒体流突然中断,并且与播放端的现有逻辑发生冲突,从而导致一些不可预料的问题。
实践经验:如果推流端是支持动态关闭音视频流的,对播放器的适配难度是比较大的,一般需要适配的场景应包括但不限于:“先有视频后有音频场景”、“先有音频后有视频场景”、“视频流突然中断”、“音频流突然中断”等场景。播放端针对音视频流的增加场景适配难度要相对容易一些,仅需在解封装模块中动态检测媒体流的数量来判断是否有新的流增加,但是针对音频/视频媒体流突然中断的场景进行适配是很有难度的,一方面作为通用播放引擎来讲是不和推流端有信令逻辑交互的,另一方面该场景可能会和播放器很多通用逻辑,比如音视频同步(视频同步音频、音频同步视频)、缓冲逻辑等发生冲突重而导致问题,比如一般播放端的默认的同步策略是视频同步音频的,但当音频流突然中断的时候我们需要感知到推流端音频流已经关闭并且调整播放端音视频同步策略。所以如果从播放器角度出发兼容这些场景是有一定难度的,而如果从推流端角度出发,在音/视频媒体流关闭时可以选择加上静音帧/fakeVideo 视频数据,这样就可保证媒体流的持续性,从而从推流端兼容大多数播放器。
另外我们在实践中也发现,在推流端音/视频流动态开关的场景中,即便是播放端做了兼容,服务器或者 CDN 厂商的支持与否也要画一个大大的问号,特别是在应用 hls 协议拉流的场景下,因为相比其他流式传输协议,hls 协议在服务器侧还涉及到切片的逻辑,很可能服务器或 CDN 厂商的 hls 切片逻辑并不兼容这种开关音视频的操作。
现象:客户反馈某个视频流使用 rtmp 拉流正常,但是使用 hls 拉流却无法拉流。
分析:通过拿到客户的源流地址,通过 ffplay 拉流分别测试 rtmp 和 hls 拉流的表现情况,其中 hls 拉流则无声音,并且也未解析到音频信息,从 rtmp 拉流 Dump 的码流分析来看,虽然可以正常播放音频,但是却是缺少音频头信息的。
实践经验:视频/音频头缺失导致的问题无法通过播放端适配来解决,并且该问题可能较为隐匿不易发现,推流端应自查在各个该补发音/视频头的情况中是否正确的发送了音频头和视频头信息,比如音频、视频更改配置、断网重连、重新推流等场景。
现象:音画不同步或者卡死。
分析:时间戳异常大致可以分为两类,第一类即音视频时间戳不同步,即跨度大,第二类即音视频时间戳回溯的问题,比如 dts/pts 突然从 0 开始。一般情况下为了防止发生此类异常时候发生卡死等情况,拉流端都会针对时间戳不连续的场景进行兼容,一般的方案可能包括丢帧或放弃原有的时钟同步策略按照各自的播放帧间隔进行播放,该方案虽然能在音视频时间戳发生异常时候仍能继续播放,但是可能会导致音画不同步、卡顿等问题。
实践经验:该问题播放端可做一定程度兼容,但是根源上需要推流端来保证音视频时间戳的同步。
ffmpeg、ffplay、ffprobe 码流 Dump/流媒体播放/查看媒体信息等。
elecard analyzer 分析码流内容。
mediaInfo 媒体文件的信息格式分析。
flvAnalyser 分析 FLV 格式封装信息。
YUV Eye 播放 YUV 数据。