〇、背景

最近有做需求关于视频缓存,了解到相关的开源库AndroidVideoCache,一款市面上相对比较流行的视频缓存框架,而我想利用该框架进行视频缓存的处理,并且希望能够支持预加载。然而该框架作者在18年就已经停止了维护,所以留下了无限的编程空间给其他程序员,对于视频预加载,只搜到一篇《AndroidVideoCache源码详解以及改造系列-源码篇》,然而点进该作者的博客列表,说好的预加载呢???后面也没有了下文,搜遍全网好像没有做AndroidVideoCache的预加载相关的事情,那么这样子的话……自己干吧。

首先需要明白AndroidVideoCache的实现原理,推荐查看《AndroidVideoCache-视频边播放边缓存的代理策略》这里不再赘述。

其实预加载的思路很简单,在进行一个播放视频后,再返回接下来需要预加载的视频url,启用后台线程去请求下载数据,不过中间涉及的细节逻辑比较多。

一、实现方案

主要逻辑为:

1、后台开启一个线程去请求并预加载一部分的数据

2、可能需要预加载的数据大于>1,利用队列先进入的先进行加载,加上前面的条件 使用HandlerThread再适合不过了。

我们首先定义好需要去处理的任务情况:

private void preload( String method,Call call) {
        switch (method) {
            case "addPreloadURL":
                addPreloadURL(call); //添加url到预加载队列
                break;
            case "cancelPreloadURLIfNeeded":
                cancelPreloadURLIfNeeded(call); //取消对应的url预加载(因为可能是立马需要播放这个视频,那么就不需要预加载了)
                break;
            case "cancelAnyPreloads": 
                cancelAnyPreLoads();//取消所有的预加载,主要是方便管理任务
                break;
            default:
            
        }
    }

那么对于每次的预加载逻辑基本上是这样的方法执行顺序:

cancelPreloadURLIfNeeded()->addPreloadURL(); //取消对应url加载的任务,因为有可能该url不需要再进行预加载了(参考抖音,当用户瞬间下滑几个视频,那么很多视频就需要跳过了不需要再进行预加载)

cancelAnyPreLoads()->addPreloadURL(); //取消对应url加载的任务(这时候需要立马播放最新的视频,那么就应该让出网速给该视频),之后再添加新一轮的预加载url。

接下来具体的处理逻辑VideoPreLoader类,我直接放上所有的代码逻辑吧,为方便观察删除了一部分不太重要的逻辑,其实总体流程也比较简单。

public class VideoPreLoader {
  private Handler handler;
  private HandlerThread handlerThread;
  private List<String> cancelList = new ArrayList<>();

  private VideoPreLoader() {
    handlerThread = new HandlerThread("VideoPreLoaderThread");
    handlerThread.start();
    handler = new Handler(handlerThread.getLooper()) {
      @Override
      public void handleMessage(Message msg) {
        super.handleMessage(msg);
      }
    };
  }

  void addPreloadURL(final VideoPreLoadModel data) {
    handler.post(new Runnable() {
      @Override
      public void run() {
        realPreload(data);
      }
    });
  }

  void cancelPreloadURLIfNeeded(String url) {
    cancelList.add(url);
  }

  void cancelAnyPreLoads() {
    handler.removeCallbacksAndMessages(null);
    cancelList.clear();
  }

  private void realPreload(VideoPreLoadModel data) {
    if (data == null || isCancel(data.originalUrl)) {
      return;
    }
    HttpURLConnection conn = null;
    try {
      URL myURL = new URL(data.proxyUrl);
      conn = (HttpURLConnection) myURL.openConnection();
      conn.connect();
      InputStream is = conn.getInputStream();
      byte[] buf = new byte[1024];
      int downLoadedSize = 0;
      do {
        int numRead = is.read(buf);
        downLoadedSize += numRead;
        if (downLoadedSize >= data.preLoadBytes || numRead == -1) { //Reached  preload range or end of Input stream.
          break;
        }
      } while (true);
      is.close();
    }
    ....
  }

  private boolean isCancel(String url) {
    if (TextUtils.isEmpty(url)) {
      return true;
    }
    for (String cancelUrl : cancelList) {
      if (cancelUrl.equals(url)) {
        return true;
      }
    }
    return false;
  }
}

对于这段代码中其实有“两个”队列,一个是HandlerThread中的队列,熟悉消息机制的同学应该都能明白,内部是一个looper在不断地循环获取消息,当一个消息处理完毕之后才会处理下一个消息。我还定义了一个就是取消队列,因为HandlerThread中的任务我们不太好控制取消具体的任务,所以设置了一个取消队列,当之后的消息再需要执行的时候会首先判断是否是在取消队列里面,这样子就能做到对预加载队列逻辑的控制。

二、关于一些细节问题

这样子我们在播放一个视频的时候,只需要传给我们接下来将会播放的视频的URL,我们就能对其预加载并缓存下来,但是会存在其他条件:

预加载的长度?

对于视频加载长度,我们很容易想到在视频url请求加入Range在header上面,比如

conn.addRequestProperty("Range", "0-102400");

我们只获取前102400 bytes,不用将整个视频全部进行预加载,我有进行这样的尝试,但是实际发现是有坑的。我做了很多尝试,发现不论怎么请求,拿到的 responseCode 虽然是206,但是 还是把数据给全部下载完了,这就有点不科学了!!

最终去源码中才发现:源码有对range做正则匹配

private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-");

private long findRangeOffset(String request) {
        Matcher matcher = RANGE_HEADER_PATTERN.matcher(request);
        if (matcher.find()) {
            String rangeValue = matcher.group(1);
            return Long.parseLong(rangeValue);
        }
        return -1;
}

看清楚了 "[R,r]ange:[ ]?bytes=(\d)-"* 它只去匹配了前面的的,也就是说 我传入了 0-102400 它最终只当作是:Range:0- 来处理,导致addRequestProperty设置的range实现。坑!不过能理解作者为什么这么做,后面总结会讲到。没有办法只有使用最原始的方法进行判断了:在每次获取inputStream的时候进行判断是否达到预加载的大小,虽然有一定的性能开销,但是不去改源码的话也没有 办法了。

do {
        int numRead = is.read(buf);
        downLoadedSize += numRead;
        if (downLoadedSize >= data.preLoadBytes || numRead == -1) { //Reached  preload range or end of Input stream.
          break;
        }
      } while (true);
      is.close();

三、总结

本文主要讲了基于AndroidVideoCache的预加载具体实现原理,以及其中遇到的坑

1、预加载主要通过HandlerThread去实现后台网络的访问以及缓存的处理逻辑

2、加入取消队列去控制对应需要取消的任务

3、对于预加载的size只能通过读取的时候进行判断,没有办法使用range去判断。其实很容易理解作者为什么正则要这样写,因为它只是一个视频缓存框架,主要是用来做“边播边存”,所以每次去进行请求的时候应该都是在原有的缓存之上去进行缓存数据处理,而缓存最终需要处理完的就是 content-size,不需要再去管Range中的结束范围了。

项目 Demo 地址:AndroidVideoCachePreload