MediaExtractor有一个方法如下:
1 | //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来跟踪视频播放进度,同时可拖动来动态改变播放进度。
mpeg编码决定seekTo方法无法精确定位到指定帧。即使使用的是某一帧精确的时间戳作为seekTo方法的输入参数也无法实现精确定位。
在google, stackoverlfow查询得出的结论是:在每次seekTo方法调用后,MediaCodec必须从关键帧开始解码。因此seekTo方法只会seek到最近的/上一个/下一个关键帧,也就是I-Frame(key frame = I frame = sync frame)。之所以要从关键帧开始解码,是因为每一帧不一定是单独编码的,只有I frame才是帧内编码,而P, B frame都是要参考别的帧来进行编码,因此单独拿出来是不完整的一帧。
stackoverflow上有人对此的做法是:seekTo的输入参数mode设置为SEEK_TO_PREVIOUS_SYNC,即seek的是指定帧的上一个关键帧。然后判断当前的时间戳是否小于定位关键帧的时间戳,如果是就调用MediaExtractor的advance方法,“快进”到指定帧。
1 | extractor.seekTo(expectedPts, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); |
但是这个方法仍不理想,如果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。
1 | mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); |
这也是为什么我在播放用这种参数编码的视频的时候,使用seekTo方法不能准确定位帧了。之前有讲,seekTo是定位到关键帧的,如果不是关键帧,那么它会去找上一个/最近一个/下一个关键帧,这取决于你输入参数mode的设置。 因此如果想使用seekBar准确拖动定位到任何一帧播放,必须保证每一帧都是关键帧。 于是,我将KEY_I_FRAME_INTERVAL设置为了0:
1 | mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 0); |
事实也证明这样可以保证录制的每一帧都是关键帧,因此在使用seekTo方法的时候终于可以准确定位任何一帧了。拖动seekBar的时候视频内容也会立刻改变,无论是往前还是往后,都不会有任何延迟和画面不完整的情况. 但是,把视频每一帧都设置为关键帧是否合理呢?是否会占太大空间呢? 带着这个疑问,我使用ffmpeg查看了我测试使用的手机(Lenovo X2)内置相机录制的视频。 只需一行代码:
1 | ffprobe -show_frames video.mp4 > frames.txt |
打开frames.txt可以看到每一帧的key_frame=1,表示是关键帧
这说明了手机本来录像就是把每一帧都作为关键帧的。 当然,不能以偏概全,于是我使用iPhone 6s录制的一段普通视频和慢动作视频。使用ffmpeg查看,发现每一帧也都是关键帧(慢动作视频1秒有240帧也都全部作为关键帧也是蛮拼的)。
目前我测试的两部手机都是如此,具体为什么手机录的视频每一帧都是关键帧我也不明白。而视频文件体积大小和是否将每一帧设为关键帧似乎不成线性关系,所以将KEY_I_FRAME_INTERVAL设置为0的方案是可行的。 因此只要保证视频每帧都是关键帧,那么seekTo方法就可以精确定位指定帧了。