当播放器播放一个视频时你可以在其界面上看到时间跳动,如果调整播放位置(Seek),播放器则会提示调整到的时间。这些时间一般都是用“秒”来计,如果一个视频只有 15 秒、10 秒、或是更短,那么如何 Seek 到 0.2 秒、0.5 秒、或是 1.5 秒的位置观看呢?下面就介绍一种 LibVLC 精细 Seek 的方法,本文内容比较简单

It's about the network
VLC 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)。