前言:视频的预加载是提高用户体验的重要因素。预加载成为网络视频播放不可或缺的一个技术环节。

 

预加载的形式:

1.边存边播:下载多少播放多少。

优点:快速加载播放,实现简单;缺点:不能拖动未存区域;适合音频媒体

2.代理服务器:预先下载媒体的头部(头部Size为 s1 byte)->监听播放器的请求,当Request的是预加载的URL->代理把媒体头部作为Response返回给播放器,并改Ranage 为 s1 byte 发送Request->代理服务器纯粹作为透传。

优点:快速加载播放,支持拖动;缺点:实现非常复杂;适合视频媒体

 

 

影响预加载的因素:

  • 网络状态

  • 缓冲文件大小

  • 视频码率

 

码率低、网速快的情况没必要使用预加载,码率中等、网速一般的情况合适使用。另外,缓冲文件也不能设置太大:过大的缓冲区会刷爆MediaPlayer内置的缓冲区,影响正常播放;再者,读取缓冲文件也耗时。

 

预加载场景:

电视剧播放第1集时,在快要结束前5分钟,开始加载第2集。播放时就会连贯起来。缩短加载时间,如当系统检测您的网络环境较差时,会提示您开启预加载模式,开启预加载模式后,系统将为您缓冲整部影片。为了您流畅观看视频,建议您在开启预加载模式5分钟后,再次点击播放按钮,开始播放。这时就不会卡在缓冲中的状态。就是在视频播放的时候,在播放器通过本地URL,请求视频数据时,本地代理截取这次请求,经过本地代理逻辑,向服务器或本地缓存请求数据。本地代理在获得视频数据后,将数据转给播放器,实现播放。相比直接请求视频数据,本地代理的优势在于,buffer由本地控制,提大提升视频播放速度。提高用户体验。

 

一张图看清本地代理:

一种视频预加载的方案_JAVA

数据预加载:

不同rom上的MediaPlayer是不同的,会有一些差异,取决rom多媒体团队对MediaPlayer的定制程度,例如:有些MediaPlayer首次播放从头buffer,有些MdiaPlayer首次播放会多次Request,Range到网络媒体文件的头部、中间和文件尾,再从指定位置buffer。系统播放器需要下载5s的数据才开始把buffer进行播放出来。5s的数据,如果网络差的话,就处于是buffering中,一旦有5s的数据,就先播起来,而后,再在背后预加载,如一般播放panel的seekbar,有两种颜色,一种颜色(下图蓝色)是当前正在播的位置,还是一种颜色(灰色)走在前面,就是加载好的数据。

一种视频预加载的方案_JAVA_02

 

效果图1:

一种视频预加载的方案_JAVA_03一种视频预加载的方案_JAVA_04

 

一种视频预加载的方案_JAVA_05

效果图2:

一种视频预加载的方案_JAVA_03一种视频预加载的方案_JAVA_07

 

实现思路及部分代码:

MainActivity.用VideoView进行视频播放,url的视频为【鬼吹灯之精绝古城第1集】,会有一个开关,决定是否预加载。默认打开。

 

HttpProxy,就是启动一个本地代理,用127.0.0.1来替换视频源的服务器地址。

  1. package com.hejunlin.videopreloaded;

  2. import android.util.Log;

  3.  

  4. public class HttpProxy {

  5.    public static final int SIZE = (int) (5 * 1024 * 1024);

  6.    public static final String TAG = HttpProxy.class.getSimpleName();

  7.    private int remotePort = -1;

  8.    private String remoteHost;

  9.    private int localPort;

  10.    private String localHost;

  11.    private ServerSocket localServer = null;

  12.    private Socket sckPlayer = null;

  13.    private Socket sckServer = null;

  14.    private SocketAddress serverAddress;

  15.  

  16.    private HandleDownLoad download = null;

  17.    /**

  18.     * 初始化代理服务器

  19.     *

  20.     * @param localport

  21.     *            代理服务器监听的端口

  22.     */

  23.    public HttpProxy(int localport) {

  24.        try {

  25.            localPort = localport;

  26.            localHost = Contants.LOCAL_IP_ADDRESS;

  27.            localServer = new ServerSocket(localport, 1,

  28.                    InetAddress.getByName(localHost));

  29.        } catch (Exception e) {

  30.            System.exit(0);

  31.        }

  32.            URI tmpURI = new URI(urlString);

  33.        String fileName = Utils.urlToFileName(tmpURI.getPath());

  34.        String filePath = Contants.getBufferDir() + "/" + fileName;

  35.        download = new HandleDownLoad(urlString, filePath, size);

  36.        download.startThread();

  37.        return filePath;

  38.    }

  39.    /**

  40.     * 把网络URL转为本地URL,127.0.0.1替换网络域名

  41.     *

  42.     * @param url网络URL

  43.     * @return [0]:重定向后MP4真正URL,[1]:本地URL

  44.     */

  45.    public String[] getLocalURL(String urlString) {

  46.        

  47.        String targetUrl = Utils.getRedirectUrl(urlString);

  48.        // ----获取对应本地代理服务器的链接----//

  49.        String localUrl = null;

  50.        URI originalURI = URI.create(targetUrl);

  51.        remoteHost = originalURI.getHost();

  52.        if (originalURI.getPort() != -1) {// URL带Port

  53.            serverAddress = new InetSocketAddress(remoteHost,

  54.                    originalURI.getPort());// 使用默认端口

  55.            remotePort = originalURI.getPort();// 保存端口,中转时替换

  56.            localUrl = targetUrl.replace(

  57.                    remoteHost + ":" + originalURI.getPort(), localHost + ":"

  58.                            + localPort);

  59.        } else {// URL不带Port

  60.            serverAddress = new InetSocketAddress(remoteHost, Contants.HTTP_PORT);// 使用80端口

  61.            remotePort = -1;

  62.            localUrl = targetUrl.replace(remoteHost, localHost + ":"

  63.                    + localPort);

  64.        }

  65.        String[] result = new String[] { targetUrl, localUrl };

  66.        return result;

  67.    }

  68.    /**

  69.     * 异步启动代理服务器

  70.     *

  71.     * @throws IOException

  72.     */

  73.    public void asynStartProxy() {

  74.        new Thread() {

  75.            public void run() {

  76.                startProxy();

  77.            }

  78.        }.start();

  79.    }

  80.    private void startProxy() {

  81.        HttpParser httpParser = null;

  82.        HttpProxyUtils utils = null;

  83.        int bytes_read;

  84.        byte[] local_request = new byte[1024];

  85.        byte[] remote_reply = new byte[1024];

  86.        while (true) {

  87.            boolean sentResponseHeader = false;

  88.            try {// 开始新的request之前关闭过去的Socket

  89.                if (sckPlayer != null)

  90.                    sckPlayer.close();

  91.                if (sckServer != null)

  92.                    sckServer.close();

  93.            } catch (IOException e1) {

  94.            }

  95.            try {

  96.                // --------------------------------------

  97.                // 监听MediaPlayer的请求,MediaPlayer->代理服务器

  98.                // --------------------------------------

  99.                sckPlayer = localServer.accept();

  100.                Log.e(TAG,

  101.                        "------------------------------------------------------------------");

  102.                if (download != null && download.isDownloading())

  103.                    download.stopThread(false);

  104.                httpParser = new HttpParser(remoteHost, remotePort, localHost,

  105.                        localPort);

  106.                utils = new HttpProxyUtils(sckPlayer, sckServer,

  107.                        serverAddress);

  108.                HttpParser.ProxyRequest request = null;

  109.                while ((bytes_read = sckPlayer.getInputStream().read(

  110.                        local_request)) != -1) {

  111.                    byte[] buffer = httpParser.getRequestBody(local_request,

  112.                            bytes_read);

  113.                    if (buffer != null) {

  114.                        request = httpParser.getProxyRequest(buffer);

  115.                        break;

  116.                    }

  117.                }

  118.                boolean isExists = new File(request._prebufferFilePath)

  119.                        .exists();

  120.                if (isExists)

  121.                    Log.e(TAG, ">> prebuffer size:" + download.getDownloadedSize());

  122.                sckServer = utils.sentToServer(request._body);

  123.                // ------------------------------------------------------

  124.                // 把网络服务器的反馈发到MediaPlayer,网络服务器->代理服务器->MediaPlayer

  125.                // ------------------------------------------------------

  126.                while ((bytes_read = sckServer.getInputStream().read(

  127.                        remote_reply)) != -1) {

  128.                    if (sentResponseHeader) {

  129.                        try {// 拖动进度条时,容易在此异常,断开重连

  130.                            utils.sendToMP(remote_reply, bytes_read);

  131.                        } catch (Exception e) {

  132.                            break;// 发送异常直接退出while

  133.                        }

  134.                        continue;// 退出本次while

  135.                    }

  136.                    List<byte[]> httpResponse = httpParser.getResponseBody(

  137.                            remote_reply, bytes_read);

  138.                    if (httpResponse.size() == 0)

  139.                        continue;// 没Header则退出本次循环

  140.                    sentResponseHeader = true;

  141.                    String responseStr = new String(httpResponse.get(0));

  142.                    Log.e(TAG, ">> responseStr " + responseStr);

  143.                    // send http header to mediaplayer

  144.                    utils.sendToMP(httpResponse.get(0));

  145.                    if (isExists) {// 需要发送预加载到MediaPlayer

  146.                        isExists = false;

  147.                        int sentBufferSize = 0;

  148.                        try {

  149.                            sentBufferSize = utils.sendPrebufferToMP(

  150.                                    request._prebufferFilePath,

  151.                                    request._rangePosition);

  152.                        } catch (Exception ex) {

  153.                            break;

  154.                        }

  155.                        if (sentBufferSize > 0) {// 成功发送预加载,重新发送请求到服务器

  156.                            int newRange = (int) (sentBufferSize + request._rangePosition);

  157.                            String newRequestStr = httpParser

  158.                                    .modifyRequestRange(request._body, newRange);

  159.                            Log.e(TAG + "-pre->", newRequestStr);

  160.                            // 修改Range后的Request发送给服务器

  161.                            sckServer = utils.sentToServer(newRequestStr);

  162.                            // 把服务器的Response的Header去掉

  163.                            utils.removeResponseHeader(httpParser);

  164.                            continue;

  165.                        }

  166.                    }

  167.                    // 发送剩余数据

  168.                    if (httpResponse.size() == 2) {

  169.                        utils.sendToMP(httpResponse.get(1));

  170.                    }

  171.                }

  172.                Log.e(TAG, ">> preloaded over");

  173.                // 关闭 2个SOCKET

  174.                sckPlayer.close();

  175.                sckServer.close();

  176.            } catch (Exception e) {

  177.                Log.e(TAG, e.toString());

  178.                Log.e(TAG, Utils.getExceptionMessage(e));

  179.            }

  180.        }

  181.    }

  182. }

HttpPaser就是解析真实地址的类,这里就不再给出。https://mp.weixin.qq.com/s/upYoLy1HUdeB6XM3WG2I6w