Android视频点播的实现代码(边播边缓存)

简述

一些知名的视频app客户端(优酷,爱奇艺)播放视频的时候都有一些缓存进度(二级进度缓存),还有一些短视频app,都有边播边缓的处理。还有就是当文件缓存完毕了再次播放的话就不再请求网络了直接播放本地文件了。既节省了流程又提高了加载速度。
今天我们就是来研究讨论实现这个边播边缓存的框架,因为它不和任何的业务逻辑耦合。

开源的项目

目前比较好的开源项目是:https://github.com/danikula/AndroidVideoCache

代码的架构写的也很不错,网络用的httpurlconnect,文件缓存处理,文件最大限度策略,回调监听处理,断点续传,代理服务等。很值得研究阅读.

个人觉得项目中有几个需要优化的点,今天就来处理这几个并简要分析下原理

优化点比如:

  1. 文件的缓存超过限制后没有按照lru算法删除,
  2. 处理返回给播放器的http响应头消息,响应头消息的获取处理改为head请求(需服务器支持)
  3. 替换网络库为okhttp(因为大部分的项目都是以okhttp为网络请求库的)

该开源项目的原理分析-本地代理

  1. 采用了本地代理服务的方式,通过原始url给播放器返回一个本地代理的一个url ,代理URL类似:http://127.0.0.1:57430/xxxx;然后播放器播放的时候请求到了你本地的代理上了。
  2. 本地代理采用ServerSocket监听127.0.0.1的有效端口,这个时候手机就是一个服务器了,客户端就是socket,也就是播放器。
  3. 读取客户端就是socket来读取数据(http协议请求)解析http协议。
  4. 根据url检查视频文件是否存在,读取文件数据给播放器,也就是往socket里写入数据。同时如果没有下载完成会进行断点下载,当然弱网的话数据需要生产消费同步处理。

优化点

1. 文件的缓存超过限制后没有按照lru算法删除.

Files类。

由于在移动设备上file.setLastModified() 方法不支持毫秒级的时间处理,导致超出限制大小后本应该删除老的,却没有删除抛出了异常。注释掉主动抛出的异常即可。因为文件的修改时间就是对的。

  static void setLastModifiedNow(File file) throws IOException {
    if (file.exists()) {
      long now = System.currentTimeMillis();
      boolean modified = file.setLastModified(now/1000*1000); // on some devices (e.g. Nexus 5) doesn't work
      if (!modified) {
        modify(file);
//        if (file.lastModified() < now) {
//          VideoCacheLog.debug("LruDiskUsage", "modified not ok ");
//          throw new IOException("Error set last modified date to " + file);
//        }else{
//          VideoCacheLog.debug("LruDiskUsage", "modified ok ");
//        }
      }
    }
  }

2. 处理返回给播放器的http响应头消息,响应头消息的获取处理改为head请求(需要服务器支持)

HttpUrlSource类。fetchContentInfo方法是获取视频文件的Content-Type,Content-Length信息,是为了播放器播放的时候给播放器组装http响应头信息用的。所以这一块需要用数据库保存,这样播放器每次播放的时候不要在此获取了,减少了请求的次数,节省了流量。既然是只需要头信息,不需要响应体,所以我们在获取的时候可以直接采用HEAD方法。所以代码增加了一个方法openConnectionForHeader如下:

 private void fetchContentInfo() throws ProxyCacheException {
    VideoCacheLog.debug(TAG,"Read content info from " + sourceInfo.url);
    HttpURLConnection urlConnection = null;
    InputStream inputStream = null;
    try {
      urlConnection = openConnectionForHeader(20000);
      long length = getContentLength(urlConnection);
      String mime = urlConnection.getContentType();
      inputStream = urlConnection.getInputStream();
      this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
      this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
      VideoCacheLog.debug(TAG,"Source info fetched: " + sourceInfo);
    } catch (IOException e) {
      VideoCacheLog.error(TAG,"Error fetching info from " + sourceInfo.url ,e);
    } finally {
      ProxyCacheUtils.close(inputStream);
      if (urlConnection != null) {
        urlConnection.disconnect();
      }
    }
  }

  // for HEAD
  private HttpURLConnection openConnectionForHeader(int timeout) throws IOException, ProxyCacheException {
    HttpURLConnection connection;
    boolean redirected;
    int redirectCount = 0;
    String url = this.sourceInfo.url;
    do {
      VideoCacheLog.debug(TAG, "Open connection for header to " + url);
      connection = (HttpURLConnection) new URL(url).openConnection();
      if (timeout > 0) {
        connection.setConnectTimeout(timeout);
        connection.setReadTimeout(timeout);
      }
      //只返回头部,不需要BODY,既可以提高响应速度也可以减少网络流量
      connection.setRequestMethod("HEAD");
      int code = connection.getResponseCode();
      redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER;
      if (redirected) {
        url = connection.getHeaderField("Location");
        VideoCacheLog.debug(TAG,"Redirect to:" + url);
        redirectCount++;
        connection.disconnect();
        VideoCacheLog.debug(TAG,"Redirect closed:" + url);
      }
      if (redirectCount > MAX_REDIRECTS) {
        throw new ProxyCacheException("Too many redirects: " + redirectCount);
      }
    } while (redirected);
    return connection;
  }

3.替换网络库为okhttp(因为大部分的项目都是以okhttp为网络请求库的)

为什么我们要换呢?!一是OKHttp是一款高效的HTTP客户端,支持连接同一地址的链接共享同一个socket,通过连接池来减小响应延迟,还有透明的GZIP压缩,请求缓存等优势,其核心主要有路由、连接协议、拦截器、代理、安全性认证、连接池以及网络适配,拦截器主要是指添加,移除或者转换请求或者回应的头部信息。得到了android开发的认可。二是大部分的app都是采用OKHttp,而且google会将其纳入android 源码中。三是该作者代码中用的httpurlconnet在HttpUrlSource有这么一段:

 @Override
  public void close() throws ProxyCacheException {
    if (connection != null) {
      try {
        connection.disconnect();
      } catch (NullPointerException | IllegalArgumentException e) {
        String message = "Wait... but why? WTF!? " +
            "Really shouldn't happen any more after fixing https://github.com/danikula/AndroidVideoCache/issues/43. " +
            "If you read it on your device log, please, notify me danikula@gmail.com or create issue here " +
            "https://github.com/danikula/AndroidVideoCache/issues.";
        throw new RuntimeException(message, e);
      } catch (ArrayIndexOutOfBoundsException e) {
        VideoCacheLog.error(TAG,"Error closing connection correctly. Should happen only on Android L. " +
            "If anybody know how to fix it, please visit https://github.com/danikula/AndroidVideoCache/issues/88. " +
            "Until good solution is not know, just ignore this issue :(", e);
      }
    }
  }

在没有像okhttp这些优秀的网络开源项目之前,android开发都是采用httpurlconnet或者httpclient,部分手机可能会遇到这个问题哈。

这里采用的 compile 'com.squareup.okhttp:okhttp:2.7.5' 版本的来实现该类的功能。在原作者的架构思路上我们只需要增加实现Source接口的类OkHttpUrlSource即可,可见作者的代码架构还是不错的,当然我们同样需要处理上文中提高的优化点2中的问题。将项目中所有用到HttpUrlSource的地方改为OkHttpUrlSource即可。
源码如下:

/**
 * ================================================
 * 作  者:顾修忠

 * 版  本:
 * 创建日期:2017/4/13-上午12:03
 * 描  述:在一些Android手机上HttpURLConnection.disconnect()方法仍然耗时太久,
 * 进行导致MediaPlayer要等待很久才会开始播放,因此决定使用okhttp替换HttpURLConnection
 */

public class OkHttpUrlSource implements Source {

  private static final String TAG = OkHttpUrlSource.class.getSimpleName();
  private static final int MAX_REDIRECTS = 5;
  private final SourceInfoStorage sourceInfoStorage;
  private SourceInfo sourceInfo;
  private OkHttpClient okHttpClient = new OkHttpClient();
  private Call requestCall = null;
  private InputStream inputStream;

  public OkHttpUrlSource(String url) {
    this(url, SourceInfoStorageFactory.newEmptySourceInfoStorage());
  }

  public OkHttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) {
    this.sourceInfoStorage = checkNotNull(sourceInfoStorage);
    SourceInfo sourceInfo = sourceInfoStorage.get(url);
    this.sourceInfo = sourceInfo != null ? sourceInfo :
        new SourceInfo(url, Integer.MIN_VALUE, ProxyCacheUtils.getSupposablyMime(url));
  }

  public OkHttpUrlSource(OkHttpUrlSource source) {
    this.sourceInfo = source.sourceInfo;
    this.sourceInfoStorage = source.sourceInfoStorage;
  }

  @Override
  public synchronized long length() throws ProxyCacheException {
    if (sourceInfo.length == Integer.MIN_VALUE) {
      fetchContentInfo();
    }
    return sourceInfo.length;
  }

  @Override
  public void open(long offset) throws ProxyCacheException {
    try {
      Response response = openConnection(offset, -1);
      String mime = response.header("Content-Type");
      this.inputStream = new BufferedInputStream(response.body().byteStream(), DEFAULT_BUFFER_SIZE);
      long length = readSourceAvailableBytes(response, offset, response.code());
      this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
      this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
    } catch (IOException e) {
      throw new ProxyCacheException("Error opening okHttpClient for " + sourceInfo.url + " with offset " + offset, e);
    }
  }

  private long readSourceAvailableBytes(Response response, long offset, int responseCode) throws IOException {
    long contentLength = getContentLength(response);
    return responseCode == HTTP_OK ? contentLength
        : responseCode == HTTP_PARTIAL ? contentLength + offset : sourceInfo.length;
  }

  private long getContentLength(Response response) {
    String contentLengthValue = response.header("Content-Length");
    return contentLengthValue == null ? -1 : Long.parseLong(contentLengthValue);
  }

  @Override
  public void close() throws ProxyCacheException {
    if (okHttpClient != null && inputStream != null && requestCall != null) {
      try {
        inputStream.close();
        requestCall.cancel();
      } catch (IOException e) {
        e.printStackTrace();
        throw new RuntimeException(e.getMessage(), e);
      }
    }
  }

  @Override
  public int read(byte[] buffer) throws ProxyCacheException {
    if (inputStream == null) {
      throw new ProxyCacheException("Error reading data from " + sourceInfo.url + ": okHttpClient is absent!");
    }
    try {
      return inputStream.read(buffer, 0, buffer.length);
    } catch (InterruptedIOException e) {
      throw new InterruptedProxyCacheException("Reading source " + sourceInfo.url + " is interrupted", e);
    } catch (IOException e) {
      throw new ProxyCacheException("Error reading data from " + sourceInfo.url, e);
    }
  }

  private void fetchContentInfo() throws ProxyCacheException {
    VideoCacheLog.debug(TAG, "Read content info from " + sourceInfo.url);
    Response response = null;
    InputStream inputStream = null;
    try {
      response = openConnectionForHeader(20000);
      if (response == null || !response.isSuccessful()) {
        throw new ProxyCacheException("Fail to fetchContentInfo: " + sourceInfo.url);
      }
      long length = getContentLength(response);
      String mime = response.header("Content-Type", "application/mp4");
      inputStream = response.body().byteStream();
      this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
      this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
      VideoCacheLog.info(TAG, "Content info for `" + sourceInfo.url + "`: mime: " + mime + ", content-length: " + length);
    } catch (IOException e) {
      VideoCacheLog.error(TAG, "Error fetching info from " + sourceInfo.url, e);
    } finally {
      ProxyCacheUtils.close(inputStream);
      if (response != null && requestCall != null) {
        requestCall.cancel();
      }
    }
  }

  // for HEAD
  private Response openConnectionForHeader(int timeout) throws IOException, ProxyCacheException {
    if (timeout > 0) {
//      okHttpClient.setConnectTimeout(timeout, TimeUnit.MILLISECONDS);
//      okHttpClient.setReadTimeout(timeout, TimeUnit.MILLISECONDS);
//      okHttpClient.setWriteTimeout(timeout, TimeUnit.MILLISECONDS);
    }
    Response response;
    boolean isRedirect = false;
    String newUrl = this.sourceInfo.url;
    int redirectCount = 0;
    do {
      //只返回头部,不需要BODY,既可以提高响应速度也可以减少网络流量
      Request request = new Request.Builder()
          .head()
          .url(newUrl)
          .build();
      requestCall = okHttpClient.newCall(request);
      response = requestCall.execute();
      if (response.isRedirect()) {
        newUrl = response.header("Location");
        VideoCacheLog.debug(TAG, "Redirect to:" + newUrl);
        isRedirect = response.isRedirect();
        redirectCount++;
        requestCall.cancel();
        VideoCacheLog.debug(TAG, "Redirect closed:" + newUrl);
      }
      if (redirectCount > MAX_REDIRECTS) {
        throw new ProxyCacheException("Too many redirects: " + redirectCount);
      }
    } while (isRedirect);

    return response;
  }

  private Response openConnection(long offset, int timeout) throws IOException, ProxyCacheException {
    if (timeout > 0) {
//      okHttpClient.setConnectTimeout(timeout, TimeUnit.MILLISECONDS);
//      okHttpClient.setReadTimeout(timeout, TimeUnit.MILLISECONDS);
//      okHttpClient.setWriteTimeout(timeout, TimeUnit.MILLISECONDS);
    }
    Response response;
    boolean isRedirect = false;
    String newUrl = this.sourceInfo.url;
    int redirectCount = 0;
    do {
      VideoCacheLog.debug(TAG, "Open connection" + (offset > 0 ? " with offset " + offset : "") + " to " + sourceInfo.url);
      Request.Builder requestBuilder = new Request.Builder()
          .get()
          .url(newUrl);
      if (offset > 0) {
        requestBuilder.addHeader("Range", "bytes=" + offset + "-");
      }
      requestCall = okHttpClient.newCall(requestBuilder.build());
      response = requestCall.execute();
      if (response.isRedirect()) {
        newUrl = response.header("Location");
        isRedirect = response.isRedirect();
        redirectCount++;
      }
      if (redirectCount > MAX_REDIRECTS) {
        throw new ProxyCacheException("Too many redirects: " + redirectCount);
      }
    } while (isRedirect);

    return response;
  }

  public synchronized String getMime() throws ProxyCacheException {
    if (TextUtils.isEmpty(sourceInfo.mime)) {
      fetchContentInfo();
    }
    return sourceInfo.mime;
  }

  public String getUrl() {
    return sourceInfo.url;
  }

  @Override
  public String toString() {
    return "OkHttpUrlSource{sourceInfo='" + sourceInfo + "}";
  }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 汇总Android视频录制中常见问题

    本文分享自己在视频录制播放过程中遇到的一些问题,主要包括: 视频录制流程 视频预览及SurfaceHolder 视频清晰度及文件大小 视频文件旋转 一.视频录制流程     以微信为例,其录制触发为按下(住)录制按钮,结束录制的触发条件为松开录制按钮或录制时间结束,其流程大概可以用下图来描述. 1.1.开始录制    根据上述流程及项目的编程惯例,可在onCreate()定义如下函数来完成功能: 初始化过程主要包括View,Data以及Listener三部分.在初始化View时,添加摄像头预览,

  • Android播放视频的三种方式

    在Android中,我们有三种方式来实现视频的播放: 1).使用其自带的播放器.指定Action为ACTION_VIEW,Data为Uri,Type为其MIME类型. 2).使用VideoView来播放.在布局文件中使用VideoView结合MediaController来实现对其控制. 3).使用MediaPlayer类和SurfaceView来实现,这种方式很灵活. 1.调用其自带的播放器: Uriuri = Uri.parse(Environment.getExternalStorageD

  • android使用videoview播放视频

    复制代码 代码如下: public class Activity01 extends Activity{ /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) {  super.onCreate(savedInstanceState); setContentView(R.layout.main); final VideoView vid

  • 详解Android App中使用VideoView来实现视频播放的方法

    通过VideoView播放视频的步骤: 1.在界面布局文件中定义VideoView组件,或在程序中创建VideoView组件 2.调用VideoView的如下两个方法来加载指定的视频 (1)setVidePath(String path):加载path文件代表的视频 (2)setVideoURI(Uri uri):加载uri所对应的视频 3.调用VideoView的start().stop().psuse()方法来控制视频的播放 VideoView通过与MediaController类结合使用,

  • Android播放assets文件里视频文件相关问题分析

    本文实例讲述了Android播放assets文件里视频文件相关问题.分享给大家供大家参考,具体如下: 今天做了一个功能,就是播放项目工程里面的视频文件,不是播放SD卡视频文件. 我开始尝试把视频文件放到 assets文件目录下. 因为之前写webview加载assets文件夹时,是这样写的: webView = new WebView(this); webView.loadUrl(file:///android_asset/sample3_8.html); 依次类推,我尝试将视频video.3g

  • Android使用MediaRecorder类进行录制视频

    我们提醒大家使用MediaRecorder录音录像的设置代码步骤一定要按照API指定的顺序来设置,否则报错 步骤为: 1.设置视频源,音频源,即输入源 2.设置输出格式 3.设置音视频的编码格式 一.首先看布局文件,这里有一个SurfaceView,这是一个绘制容器,可以直接从内存或者DMA等硬件接口取得图像数据, <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tool

  • android webvie指定视频播放器播放网站视频

    过滤掉其他的播放器,使用我自己的播放器来做 复制代码 代码如下: wv.setWebViewClient(new WebViewClient() {            public boolean shouldOverrideUrlLoading(final WebView view,                    final String url) { if (url.contains("3gp") || url.contains("mp4")) {/

  • Android视频点播的实现代码(边播边缓存)

    简述 一些知名的视频app客户端(优酷,爱奇艺)播放视频的时候都有一些缓存进度(二级进度缓存),还有一些短视频app,都有边播边缓的处理.还有就是当文件缓存完毕了再次播放的话就不再请求网络了直接播放本地文件了.既节省了流程又提高了加载速度. 今天我们就是来研究讨论实现这个边播边缓存的框架,因为它不和任何的业务逻辑耦合. 开源的项目 目前比较好的开源项目是:https://github.com/danikula/AndroidVideoCache 代码的架构写的也很不错,网络用的httpurlco

  • Android开发之滑动图片轮播标题焦点

    先给大家这是下效果图: 谷歌提供的v4包,ViewPager 在布局文件中,先添加<android.support.v4.view.ViewPager/>控件,这个只是轮播的区域 在布局文件中,布置标题描述部分 线性布局,竖向排列,背景色黑色半透明,这个布局和上面的ViewPager底部对齐layout_alignBottom="@id/xxx" <TextView/>居中显示, 小点部分,先放过空的LinearLayout,id是ll_points在代码中对其

  • Android Viewpager实现无限循环轮播图

    在网上找了很多viewpager实现图片轮播的,但是大多数通过以下方式在PagerAdapter的getCount()返回一个无限大的数,来实现 伪无限 @Override public int getCount() { return Integer.MAX_VALUE;//返回一个无限大的值,可以 无限循环 } 虽然通过这种方式是能达到效果,但是从严格意义上来说并不是真正的无限. 假如有五张轮播图 item的编号为(0,1,2,3,4) 要想实现 无限循环  我们在这五张的头部和尾部各加一张即

  • Android设置铃声实现代码

    本文实例讲述了Android设置铃声实现代码.分享给大家供大家参考.具体如下: public void setMyRingtone(File file) { ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath()); // values.put(MediaStore.MediaColumns.TITLE, file.getName());

  • Android 自定义状态栏实例代码

    一.目标:Android5.0以上 二.步骤 1.在res-values-colors.xml下新建一个RGB颜色 <?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#3F51B5</color> <color name="colorPrimaryDark">#3

  • 基于Android实现转盘按钮代码

    先给大家展示下效果图: package com.lixu.circlemenu; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.TextView; import android.widget.Toast; import com.lixu.circlemenu.view.CircleImageView; import com.lixu.ci

  • Android 日期选择器实例代码

    废话不多说了,直接给大家贴代码了,具体代码如下所示: //出生年月设置 private void birthSetting() { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_YEAR, 1); new DatePickerDialog(mContext, new DatePickerDialog.OnDateSetListener() { @Override public void onDat

  • Android 滑动拦截实例代码解析

    废话不多说了,直接给大家贴代码了,具体代码如下所示: package demo.hq.com.fby; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout; /** * Created by huqing on 2016/12/7.

  • Android 验证码功能实现代码

    先给大家展示下效果图,如果大家感觉还不错,请参考实现代码 很简单的一个例子,点击刷新验证码,刷新当前显示的验证码,点击确定,如果输入的和显示的匹配,就会跳转到下一个界面中,这里只是实现了跳转,并没有进行其它的操作 好了 接下来就是代码了 首先看MainActivity的布局 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://sche

  • Android支付宝支付封装代码

    在做Android支付的时候肯定会用到支付宝支付, 根据官方给出的demo做起来非常费劲,所以我们需要一次简单的封装. 封装的代码也很简单,就是将官网给的demo提取出一个类来方便使用. public class Alipay { // 商户PID public static final String PARTNER = "123456789"; // 商户收款账号 public static final String SELLER = "qibin0506@gmail.co

随机推荐