Android SeekBar控制视频播放进度二——seekTo不准确
- 简介
- seekTo()
- 视频帧 和 视频关键帧
- 解决办法
- 方法一
- 方法二
简介
上一篇文章中,我们介绍了使用SeekBar控制视频播放,使用过程中发现,对于一些视频,我们拖动SeekBar
进度条调节播放进度时,调节到指定位置后,进度条会往回跳,并不会在我们拖动位置继续播放。
网上搜索了解到,VideoView.seekTo()
方法的策略决定的。具体看一下seekTo()
方法:
seekTo()
- 如下是
VideoView.seekTo(int msec)
的代码实现,我们就是通过调用该方法实现进度调节。通过查看代码,我们知道该方法实际调用的是MediaPlayer.seekTo(msec);
@Override
public void seekTo(int msec) {if (isInPlaybackState()) {mMediaPlayer.seekTo(msec);mSeekWhenPrepared = 0;} else {mSeekWhenPrepared = msec;}
}
- 继续查看
MediaPlayer.seekTo(msec);
方法的实现,该方法调用seekTo(long msec, @SeekMode int mode)
方法,默认的mode
为SEEK_PREVIOUS_SYNC
。
/*** 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 */);
}/*** Moves the media to specified time position by considering the given mode.* <p>* When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user.* There is at most one active seekTo processed at any time. If there is a to-be-completed* seekTo, new seekTo requests will be queued in such a way that only the last request* is kept. When current seekTo is completed, the queued request will be processed if* that request is different from just-finished seekTo operation, i.e., the requested* position or mode is different.** @param msec the offset in milliseconds from the start to seek to.* When seeking to the given time position, there is no guarantee that the data source* has a frame located at the position. When this happens, a frame nearby will be rendered.* If msec is negative, time position zero will be used.* If msec is larger than duration, duration will be used.* @param mode the mode indicating where exactly to seek to.* Use {@link #SEEK_PREVIOUS_SYNC} if one wants to seek to a sync frame* that has a timestamp earlier than or the same as msec. Use* {@link #SEEK_NEXT_SYNC} if one wants to seek to a sync frame* that has a timestamp later than or the same as msec. Use* {@link #SEEK_CLOSEST_SYNC} if one wants to seek to a sync frame* that has a timestamp closest to or the same as msec. Use* {@link #SEEK_CLOSEST} if one wants to seek to a frame that may* or may not be a sync frame but is closest to or the same as msec.* {@link #SEEK_CLOSEST} often has larger performance overhead compared* to the other options if there is no sync frame located at msec.* @throws IllegalStateException if the internal player engine has not been* initialized* @throws IllegalArgumentException if the mode is invalid.*/
public void seekTo(long msec, @SeekMode int mode) {if (mode < SEEK_PREVIOUS_SYNC || mode > SEEK_CLOSEST) {final String msg = "Illegal seek mode: " + mode;throw new IllegalArgumentException(msg);}// TODO: pass long to native, instead of truncating here.if (msec > Integer.MAX_VALUE) {Log.w(TAG, "seekTo offset " + msec + " is too large, cap to " + Integer.MAX_VALUE);msec = Integer.MAX_VALUE;} else if (msec < Integer.MIN_VALUE) {Log.w(TAG, "seekTo offset " + msec + " is too small, cap to " + Integer.MIN_VALUE);msec = Integer.MIN_VALUE;}_seekTo(msec, mode);
}
SeekMode
有如下几种模式,
SEEK_PREVIOUS_SYNC: seek到上一个关键帧
SEEK_NEXT_SYNC: seek到下一个关键帧
SEEK_CLOSEST_SYNC: seek到最近的关键帧
SEEK_CLOSEST: seek到最近的帧(不需要是关键帧)
/*** 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;/*** 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 after or at the given time.** @see #seekTo(long, int)*/public static final int SEEK_NEXT_SYNC = 0x01;/*** 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* closest to (in time) or at the given time.** @see #seekTo(long, int)*/public static final int SEEK_CLOSEST_SYNC = 0x02;/*** This mode is used with {@link #seekTo(long, int)} to move media position to* a frame (not necessarily a key frame) associated with a data source that* is located closest to or at the given time.** @see #seekTo(long, int)*/public static final int SEEK_CLOSEST = 0x03;
- 所以当视频在跳转到相应的 position 位置缺少关键帧的情况下,调用 seekTo 方法是无法在当前位置开始播放。这时会寻找离指定 position 最近的关键帧位置开始播放。
我们通过seekTo函数调用的实际是默认的mode
——SEEK_PREVIOUS_SYNC
,这时会寻找position的上一个关键帧。所以调节视频进度后,视频会往回跳一段,并没有在我们拖动位置继续播放。
视频帧 和 视频关键帧
上面的方法提到了帧和关键帧,下面我们简单的介绍一下两者的关联和区别。我们知道视频是由一帧一帧的图像组成的,而关键帧则是其中的某些帧。理想情况下,我们将所有的普通帧都变为关键帧,那么调节视频播放进度时将不会发生回跳情况。
解决办法
方法一
根据SeekMode
几种模式的描述,调用时指定mode
为SEEK_CLOSEST
。
方法二
对视频源文件进行处理,增加其关键帧数量。使用FFmpeg对视频处理,增加视频的关键帧。
- 首先我们通过如下命令查看当前视频中关键帧的数量:
ffprobe -show_frames /Users/Admin/Desktop/test.mp4 >video_log.txt
将视频信息输出到文本文件中,打开video_log.txt
文件,搜索关键字pict_type=I
查看关键帧。可以看到我们当前视频只有11个关键帧。
2. 对视频增加关键帧,keyint=30
每隔 30 帧设置一个关键帧。命令如下:
ffmpeg.exe -i "/Users/Admin/Desktop/test.mp4" -c:v libx264 -preset superfast -x264opts keyint=30 -acodec copy -f mp4 "/Users/Admin/Desktop/test_out.mp4"
使用步骤1的命令,查看处理后的视频信息。可以看到处理后,我们视频的关键帧数量有99个。