音视频播放过程中的问题解决(播放质量优化)

8,778 阅读18分钟

一、概述

本文主要介绍音视频播放过程中的一些问题,以及针对具体问题的优化方法。

二、异常情况及原因简介

  • 卡顿,上行链路或者下行链路网络带宽不足播放,设备性能不足,视频流时间戳问题
  • 花屏,有可能出现整幅画面的模糊或马赛克,sps/pps参数设置错误,或者P帧丢失或解码失败导致局部画面花屏。
  • 绿屏,sps/pps获取失败或错误,无法渲染的画面有些用黑色填充,有些用绿色填充,有些用上一帧画面填充。视频参数改变, 而解码端的SPS&PPS信息未及时重新获取更新,会导致画面无法正常渲染,继而导致绿屏的现象出现
  • 跳播,体现为播放过程中音频或视频的不连续——>即时间戳的不连续。主要由于缓冲管理、音画同步、一些主动的丢帧策略或性能瓶颈造成。
  • 拖拽无效,seek的位置不合法,seek位置计算错误(进度条错误、时间戳错误或者整个视频文件时长错误)
  • 拖拽误差过大,GOP太大
  • 黑屏,有声音但画面为黑屏。
  • 切码率异常(无痕切换),一般表现为无法渲染,或花屏,或跳播,或黑屏之类的。其实都是在不同码流切换时一些信息设置错误/没有重置。
  • 断流,除了设置断流阈值,超过阈值重连,没想到啥好办法。但重连的实现过程有许多细节要处理。
  • 音画不同步,同步算法处理不当,导致音频时间戳和视频时间戳差值过大。但也有物理意义上的音画不同步,如音频源距离麦克风太远,而音速也比光速慢很多(离麦克风别太远)。或者采集过程中的音频处理模块耗时过长(没啥办法)。
  • 声音异常,比如变声,噪声,回声,声音不连续这些现象。首先除了噪声抑制、回声消除、自动增益、重采样等这些模块的处理不当,还可能有采集或播放的编解码、渲染参数错误问题。
  • 播放失败,原因有很多。分发流中入口错误、播放后端接口返回错误、业务层创建播放container失败、创建播放器错误、流状态错误、播放url无效、DNS解析错误,以上都属于业务(第三方)相关的错误。播放层面可能导致播放失败的错误包括CDN节点连接失败、读packet错误、解码错误、播放端网络中断、流服务器宕机或负载过高、源流中断等。我们在实际工业场景中一般认为阈值时间内可以恢复的错误,不算播放失败。
  • 首帧时间长,播放器初始化时间,后端接口耗时,dns解析耗时,cdn节点调度,metadata解析时间,解码器初始化时间,播放队列开始往渲染器送数据的缓冲阈值,都会导致首帧时间过长。
  • 延迟大,主要是采集端编码耗时、上行传输链路耗时(如果是tcp会有ack和重传机制,耗时更多)、转码耗时、分发到cdn节点耗时、下行链路耗时。
  • 累积延迟,一般指上行或下行采用基于TCP协议的流媒体协议传输,由于网络抖动以及TCP的重传机制,会导致延迟不断累加增大。
  • 手机发热,采集端较为常见,播放端码率较高时也会常见。实际上是CPU的使用率过高,或内存使用率过高,全景和VR场景也有可能显卡使用率过高。比如缓冲设计不合理、频繁复制数据、算法时间复杂度过高、编解码过于依赖CPU计算、渲染数据量过大、渲染过程中的转换计算量过大、编码算法与机器配置不匹配、有一些数据精度过高等。
  • 录制视频播放异常,一般是录制时音视频编码参数设置错误,或者时间戳异常。
  • 播放闪屏,频繁出现稳定画面和融合后的画面的切换,给用户一种画面抖动的感觉。

三、优化方法

针对每一种异常情况或优化点,逐个解决及优化:

1. 卡顿

  • 上行优化,解决源流的卡顿。比如推流端设备性能配置优化、推流端性能优化、硬编、推流边缘CDN节点就近选择。
  • 抖动缓冲,在卡顿和延迟中做平衡。
  • 下载链路优化,选择更近更优的CDN边缘节点。
  • 针对播放严重卡顿的用户,或CDN负载过高的情况,直播后端下发消息,强制客户端降码率。客户端本身在检测到持续卡顿后也可以强制降码率。
  • 播放端采用硬解,优化解码性能。
  • 底层预估网络变化,重新建立与CDN的连接,并无痕切换(链路切换)。
  • TCP协议和UDP协议的挑选和结合。TCP协议可靠,但在网络抖动时会大量重传,造成更严重的拥塞。UDP协议对网络抖动的适应更好,但可能会有包乱序和包丢弃的问题
  • 可变码率,如转码平台和播放端可以支持真正的码率自适应流,应重点考虑。否则也可以采用折中方案,客户端预估网络变化,播放器底层建立与CDN低清码流的连接,获取低清晰度数据并解码,提供给上层渲染(假的分层编码实现方式),待网络恢复后又切换回源码流。这样不用从协议或转码、解码方式上进行大改,只是有可能加大CDN负载,播放SDK的下载和解码、同步逻辑会较为复杂。
  • 分层编码,直播场景下实现难度较大,但也值得考虑,在点播播放时应重点考虑。主要是对转码和解码算法的要求不同。
  • 插入假数据,比如插入少量平稳的音频帧,以及在画面变化较小时插入帧。实现难度较大,首先对执行插帧的阈值需要及时判断,否则会造成体验很差,或来不及插帧,需要考虑时间戳的修改及音画同步,判断在哪些点可以插音频帧,哪些点可以插视频帧,在下载链路恢复正常后如何切换,再次校准时间戳。这部分可以参考回声消除时,远端信号与近端信号不同步时,如何为远端信号插帧。
  • 时间戳错乱的问题(比如视频时间戳回退),这种情况下如果直接按照严格的音画同步策略,可能会直接丢弃一些视频帧不进行渲染,这样画面就暂停(卡顿)了,直到时间戳恢复正常。这时候可以增大缓冲区,调整时间戳为单调递增。

2. 花屏

  • 码率过低导致的马赛克,除了给当前分辨率匹配合适的码率,没别的办法
  • 显卡性能瓶颈,优化渲染模块的代码逻辑,比如全景和VR之类的播放,可以局部更新
  • 获取sps/pps解码信息失败或者错误,对于可伸缩编码或多码流的情况下,如果分辨率等数据变化但未及时刷新,就可能花屏(有的机型表现为绿屏)。解码信息的修改主要来源于转码服务,(比如在flv中,默认只有刚开始播放时下载的video header中才带有sps/pps信息,中途修改了编码信息,默认并不会再次发送video header),感觉服务器如果只是转封装而不是转码,很容易有这样的问题,如果服务器做了转码,可以在源流切换解码信息时加入AVCDecorderConfigurationRecord这种帧。
  • 部分参考帧丢失,有可能是采集端/播放端存在丢帧策略,而且丢弃的帧被参考,也有可能是流媒体服务器的接收缓冲区SO_SNDBUF太小,网络抖动恢复时接收的一些帧被丢弃,且这些帧被参考,导致局部画面花屏。在UDP场景下,当然也有可能是传输的不可靠性导致丢包。
  • 解码器初始化参数错误或处理逻辑错误,导致解码失败,且解码失败的帧被参考,推流端编码器初始化参数错误也会导致编码异常。
  • 有些android机型的硬编硬解兼容性不够好,当编码/解码异常时未检测出来,这个只能根据历史经验设置黑名单
  • 给渲染模块传递的参数不是实际参数,这个是bug了
  • 拖拽或重新设置解码器参数时,没有清空解码队列

3. 绿屏

  • 获取sps/pps解码信息失败或者错误,就可能绿屏。(同花屏)
  • 硬解时ios videotoolbox无法解析被切分为多个NALU单元的帧,因为ios硬解码器认为这样的帧是不完整的。播放端可以在送入videotoolbox前将本帧的多个NALU中的data(具体参考H264中nalu格式)全部复制到AVPacket中,再塞给videotoolbox。注意:当采集端开启mutli-slice会导致源流中部分视频帧切分为多个slice,存储在不同的NALU中,所以有这个问题

4. 跳播

  • 服务端缓冲管理和性能瓶颈导致,缓冲已满、流媒体服务器来不及处理缓冲中的数据,就会丢弃部分源流中的数据,导致播放端出现跳播的体验。如果是视频或直播回放录制过程中硬盘读写跟不上CPU处理产生的数据,也有可能会直接丢弃部分帧,导致跳播。
  • 播放端CPU占用过高,比如使用率过高或访问频率过高,出现性能瓶颈,可能直接丢弃部分数据。
  • 音画同步导致,如果出现时间戳回退,播放器默认会直接丢弃回退过程中的帧。或音频和视频时间戳差异过大,为了同步参考时钟,也可能丢弃部分帧。
  • 主动丢帧策略导致,为了平衡编解码/网络传输/播放速度/延迟采取丢帧策略,可能导致跳播,比如连续丢弃有关键声音信号的音频帧,或者连续丢弃视频帧。最为明显的是丢弃GOP时。
  • UDP传输不可靠导致乱序和丢包,如果外部参考时钟对应的帧已经到了,就会丢弃中间的帧。可以开发UTCP的机制,参考TCP做验证包序,以及像SRT那样客户端主动发送NACK请求重传

5. 拖拽无效

ffmpeg中seek的实现可以参考最后一部分中的文件how to seek in mp4/mkv/ts/flv。 【注:个人经验在没有关键帧列表的flv中拖拽会很慢很慢很慢。】通常seek是由用户拖动前端显示的进度条,播放器上层计算出seekTime=(进度条位置/总进度条长度)*视频总时长,然后传递到播放内核。如果seekTime不在实际的视频总时长内,就会导致seek无效,没有bug一般不会有这问题。向前seek时,由于数据未缓冲,如果下载速度过慢,某个时间段内一直没有缓冲到数据,也有可能出发播放器内部的重启机制,将本次seek取消。

6. 拖拽误差过大

seek的原理是,找seekTime位置最近的I帧。GOP=n*fps,这个n值越大,seekTime距离实际定位点越远(平均来说)。【注意seek操作后要清空解码缓冲队列】
如果是视频总时长较长,那么精准seek的需求不是很迫切,只要GOP别极其大就行。如果视频总时长较小,或者是画面切换很频繁的场景(比如户外运动),那么可能需要精准seek。
可以先定位到seekTime的前一个I帧,解码I帧及其后的数据,直到seekTime的时间戳所属的帧出现,才放入播放队列。(这样可能从体验上会感觉seek耗时过长,但一般还好,因为这种场景GOP也不用设特别大)

7. 黑屏

  • 源流没有图像,可能是推流端采集图像失败或者编码失败,可以在编码模块增加检测逻辑。
  • 持续解码失败,播放端有可能对特定的格式不支持,这是需要完善回调机制,通知上层解码失败。底层可以自行尝试重置解码器,或重新建立连接。
  • 部分推流端编码和封装格式不标准,这也是源流问题。
  • 推流端业务层切换纯音频推流/音视频推流的模式,如果一开始是纯音频流,后来切成音视频流,因为播放器默认不会重新初始化解码器,则会导致播放端黑屏。可以在播放器内部增加是否有新的帧格式出现的探测,加入播放过程中重新初始化解码器的逻辑。或者服务端下发通知。

8. 切码率异常

一般表现为无法渲染,或花屏,或跳播,或黑屏之类的。其实都是在不同码流切换时一些信息设置错误/没有重置。

9. 断流

断流无法避免地会对播放体验造成影响,只能尽可能快速恢复,并保证恢复后的正常播放

  • 断流检测的周期设置(针对源流中断或播放端下载链路等不同情况)
  • 关闭原有stream连接,清除一些不必要的数据,关闭avformat
  • 重新打开流、获取音视频解码数据
  • 重建stream连接并开始读取音视频数据
  • 此过程中find_stream_info重置解码参数可能导致恢复后倍速播放等,最好保持原解码参数不变。具体原理未知 todo

10. 音画不同步

参考音画同步原理及实现

11. 声音异常

  • 音频预处理部分的工作,可以使用webrtc的音频处理模块,比speex效果要好很多,sdk体积的增加也可以接受。参考链接: 噪声抑制 回声消除 混音 静音检测
  • 采集或播放的编解码、采样率、渲染参数错误问题。工程实现上需要注意这些问题的处理。
  • 跳播导致的声音不连续,参考跳播的处理。
  • 丢帧导致的声音不连续或变调,主要是丢帧策略的调整不当导致。比如丢帧频率很高,那么会有一顿一顿的体验,如果一次性丢弃很多时序相连的音频帧,会有跳播的体验,如果不丢音频只丢视频,声音可能变调,使用音频变速不变调算法如WSOLA。

12. 播放失败

  • 因为导致播放失败的原因可能有很多,所以最最重要的是日志收集的完善。
  • 对于上层业务(第三方)相关的错误,通过日志分析推进相关方修改。
  • CDN节点连接失败,可以尝试一次DNS解析返回多个ip,客户端在某个ip重试失败后,使用其他ip建立连接
  • 读packet错误,如果不是AVERROR_EOF这种,一般都直接丢弃packet了但频率较高或连续的读packet错误会导致用户播放体验受严重影响,而致使用户在播放异常状态下退出直播间(这种我们认为是播放失败),当读packet错误累计一定量时可以重新建立连接,防止无法恢复正常播放。出现AVERROR_EOF错误就直接重连。
  • 解码错误,一般的解码错误会体现为渲染异常,处理方式参考读packet错误
  • 播放端网络中断,其实和前面的读packet错误是部分重合,仍旧是有超时重连,加大超时时间,达到阈值就换链路重连的策略
  • 流服务器宕机或负载过高,日志上报只能起到事后分析的作用。主要是通过流服务本身的监控来实现整个集群的稳定性
  • 源流中断,有可能是推流端异常关闭,或上行链路中断,可以在推流端做一些错误处理。

13. 首帧时间长

  • 预设播放格式,如果url中已经带有音视频的一些解码参数,可以预设iformat,提前开始解码
  • DNS预解析,可以由业务层在初始化播放器的同时,向调度服务器发送请求
  • CDN调度优化,下载链路优化
  • flv metadata解析简化,因为flv的audio和video tag中其实已经带有很多信息了,上层也可以在初始化播放器时预设一些信息,所以可以一边read_frame一边解析metadata,而无需等待。
  • 播放起始过程中等待和同步的优化,比如减小packets buffer队列长度,prepare过程中不进行任何等待(start-on-prepared),强制刷新

14. 延迟大(直播)

  • 采集端网络自适应,网络环境极其差时丢帧,设计要求高,需要考虑对源流数据连续性的影响。关于采集端和播放端的丢帧,其实可以单独写一篇。如果丢弃I帧,则需丢整个GOP,如果丢弃P帧,考虑GOP中靠后的P帧,B帧可以直接丢弃。注:网络直播app中的采集端一般不编码B帧。
  • 采集端编码性能优化及代码逻辑优化,减少编码延时,可以设置mutli slice
  • 采集端采用实时性更高的协议,比如RTP或自定义UDP
  • 采集端使用可变码率VBR,当画面变化较少或网络带宽不够时,降低码率,其他时候保持或升高码率
  • 转码平台CDN节点调度优化,尽可能保证与采集端建立连接时间短,网络传输高效稳定
  • 转码平台转码性能优化,减少解码、再编码造成的延迟,拆分实时性和压缩比需求不同的任务
  • CDN分发后下载节点调度优化,性能和负载优化
  • 播放端网络自适应,丢帧,类似采集端的丢帧策略。一般丢弃音频帧后,用视频帧与音频帧比对时间戳,再基于解码渲染正常的前提,抉择丢弃的P帧编号。如果是已知解码所需的分辨率等信息,那么可以考虑直接丢弃解码前的帧(解码前丢帧如果操作不当,会造成跳播和花屏/绿屏)
  • 播放端倍速播放,音频会变调,需要进行变速不变调的处理
  • 播放端解码性能的优化
  • 播放端抖动缓冲(实现与卡顿的平衡)
  • 播放端采用实时性更高的协议
  • 播放端采用可变码率
  • 播放端采用分层编码流

15. 累积延迟(直播)

  • 平滑丢帧和快速丢帧两种方式

16. 手机发热

目前记录到的:

  • CPU使用率过高,主要是计算复杂度过高,数据精度过大,算法要求机器配置较高,软编的时候很常见,矩阵转换之类的操作多,全景的时候很常见
  • CPU使用频率过高,核数较少,线程切换极为频繁。在核少的机型上,有时候单线程解码会比多线程解码效率更高。
  • 内存使用率过高,缓冲队列设计不合理,频繁复制数据之类的
  • 显卡使用率过高,超清或全景/VR之类的会存在显卡使用率过高的情况,可以降低部分画面的刷新率

17. 录制视频播放异常

  • pts写入时校准
  • 一些关键的解码信息不能写错

18. 闪屏

这部分我没有遇到过,网上资料提到是指视频图像融合,或者播放器卡顿时中加入一些默认的图片,导致画面抖动(也就是闪屏)。连麦时的画面合成,或者加logo,或者播放队列插帧,都要注意平滑过渡。

四、检测和监控

  • 播放端日志上报
  • 针对streamid实现从推流、转码、分发、下载、播放的全链路监控

五、参考

  1. 直播问题分析总结
  2. ffmpeg-how to seek in mp4/mkv/ts/flv