Android-VideoView.seekTo() 不正确问题原因分析

困惑:
最近项目里出现了一些视频,当我们使用系统自带的VideoView去播放的时候,暂停播放后再次播放,或者拖动进度条改变seekTo之后,都无法正确的回到预期位置重新开始。而且奇怪的是,它总会总相邻的固定的几个时间点开始播放。比如我把进度拖动到10-19秒,手一松开就自动从10秒开始播放。那具体原因是什么呢,我们今天就来一探究竟。

第一步,Google。
搜到的第一条结果: https://stackoverflow.com/questions/7869148/android-video-seekto-error

QQ20201110-172514@2x.png

大致意思就是要从 VideoView 的 MediaPlayer 的 onSeekComplete() 入手,把VideoView.start() 放在它之后

// 设置 VideoView 的 OnPrepared 监听,拿到 MediaPlayer 对象。
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
            //设置 MediaPlayer 的 OnSeekComplete 监听
                mp.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
                    @Override
                    public void onSeekComplete(MediaPlayer mp) {
                    // seekTo 方法完成时的回调
                        if(isPause){
                            videoView.start();
                            isPause = false;
改完之后发现问题还是存在,奇怪了,why?
原因有二:
1、上述操作是针对边下边播、或者来源于网络端的视频的,而楼主遇到的视频都是已经下好在本地的,所以药不对口;
2、真正的原因还是因为视频的关键帧不足,我们看一下VideoView.seekTo()的源码。

    @Override
    public void seekTo(int msec) {
        if (isInPlaybackState()) {
            mMediaPlayer.seekTo(msec);
            mSeekWhenPrepared = 0;
        } else {
            mSeekWhenPrepared = msec;
     * Seeks to specified time position.
     * Same as {@link #seekTo(long, int)} with {@code mode = SEEK_PREVIOUS_SYNC}.
     * @param msec the offset in milliseconds from the start to seek to
     * @throws IllegalStateException if the internal player engine has not been
     * initialized
    public void seekTo(int msec) throws IllegalStateException {
        seekTo(msec, SEEK_PREVIOUS_SYNC /* mode */);
     * Seek modes used in method seekTo(long, int) to move media position
     * to a specified location.
     * Do not change these mode values without updating their counterparts
     * in include/media/IMediaSource.h!
     * This mode is used with {@link #seekTo(long, int)} to move media position to
     * a sync (or key) frame associated with a data source that is located
     * right before or at the given time.
     * @see #seekTo(long, int)
    public static final int SEEK_PREVIOUS_SYNC    = 0x00;
seekTo 默认使用了 SEEK_PREVIOUS_SYNC 作为 SeekMode,而SEEK_PREVIOUS_SYNC 的作用就是将位置移动到关键帧。

好了,明确了原因,我们就通过 ffmpeg 的指令来验证这一猜想吧。
首先,确保你电脑上编译了ffmpeg,然后在控制台输入查看帧信息的指令
ffprobe -show_frames -select_streams video input.mp4

接下来给大家贴一下我有问题的视频帧信息

30秒的视频才4个关键帧(还包括了0秒的第一个关键帧),难怪一直seekTo不正确。

通过ffmpeg的指令

ffmpeg -i input.mp4 -keyint_min 60 -g 60 -sc_threshold 0 -y output.mp4

对关键帧数量进行处理后得到