MediaExtractor有一个方法如下 http://developer.android.com/intl/es/reference/android/media/MediaExtractor.html
All selected tracks seek near the requested time according to the specified mode.
timeUs是要seek的时间戳,mode是seek的模式,可以是SEEK_TO_PREVIOUS_SYNC, SEEK_TO_CLOSEST_SYNC, SEEK_TO_NEXT_SYNC,分别是seek指定帧的上一帧,最近帧和下一帧。
此方法可用于视频播放时动态定位播放帧,用于动态改变视频播放进度,比如使用seekBar来跟踪视频播放进度,同时可拖动来动态改变播放进度。
之前一直有一个问题困扰我:seekTo方法无法精确定位到指定帧。即使使用的是某一帧精确的时间戳作为seekTo方法的输入参数也无法实现精确定位。
在google, stackoverlfow上一轮搜索,得出的结论是:在每次seekTo方法调用后,MediaCodec必须从关键帧开始解码。因此seekTo方法只会seek到最近的/上一个/下一个关键帧,也就是I-Frame(key frame = I frame = sync frame)。之所以要从关键帧开始解码,是因为每一帧不一定是单独编码的,只有I frame才是帧内编码,而P, B frame都是要参考别的帧来进行编码,因此单独拿出来是不完整的一帧。 关于I,P,B frame的问题可以参考博客: http://www.cnblogs.com/qingquan/archive/2011/07/27/2118967.html
stackoverflow上有人对此的做法是:seekTo的输入参数mode设置为SEEK_TO_PREVIOUS_SYNC,即seek的是指定帧的上一个关键帧。然后判断当前的时间戳是否小于定位关键帧的时间戳,如果是就调用MediaExtractor的advance方法,“快进”到指定帧。
extractor.seekTo(expectedPts, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
while (currentPts < expectedPts) {
extractor.advance();
}
但是这个方法仍不理想,如果seek的位置和当前位置比较远的话,会有一定延迟。而且视频内容偶尔会出现不完整的帧的闪现。
经过一段时间的研究,终于解决了这个问题,现在播放时可以根据seekBar随时拖动到视频任何一帧,不会有任何延迟,甚至可以实现倒播了。
因为之前播放视频的是自己用MediaCodec, MediaMuxer等编码合成的视频文件,在编码参数设置的时候,将关键帧间隔KEY_I_FRAME_INTERVAL设置为了1(因为要求参数为整数)。注意这个参数的单位是秒,而不是帧数!网上看到很多例子包括fadden的bigflake和Grafika上都将这里设置为了20几。搞得我一开始还以为是每隔二十几帧就有一个关键帧。如果设置为20几,那么就是说你用MediaCodec编码录制一段20多秒的视频,只有开头的一个关键帧!剩下的都是P或者B帧。
这显然是不合理的。一般来说是每隔1秒有一个关键帧,这样就可以seek到对应秒的关键帧。或者说1秒内如果有30帧,那么这30帧至少有一个关键帧。因此我将KEY_I_FRAME_INTERVAL设置为了1。
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
因此如果想使用seekBar准确拖动定位到任何一帧播放,必须保证每一帧都是关键帧。
于是,我将KEY_I_FRAME_INTERVAL设置为了0:
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 0);
事实也证明这样可以保证录制的每一帧都是关键帧,因此在使用seekTo方法的时候终于可以准确定位任何一帧了。拖动seekBar的时候视频内容也会立刻改变,无论是往前还是往后,都不会有任何延迟和画面不完整的情况(这可比我测试手机使用的自带的视频播放器强哦,自带的播放器在拖动进度条时是不会动态改变的,只有松手了才会跳到对应帧)。据此,也可以实现视频的倒序播放了。
但是,把视频每一帧都设置为关键帧是否合理呢?是否会占太大空间呢?
带着这个疑问,我使用ffmpeg查看了我测试使用的手机(Lenovo X2)内置相机录制的视频。
只需一行代码:
ffprobe -show_frames video.mp4 > frames.txt
打开frames.txt可以看到每一帧的key_frame=1,表示是关键帧
这说明了手机本来录像就是把每一帧都作为关键帧的。
当然,不能以偏概全,于是我使用iPhone 6s录制的一段普通视频和慢动作视频。使用ffmpeg查看,发现每一帧也都是关键帧(慢动作视频1秒有240帧也都全部作为关键帧也是蛮拼的)。
目前我测试的两部手机都是如此,具体为什么手机录的视频每一帧都是关键帧我也不明白。而视频文件体积大小和是否将每一帧设为关键帧似乎不成线性关系,所以将KEY_I_FRAME_INTERVAL设置为0的方案是可行的。
因此只要保证视频每帧都是关键帧,那么seekTo方法就可以精确定位指定帧了。