播放器性能的极致优化,就是要发现播放流程中一点一滴的耗时,然后分析这些耗时,并提出有效的方式解决这些耗时.了解过ExoPlayer播放器的同学们都知道ExoPlayer的解码是依赖Android系统提供的原生的解码模块,即MediaCodec来实行视频和音频解码的.
基于我们对MediaCodec使用方式和原理的熟悉,我们在使用MediaCodec的时候,首先要初始化,codec configure, codec start等流程,经过这些流程,codec分配内存来存储input buffer 和 output buffer
这块耗时对视频播放而言是无法避免的,不管是网络视频还是本地视频, codec 初始化--->configure ---> start ---> 上屏这部分的耗时都是无法避免的.
我们抓一下一个1080P的本地视频的systrace, 显示如下:
可以发现: video decoder和audio decoder创建/configure/start耗时了170ms, 直接占用了绝大部分耗时, 确实是耗时的大头.
目前ExoPlayer最新的版本是2.12.2, 而且保持着凉州一个小版本, 两个月一个大版本的更新节奏, google的工作效率还是比较高的. 针对codec 复用, 在2.10版本之前和2.10之后的版本, 使用方式和原理还是有所不同的.
ExoPlayer 2.10版本之前的codec复用
- disabled状态, 在此状态下, 渲染器没有要播放的媒体流并且不保存解码器实例。
- enabled状态, 在此状态下,渲染器可以播放媒体流,并在可能的情况下(例如,在读取流的格式之后)获取解码器实例。
- started状态, 在此状态下,渲染器使用持有的解码器实例来播放视频
在2.10之前的版本中,只要视频渲染器保持在启用和启动状态,ExoPlayer就会尽可能重复使用视频解码器。特别是,这意味着从播放列表中的一种媒体过渡到另一种媒体时,视频解码器将被重用。在渲染列表过渡到禁用状态时,音频解码器不会在此类播放列表过渡中重用,视频或音频解码器也不会重用。重要的是,重新准备播放器以播放其他MediaSource会导致这种类型的状态转换,因此在这种情况下不会重复使用解码器。
一句话, 想复用codec实例,不能调用stop方法,不能将codec置为disabled状态
ExoPlayer 2.10版本之后的codec复用
2.10版本可以保证在播放新的视频源的时候复用播放器解码器资源.即使当前的渲染器持有的解码器实例已经被置为disabled状态
复用解码器实例可以减少多个视频源之间切换导致的丢帧的问题,更加可以节省时间.下面是复用解码器实例之后的systrace耗时:
解码器实例复用要求
解码器实例可以复用,肯定是有条件的,例如上一个视频播放的是VP9的视频,下一个视频播放的是H265的视频,那么解码器实例肯定不能复用. 具体是什么条件了,我们还是查看源码确认一下: MediaCodecRenderer.java有两个子类, MediaCodecVideoRenderer.java和MediaCodecAudioRenderer.java
MediaCodecVideoRenderer.java
代码语言:javascript复制
@Override
protected DecoderReuseEvaluation canReuseCodec(
MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) {
DecoderReuseEvaluation evaluation = codecInfo.canReuseCodec(oldFormat, newFormat);
@DecoderDiscardReasons int discardReasons = evaluation.discardReasons;
if (newFormat.width > codecMaxValues.width || newFormat.height > codecMaxValues.height) {
discardReasons |= DISCARD_REASON_VIDEO_MAX_RESOLUTION_EXCEEDED;
}
if (getMaxInputSize(codecInfo, newFormat) > codecMaxValues.inputSize) {
discardReasons |= DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED;
}
return new DecoderReuseEvaluation(
codecInfo.name,
oldFormat,
newFormat,
discardReasons != 0 ? REUSE_RESULT_NO : evaluation.result,
discardReasons);
}
- 首先当前的视频的最大分辨率不能超过codec支持的最大解码分辨率
- 其次视频的输入size不能超过codec的input buffer size
MediaCodecAudioRenderer.java
代码语言:javascript复制@Override
protected DecoderReuseEvaluation canReuseCodec(
MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) {
DecoderReuseEvaluation evaluation = codecInfo.canReuseCodec(oldFormat, newFormat);
@DecoderDiscardReasons int discardReasons = evaluation.discardReasons;
if (getCodecMaxInputSize(codecInfo, newFormat) > codecMaxInputSize) {
discardReasons |= DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED;
}
return new DecoderReuseEvaluation(
codecInfo.name,
oldFormat,
newFormat,
discardReasons != 0 ? REUSE_RESULT_NO : evaluation.result,
discardReasons);
}
- 音频的输入size不能超过codec的input buffer size
我们如何复用解码器
- 保证你播放的视频的格式是相同的,例如短视频信息流都是H264/AAC编码格式
- 如果使用ExoPlayer播放器,最好使用同一个ExoPlayer实例,因为codec实例是封装在ExoPlayer实例中的
- 当重新prepare新的视频时,在调用prepare之前不能调用stop函数,因为stop函数会释放解码器实例
- 如果你要调用stop函数的情况下还想保持解码器实例不被释放,你需要调用setForegroundMode(...)函数,这个函数的使用方式是比较复杂的,不能误用
https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ExoPlayer.html#setForegroundMode-boolean-
打开了ForegroundMode模式,意味着解码器资源在调用stop的时候都不会被释放,当然这是在复用解码器资源的场景下使用的,使用的时候要小心一些,在最终的时候还是要记得释放资源的,不然会出现问题.