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个。