一种视频预加载的方案

Posted 码农突围

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一种视频预加载的方案相关的知识,希望对你有一定的参考价值。

转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/54234583

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

  • 预加载的形式
  • 影响预加载的因素
  • 预加载场景
  • 一张图看清本地代理
  • 数据预加载
  • 效果图
  • HttpProxy

预加载的形式:

  • 1.边存边播:下载多少播放多少。
    优点:快速加载播放,实现简单;缺点:不能拖动未存区域;适合音频媒体
  • 2.代理服务器:预先下载媒体的头部(头部Size为 s1 byte)->监听播放器的请求,当Request的是预加载的URL->代理把媒体头部作为Response返回给播放器,并改Ranage 为 s1 byte 发送Request->代理服务器纯粹作为透传。
    优点:快速加载播放,支持拖动;缺点:实现非常复杂;适合视频媒体

影响预加载的因素:

  • 网络状态
  • 缓冲文件大小
  • 视频码率

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

预加载场景:

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

一张图看清本地代理:

这里写图片描述

数据预加载:

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

效果图1:


这里写图片描述

效果图2:

这里写图片描述


这里写图片描述

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

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

package com.hejunlin.videopreloaded;
import android.util.Log;

public class HttpProxy {
    public static final int SIZE = (int) (5 * 1024 * 1024);
    public static final String TAG = HttpProxy.class.getSimpleName();
    private int remotePort = -1;
    private String remoteHost;
    private int localPort;
    private String localHost;
    private ServerSocket localServer = null;
    private Socket sckPlayer = null;
    private Socket sckServer = null;
    private SocketAddress serverAddress;

    private HandleDownLoad download = null;
    /**
     * 初始化代理服务器
     *
     * @param localport
     *            代理服务器监听的端口
     */
    public HttpProxy(int localport) {
        try {
            localPort = localport;
            localHost = Contants.LOCAL_IP_ADDRESS;
            localServer = new ServerSocket(localport, 1,
                    InetAddress.getByName(localHost));
        } catch (Exception e) {
            System.exit(0);
        }
            URI tmpURI = new URI(urlString);
        String fileName = Utils.urlToFileName(tmpURI.getPath());
        String filePath = Contants.getBufferDir() + "/" + fileName;
        download = new HandleDownLoad(urlString, filePath, size);
        download.startThread();
        return filePath;
    }
    /**
     * 把网络URL转为本地URL,127.0.0.1替换网络域名
     *
     * @param url网络URL
     * @return [0]:重定向后MP4真正URL,[1]:本地URL
     */
    public String[] getLocalURL(String urlString) {

        String targetUrl = Utils.getRedirectUrl(urlString);
        // ----获取对应本地代理服务器的链接----//
        String localUrl = null;
        URI originalURI = URI.create(targetUrl);
        remoteHost = originalURI.getHost();
        if (originalURI.getPort() != -1) {// URL带Port
            serverAddress = new InetSocketAddress(remoteHost,
                    originalURI.getPort());// 使用默认端口
            remotePort = originalURI.getPort();// 保存端口,中转时替换
            localUrl = targetUrl.replace(
                    remoteHost + ":" + originalURI.getPort(), localHost + ":"
                            + localPort);
        } else {// URL不带Port
            serverAddress = new InetSocketAddress(remoteHost, Contants.HTTP_PORT);// 使用80端口
            remotePort = -1;
            localUrl = targetUrl.replace(remoteHost, localHost + ":"
                    + localPort);
        }
        String[] result = new String[] { targetUrl, localUrl };
        return result;
    }
    /**
     * 异步启动代理服务器
     *
     * @throws IOException
     */
    public void asynStartProxy() {
        new Thread() {
            public void run() {
                startProxy();
            }
        }.start();
    }
    private void startProxy() {
        HttpParser httpParser = null;
        HttpProxyUtils utils = null;
        int bytes_read;
        byte[] local_request = new byte[1024];
        byte[] remote_reply = new byte[1024];
        while (true) {
            boolean sentResponseHeader = false;
            try {// 开始新的request之前关闭过去的Socket
                if (sckPlayer != null)
                    sckPlayer.close();
                if (sckServer != null)
                    sckServer.close();
            } catch (IOException e1) {
            }
            try {
                // --------------------------------------
                // 监听MediaPlayer的请求,MediaPlayer->代理服务器
                // --------------------------------------
                sckPlayer = localServer.accept();
                Log.e(TAG,
                        "------------------------------------------------------------------");
                if (download != null && download.isDownloading())
                    download.stopThread(false);
                httpParser = new HttpParser(remoteHost, remotePort, localHost,
                        localPort);
                utils = new HttpProxyUtils(sckPlayer, sckServer,
                        serverAddress);
                HttpParser.ProxyRequest request = null;
                while ((bytes_read = sckPlayer.getInputStream().read(
                        local_request)) != -1) {
                    byte[] buffer = httpParser.getRequestBody(local_request,
                            bytes_read);
                    if (buffer != null) {
                        request = httpParser.getProxyRequest(buffer);
                        break;
                    }
                }
                boolean isExists = new File(request._prebufferFilePath)
                        .exists();
                if (isExists)
                    Log.e(TAG, ">> prebuffer size:" + download.getDownloadedSize());
                sckServer = utils.sentToServer(request._body);
                // ------------------------------------------------------
                // 把网络服务器的反馈发到MediaPlayer,网络服务器->代理服务器->MediaPlayer
                // ------------------------------------------------------
                while ((bytes_read = sckServer.getInputStream().read(
                        remote_reply)) != -1) {
                    if (sentResponseHeader) {
                        try {// 拖动进度条时,容易在此异常,断开重连
                            utils.sendToMP(remote_reply, bytes_read);
                        } catch (Exception e) {
                            break;// 发送异常直接退出while
                        }
                        continue;// 退出本次while
                    }
                    List<byte[]> httpResponse = httpParser.getResponseBody(
                            remote_reply, bytes_read);
                    if (httpResponse.size() == 0)
                        continue;// 没Header则退出本次循环
                    sentResponseHeader = true;
                    String responseStr = new String(httpResponse.get(0));
                    Log.e(TAG, ">> responseStr " + responseStr);
                    // send http header to mediaplayer
                    utils.sendToMP(httpResponse.get(0));
                    if (isExists) {// 需要发送预加载到MediaPlayer
                        isExists = false;
                        int sentBufferSize = 0;
                        try {
                            sentBufferSize = utils.sendPrebufferToMP(
                                    request._prebufferFilePath,
                                    request._rangePosition);
                        } catch (Exception ex) {
                            break;
                        }
                        if (sentBufferSize > 0) {// 成功发送预加载,重新发送请求到服务器
                            int newRange = (int) (sentBufferSize + request._rangePosition);
                            String newRequestStr = httpParser
                                    .modifyRequestRange(request._body, newRange);
                            Log.e(TAG + "-pre->", newRequestStr);
                            // 修改Range后的Request发送给服务器
                            sckServer = utils.sentToServer(newRequestStr);
                            // 把服务器的Response的Header去掉
                            utils.removeResponseHeader(httpParser);
                            continue;
                        }
                    }
                    // 发送剩余数据
                    if (httpResponse.size() == 2) {
                        utils.sendToMP(httpResponse.get(1));
                    }
                }
                Log.e(TAG, ">> preloaded over");
                // 关闭 2个SOCKET
                sckPlayer.close();
                sckServer.close();
            } catch (Exception e) {
                Log.e(TAG, e.toString());
                Log.e(TAG, Utils.getExceptionMessage(e));
            }
        }
    }
}

第一时间获得博客更新提醒,以及更多android干货,源码分析,欢迎关注我的微信公众号,扫一扫下方二维码或者长按识别二维码,即可关注。

这里写图片描述

如果你觉得好,随手点赞,也是对笔者的肯定,也可以分享此公众号给你更多的人,原创不易

以上是关于一种视频预加载的方案的主要内容,如果未能解决你的问题,请参考以下文章

进度条(视频预加载器)

分享前端开发常用代码片段

收藏|分享前端开发常用代码片段

在 Android 应用程序开发中预加载或预缓冲 .mp4 视频

js 常用代码片段

关于js----------------分享前端开发常用代码片段