开源视频播放框架:AndroidVideoCache
github:AndroidVideoCache
该框架的思想就是在本地构建一个ServerSocket作为代理服务器,将对Mp4地址进行封装,从而拦截到本地ServerSocket,拦截之后解析出url和请求头进行真正的网络请求。而视频播放控件例如MediaPlay、VideoView就相当于客户端了,最后将真正的网络请求通过Socket的方式写入到客户端,这样视频控件就可以播放了。
具体如何使用可以直接看github上面的连接,作者讲的很详细,本篇文章主要是分析一下源码。
原理图看不懂没关系,看代码:
Builder类
public static final class Builder { private static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024; private File cacheRoot; private FileNameGenerator fileNameGenerator; private DiskUsage diskUsage; private SourceInfoStorage sourceInfoStorage; private HeaderInjector headerInjector; public Builder(Context context) { this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context); this.cacheRoot = StorageUtils.getInpidualCacheDirectory(context); this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE); this.fileNameGenerator = new Md5FileNameGenerator(); this.headerInjector = new EmptyHeadersInjector(); } }StorageUtils.getInpidualCacheDirectory(context):主要是视频文件下载的目录,这里作者分析的很全面,以后写项目可以应用到。 SourceInfoStorageFactory.newSourceInfoStorage(context);:主要是数据库,对url对应的信息进行存储,包括url地址、视频的长度、mime信息。
这里分两种url:
a:一种是作为ping的url,类似于心跳包。格式为:http://127.0.01:port/ping 主要是查看本地是否能ping通。
b:另一种是作为真正的网络请求的url,也就是拦截的url。格式为: http://127.0.01:port/url。 TotalSizeLruDiskUsage:主要对文件进行lrucache算法,将下载的视频进行lastmodify排序,当超过规定的内存大小之后,对之前下载的视频进行删除操作。 Md5FileNameGenerator:对视频的url进行md5算法,用于生成文件名。 EmptyHeadersInjector:用于添加请求的header。
设置直连代理
private static final String PROXY_HOST = "127.0.0.1"; InetAddress inetAddress = InetAddress.getByName(PROXY_HOST); this.serverSocket = new ServerSocket(0, 8, inetAddress); this.port = serverSocket.getLocalPort(); IgnoreHostProxySelector.install(PROXY_HOST, port);
定义一个host:127.0.0.1
并且生成一个匿名端口号
class IgnoreHostProxySelector extends ProxySelector { private static final ListNO_PROXY_LIST = Arrays.asList(Proxy.NO_PROXY); private final ProxySelector defaultProxySelector; private final String hostToIgnore; private final int portToIgnore; IgnoreHostProxySelector(ProxySelector defaultProxySelector, String hostToIgnore, int portToIgnore) { this.defaultProxySelector = checkNotNull(defaultProxySelector); this.hostToIgnore = checkNotNull(hostToIgnore); this.portToIgnore = portToIgnore; } static void install(String hostToIgnore, int portToIgnore) { ProxySelector defaultProxySelector = ProxySelector.getDefault(); ProxySelector ignoreHostProxySelector = new IgnoreHostProxySelector(defaultProxySelector, hostToIgnore, portToIgnore); ProxySelector.setDefault(ignoreHostProxySelector); } @Override public List select(URI uri) { boolean ignored = hostToIgnore.equals(uri.getHost()) && portToIgnore == uri.getPort(); return ignored ? NO_PROXY_LIST : defaultProxySelector.select(uri); } @Override public void connectFailed(URI uri, SocketAddress address, IOException failure) { defaultProxySelector.connectFailed(uri, address, failure); } }
主要是选择判断选择的代理,如果是以127.0.0.1并且是ServerSocket生成的端口号则采用直连的方式,其他的url和端口号使用系统默认的请求代理。
生成客户端Socket
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal)); this.waitConnectionThread.start(); private final class WaitRequestsRunnable implements Runnable { private final CountDownLatch startSignal; public WaitRequestsRunnable(CountDownLatch startSignal) { this.startSignal = startSignal; } @Override public void run() { startSignal.countDown(); waitForRequest(); } } } private void waitForRequest() { try { while (!Thread.currentThread().isInterrupted()) { Socket socket = serverSocket.accept(); socketProcessor.submit(new SocketProcessorRunnable(socket)); } } catch (IOException e) { onError(new ProxyCacheException("Error during waiting connection", e)); } }
生成了一个Socket,用于将视频的流写入到客户端(所谓的MediaPlayer、VideoView)
查看本地心跳包是否ping通
当我们使用这个框架时作者提供了一个这个方法String proxyUrl = proxy.getProxyUrl(url);
videoView.setVideoPath(proxyUrl);
videoView.start();
生成了一个代理url
public String getProxyUrl(String url, boolean allowCachedFileUri) { if (allowCachedFileUri && isCached(url)) { File cacheFile = getCacheFile(url); touchFileSafely(cacheFile); return Uri.fromFile(cacheFile).toString(); } return isAlive() ? appendToProxyUrl(url) : url; }
进入到这个方法,首先查看这个文件是否下载完毕,如果下载完毕就返回本地的url地址,可以离线播放。
如果本地文件没有下载完毕,则进入isAlive()
boolean ping(int maxAttempts, int startTimeout) { checkArgument(maxAttempts >= 1); checkArgument(startTimeout > 0); int timeout = startTimeout; int attempts = 0; while (attempts < maxAttempts) { try { Future pingFuture = pingExecutor.submit(new PingCallable()); boolean pinged = pingFuture.get(timeout, MILLISECONDS); if (pinged) { return true; } } catch (TimeoutException e) { LOG.warn("Error pinging server (attempt: " + attempts + ", timeout: " + timeout + "). "); } catch (InterruptedException | ExecutionException e) { LOG.error("Error pinging server due to unexpected error", e); } attempts++; timeout *= 2; } String error = String.format(Locale.US, "Error pinging server (attempts: %d, max timeout: %d). " + "If you see this message, please, report at https://github.com/danikula/AndroidVideoCache/issues/134. " + "Default proxies are: %s" , attempts, timeout / 2, getDefaultProxies()); LOG.error(error, new ProxyCacheException(error)); return false; }
首先利用pingExecutor开辟了一个线程池
private class PingCallable implements Callable { @Override public Boolean call() throws Exception { return pingServer(); } }
private boolean pingServer() throws ProxyCacheException { String pingUrl = getPingUrl(); HttpUrlSource source = new HttpUrlSource(pingUrl); try { byte[] expectedResponse = PING_RESPONSE.getBytes(); source.open(0); byte[] response = new byte[expectedResponse.length]; source.read(response); boolean pingOk = Arrays.equals(expectedResponse, response); LOG.info("Ping response: `" + new String(response) + "`, pinged? " + pingOk); return pingOk; } catch (ProxyCacheException e) { LOG.error("Error reading ping response", e); return false; } finally { source.close(); } }
这里
getPingUrl() 生成了一个http://127.0.0.1:port/ping的url地址。
接着创建了一个HttpUrlSource的对象,这个对象主要是处理网络请求有关的。
跳入了一个只有url参数的构造函数
public HttpUrlSource(String url) { this(url, SourceInfoStorageFactory.newEmptySourceInfoStorage()); }
注意这里面使用的是
SourceInfoStorageFactory.newEmptySourceInfoStorage()
public static SourceInfoStorage newEmptySourceInfoStorage() { return new NoSourceInfoStorage(); }
public class NoSourceInfoStorage implements SourceInfoStorage { @Override public SourceInfo get(String url) { return null; } @Override public void put(String url, SourceInfo sourceInfo) { } @Override public void release() { } }
这个数据库对象是一个空实现,也就是对http://127.0.0.1:port/ping这种连接的地址没有必要去存储他们的信息。
byte[] expectedResponse = PING_RESPONSE.getBytes();
生成了ping ok的字节数组source.open(0);
打开一个HttpURLConnection。
具体代码:
@Override public void open(long offset) throws ProxyCacheException { try { connection = openConnection(offset, -1); String mime = connection.getContentType(); inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE); long length = readSourceAvailableBytes(connection, offset, connection.getResponseCode()); this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime); this.sourceInfoStorage.put(sourceInfo.url, sourceInfo); } catch (IOException e) { throw new ProxyCacheException("Error opening connection for " + sourceInfo.url + " with offset " + offset, e); } }
困了,先写到这,我先发布了,爱咋咋地吧。
推荐阅读
-
超级简单的跨平台高性能音视频播放框架QtAv编译指南
-
开源视频播放框架:AndroidVideoCache
-
开源视频播放框架学习——AndroidVideoCache
-
利用AJAX开源项目 在网页里播放视频实现方法
-
Clappr——开源的Web视频播放器
-
用什么PHP框架最好?框架?还不如用开源系统吧 php框架是什么 laravel视频教程 php框架下载
-
超级简单的跨平台高性能音视频播放框架QtAv编译指南
-
开源视频播放框架:AndroidVideoCache
-
利用AJAX开源项目 在网页里播放视频实现方法
-
用什么PHP框架最好?框架?还不如用开源系统吧 php框架是什么 laravel视频教程 php框架下载