ExoPlayer通过ffmpeg支持更多容器实现说明

FfmpegExtractor实现说明

Posted by Jack on May 20, 2020

目的

ExoPlayer原生支持的容器有限,解复用和解码能力不足,需要通过ffmpeg来扩充解复用和解码能力,本文档描述用在ExoPlayer里用ffmpeg的libavformat进行解复用的实现方案,同时也会说明ExoPlayer的Extractor扩展方式。

ExoPlayer版本

此文档基于当前最新的ExoPlayer 2.10.x版本,后续新版本可能会有较大变化。

Extractor扩展说明

Extractor的功能

1. 拆包

ExtractorInput送过来的数据流解析后,拆包成未解码的音频和视频帧,并加上时间戳后,通过TrackOuput.sampleData输出。

2. 时间Seek

Seek到指定的时间,有两种seek方式:

  1. 基于索引表,能通过文件头信息解析出时间与文件位置的关系,直接Seek到对应的文件位置,通过查找SeekMap实现,mp4文件采用此种方法,但很多文件并没有索引表,会采用方法2;
  2. 二进制seek,先通过文件大小得出一个大致的文件位置,然后用二分查找法找到最接近目标时间的文件位置,通过Extractor.read来实现Seek的操作。

实现FfmpegExtractor要解决的问题

由于ExoPlayer对IO进行了封装,在Extractor层面只能通过ExtractorInput获取流数据,并没有文件常见的read和seek操作,而ffmpeg的IO已经通过AVIOContext封装,提供了read_packetseek回调函数,由此可见,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_frameav_seek_frame都是同步调用,所以调用必须在另外一个线程里,解决方案如下:

  1. FfmpegExtrator使用HandlerThread创建Seek线程,并创建发送message的主线程Handler;
  2. 当read或seekTimeUs时,在Seek线程异步调用FfmpegParserJni.readFrameFfmpegParserJni.seekTo,并阻塞load线程;
  3. 最终通过native函数调用到libavformat的av_read_frameav_seek_frame函数,此时进入了ffmpeg的IO模式;
  4. libavformat将根据所解析的容器协议进行数据read和seek,read操作直接回调到FfmpegParserJni.read,从ExtractorInputWrapper里读取数据;
  5. 也有可能调用IO Seek,seek操作回调到FfmpegParserJni.seek,此回调先唤醒Load线程,告知ffmpeg需要seek的位置;
  6. FfmpegExtractor.read返回RESULT_SEEK,并将要seek的位置放在seekPosition.position
  7. ProgressiveMediaPeriod.ExtractingLoadable.load将会重新创建一个新的ExtractorInput,数据的位置就是seekPosition.position

通过上面的步骤实现了,FfmpegExtractor和ffmpeg异步IO对接,实现更多容器的解复用。