当播放器播放一个视频时你可以在其界面上看到时间跳动,如果调整播放位置(Seek),播放器则会提示调整到的时间。这些时间一般都是用“秒”来计,如果一个视频只有 15 秒、10 秒、或是更短,那么如何 Seek 到 0.2 秒、0.5 秒、或是 1.5 秒的位置观看呢?下面就介绍一种 LibVLC 精细 Seek 的方法,本文内容比较简单。
顺便提一下,Android App 使用 LibVLC 还需要集成 VLC 的 MediaLibrary,因为源码有依赖。本文提到的 MediaPlayer 均指 LibVLC 的媒体播放器 org.videolan.libvlc.MediaPlayer
,不是 Android 系统的媒体播放器 android.media.MediaPlayer
。
LibVLC 播放引擎在播放时会主动的报告位置变化、时间变化等信息。MediaPlayer 通过 setEventListener(MediaPlayer.EventListener listener)
方法来设置播放器事件侦听,侦听器(MediaPlayer.EventListener)会接收到播放器事件。LibVLC 已经将播放器事件侦听放到了主线程(UI 线程),首先我们来确认一下,以下代码来自 LibVLC 3.0.13。
MediaPlayer.java 第 1154 行。设置侦听器实则调用父类(VLCObject)的方法,继续往下跟。
public synchronized void setEventListener(EventListener listener) {
super.setEventListener(listener);
}
VLCObject.java 第 100 行。父类实现是直接调 setEventListener(listener, null)
,而具体的实现就紧接着下方,这里可以看到会创建一个 Handler 并且绑定到 App 的主线程,源码的注释也写了是运行在主线程。
/**
* Set an event listener.
* Events are sent via the android main thread.
*
* @param listener see {@link VLCEvent.Listener}
*/
protected synchronized void setEventListener(VLCEvent.Listener<T> listener) {
setEventListener(listener, null);
}
/**
* Set an event listener and an executor Handler
* @param listener see {@link VLCEvent.Listener}
* @param handler Handler in which events are sent. If null, a handler will be created running on the main thread
*/
protected synchronized void setEventListener(VLCEvent.Listener<T> listener, Handler handler) {
if (mHandler != null)
mHandler.removeCallbacksAndMessages(null);
mEventListener = listener;
if (mEventListener == null)
mHandler = null;
else if (mHandler == null)
mHandler = handler != null ? handler : new Handler(Looper.getMainLooper());
}
有兴趣的朋友也继续往下跟,LibVLC 基本是 C 语言开发的,Andrid 版只是加了一层 JNI 以及少量的 Java Wrap。播放引擎事件通知是 C 调用 Java 类中的方法,相关的代码可以参考:libvlcjni.c(第 199 行)、libvlcjni-vlcobject.c(第 190 行)。
确定了事件侦听在主线程,就可以在侦听器里直接操作 UI 元素,不用做重复的转换主线程的动作,以至于降低 App 的运行效率。接下来分析这个精细 Seek 的需求。
首先看功能,MediaPlayer 调整播放位置的方法是 setPosition(float pos)
,传入的参数是个比例(0.0f 表示开始处,1.0f 表示结尾处),不用转换为“秒”或“毫秒”,只要参数是精细的调整的位置就是精细的,即功能本身是支持的。然后看控制,我们开发播放器一般使用 SeekBar 控件来指示和调整播放进度,这里 SeekBar 按比例转换即可。最后是显示,时间显示需要添加支持 .01 秒,时间变化有两种情况,一个是播放引擎主动发出来的时间变化事件,另一个是用户操作 SeekBar 调整播放进度时的时间提示。
下方是播放器事件侦听的一部分,MediaPlayer.Event.TimeChanged
事件报告当前播放进度的时间值,单位是 ms(毫秒)。setTime()
则是更新时间显示,这里直接操作 UI 元素。
private final MediaPlayer.EventListener mListenMediaPlayer = new MediaPlayer.EventListener() {
@Override
public void onEvent(MediaPlayer.Event event) {
switch (event.type) {
case MediaPlayer.Event.TimeChanged:
setTime(TextUtil.formatTime(event.getTimeChanged()));
break;
//省略部分代码
}
}
};
下方是 SeekBar 侦听部分代码,当滑动 SeekBar 时会有个时间提示,告知用户 Seek 到的时间点。
private final SeekBar.OnSeekBarChangeListener mListenSeekBar = new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (!fromUser || mVideoLengthMS < 100) {
return;
}
float position = (float) progress / seekBar.getMax();
String time = TextUtil.formatTime((long) (mVideoLengthMS * position));
setState(time);
}
//省略部分代码
};
把时间格式化放到公用静态方法里 String formatTime(long milliseconds)
,当小于 2 秒时则按 .2f
格式返回,否则按普通格式返回。
private static final StringBuilder sFormatBuilder = new StringBuilder();
private static final Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
public static String formatTime(long milliseconds) {
sFormatBuilder.setLength(0);
if (milliseconds < 1000 * 2) {
return sFormatter.format("%.2f", (float) milliseconds / 1000).toString();
}
// >= 2 seconds
long seconds = milliseconds / 1000;
if (seconds < 3600) {
int m = (int) (seconds / 60);
int s = (int) (seconds % 60);
return sFormatter.format("%02d:%02d", m, s).toString();
} else {
int h = (int) (seconds / 3600);
int m = (int) (seconds % 3600 / 60);
int s = (int) (seconds % 60);
return sFormatter.format("%d:%02d:%02d", h, m, s).toString();
}
}
效果图在开头,一个长度为 9 秒视频。播放器暂停于 1.15 秒位置。屏幕中间显示的是用户调整时的时间提示,设置到了 0.65 秒处(松开 SeekBar 才会 setPosotion)。