目的
ExoPlayer原生支持的容器有限,解复用和解码能力不足,需要通过ffmpeg来扩充解复用和解码能力,本文档描述用在ExoPlayer里用ffmpeg的libavformat进行解复用的实现方案,同时也会说明ExoPlayer的Extractor
扩展方式。
ExoPlayer版本
此文档基于当前最新的ExoPlayer 2.10.x版本,后续新版本可能会有较大变化。
Extractor扩展说明
Extractor的功能
1. 拆包
将ExtractorInput
送过来的数据流解析后,拆包成未解码的音频和视频帧,并加上时间戳后,通过TrackOuput.sampleData
输出。
2. 时间Seek
Seek到指定的时间,有两种seek方式:
- 基于索引表,能通过文件头信息解析出时间与文件位置的关系,直接Seek到对应的文件位置,通过查找SeekMap实现,mp4文件采用此种方法,但很多文件并没有索引表,会采用方法2;
- 二进制seek,先通过文件大小得出一个大致的文件位置,然后用二分查找法找到最接近目标时间的文件位置,通过
Extractor.read
来实现Seek的操作。
实现FfmpegExtractor要解决的问题
由于ExoPlayer对IO进行了封装,在Extractor层面只能通过ExtractorInput
获取流数据,并没有文件常见的read和seek操作,而ffmpeg的IO已经通过AVIOContext
封装,提供了read_packet
和seek
回调函数,由此可见,ExoPlayer和ffmpeg的IO是无法直接兼容的,需要解决一下两个问题:
1. 数据读取
由于ExtractorInput
是动态的,当ProgressiveMediaPeriod.ExtractingLoadable.load
函数的while循环退出时,就会从dataSource
新创建ExtractorInput
,所以要采用代理模式,通过ExtractorInputWrapper
将动态的ExtractorInput
进行封装,提供给FfmpegParserJni
的read回调,给ffmpeg提供流数据。
2. 数据Seek
这里的数据seek是指IO Seek,而不是上面提到的时间Seek,是指ffmpeg在解析流信息和执行时间seek时,需要在流数据里前后随机访问指定位置的数据,而ExoPlayer提供给Extractor访问数据的方式,无法满足随机访问的方式,唯一的实现方法是在Extractor.read里返回RESULT_SEEK,并设置seekPosition.position,让上层的load循环重新创建ExtractorInput,但ffmpeg里的IO操作是不能被打断的,所以,必须要想办法解决这个问题。
解决问题的思路
从上面的分析可以得出,libavformat的IO操作是不能被打断,且av_read_frame
和av_seek_frame
都是同步调用,所以调用必须在另外一个线程里,解决方案如下:
FfmpegExtrator
使用HandlerThread
创建Seek线程,并创建发送message的主线程Handler;- 当read或seekTimeUs时,在Seek线程异步调用
FfmpegParserJni.readFrame
或FfmpegParserJni.seekTo
,并阻塞load线程; - 最终通过native函数调用到libavformat的
av_read_frame
或av_seek_frame
函数,此时进入了ffmpeg的IO模式; - libavformat将根据所解析的容器协议进行数据read和seek,read操作直接回调到
FfmpegParserJni.read
,从ExtractorInputWrapper
里读取数据; - 也有可能调用IO Seek,seek操作回调到
FfmpegParserJni.seek
,此回调先唤醒Load线程,告知ffmpeg需要seek的位置; FfmpegExtractor.read
返回RESULT_SEEK,并将要seek的位置放在seekPosition.position
;ProgressiveMediaPeriod.ExtractingLoadable.load
将会重新创建一个新的ExtractorInput
,数据的位置就是seekPosition.position
。
通过上面的步骤实现了,FfmpegExtractor和ffmpeg异步IO对接,实现更多容器的解复用。