Android 边播边缓存 (MP4 未加密m3u8)

1,985 阅读2分钟

实现思路

红色框的 ProxyServer就是需要实现的一个代理服务器。 当客户端拿到一个视频的url(mp4或者m3u8)时,通过proxyServer转化为一个代理的url,然后请求代理服务器;代理服务器接收到客户端的请求后,先查看本地是否存在缓存,如果不存在则向真实服务器发送请求,拿到结果后再存到本地。

实现重点

缓存是一个代理服务器的主要部分,所以这部分是一个重点。本设计的缓存是一个分片的LRU缓存。分片的好处是灵活方便做LRU。当真实服务器返回一个大文件时,我们在进行切割后缓存在本地,并返回给客户端,不用等待所有数据返回后再返回给客户端。

使用方式

在app初始化的时候 创建代理服务器

public class APP extends Application {
    private static VideoCacheServer videoCacheServer;

    @Override
    public void onCreate() {
        super.onCreate();

        if (videoCacheServer == null) {
           // 缓存路径
            String cachePath = getCacheDir().getAbsolutePath();
            // 缓存大小 1024 * 1024 * 500
            videoCacheServer = new VideoCacheServer(cachePath, 1024 * 1024 * 500);
        }
    }

    public static VideoCacheServer getVideoProxyServer() {
        return videoCacheServer;
    }

}

代理服务建立好了 ,使用的时候只需要将真实url转换为代理url就好了

String proxyUrl = APP.getVideoProxyServer().getLocalProxyUrl("https://sina.com-h-sina.com/20181024/21342_8f737b71/1000k/hls/index.m3u8");
 videoView.setVideoPath(proxyUrl);

转换的规则 是将https的请求转换为http的请求 ,并且替换域名为代理服务器的地址,将真实服务器的地址作为参数添加到代理url的后面。

例如 sina.com-h-sina.com/20181024/21… 地址转换后变成了 https://127.0.0.1:3260/20181024/21342_8f737b71/1000k/hls/index.m3u8?RealHostParam=sina.com-h-sina.com 3260是VideoCacheServer监听的端口

实现细节

代理服务器的建立

public class VideoCacheServer{

	private ExecutorService pool = Executors.newFixedThreadPool(20);
 
 	public int start() {
        if (isRunning) {
            return curPort;
        }
        curPort = new Random().nextInt(65535);
        try {
            final ServerSocket server = new ServerSocket(curPort);
            isRunning = true;
            singleService.submit(new Runnable() {
                @Override
                public void run() {
                    while (isRunning) {
                        try {
                            Socket connection = server.accept();
                            connection.setKeepAlive(true);
                            pool.submit(new ProxyHandler(connection));
                        } catch (IOException ex) {
                            if (Constant.enableLog) {
                                logger.log(Level.WARNING, "Exception accepting connection", ex);
                            }
                        } catch (Exception ex) {
                            if (Constant.enableLog) {
                                logger.log(Level.SEVERE, "Unexpected error", ex);
                            }
                        }
                    }
                }
            });
            return curPort;
        } catch (IOException e) {
            e.printStackTrace();
            return start();
        }
    }
}

通过socket实现端口的监听,当请求到来时,使用ProxyHandler来处理。

 public class ProxyHandler implements Runnable {

        private Socket realClientSocket;

        ProxyHandler(Socket realClientSocket) {
            this.realClientSocket = realClientSocket;
        }

        @Override
        public void run() {
            try {
                BufferedOutputStream outputStream = new BufferedOutputStream(realClientSocket.getOutputStream());
                BufferedInputStream inputStream = new BufferedInputStream(realClientSocket.getInputStream());
                HttpRequest realRequest = HttpRequest.parse(inputStream);
                HttpResponse response = getResponseWithInterceptorChain(realRequest);
                writeResponseAndClose(response, outputStream);

            } catch (Exception e) {
                if (Constant.enableLog) {
                    logger.log(Level.SEVERE, "error proxy ", e);
                }
            } finally {
                CloseUtil.close(realClientSocket);
            }
        }

        private HttpResponse getResponseWithInterceptorChain(HttpRequest realRequest) {
            List<Interceptor> interceptors = new ArrayList<>();
            interceptors.add(new VideoTypeInterceptor());
            interceptors.add(new HostFilterInterceptor(curPort));
            interceptors.add(new CacheInterceptor(diskCache));
            interceptors.add(new ConnectInterceptor());
            InterceptorChain interceptorChain = new InterceptorChain(interceptors, realRequest, 0);
            return interceptorChain.proceed(realRequest);
        }
}

ProxyHandler中使用拦截器的模式,将请求分部处理

HostFilterInterceptor 将代理的url 还原为真实的url
CacheInterceptor 用于缓存
ConnectInterceptor 建立代理服务器与真实服务器的连接
VideoTypeInterceptor 主要是针对m3u8类型,因为m3u8会先返回一个m3u8的文件,文件里面记录了每个ts的地址,VideoTypeInterceptor就是将返回的文件中的ts地址转换为代理服务器的地址

项目地址 github.com/ZhangHao555…