一、背景
当初的挪动利用,视频是一个十分重要的组成部分,如同外面不搞一点视频就不是一个失常的挪动App。在视频开发方面,能够分为视频录制和视频播放,视频录制的场景可能还比拟少,这方面能够应用Google开源的 grafika。相比于视频录制,视频播放能够抉择的计划就要多许多,比方Google的 ExoPlayer,B站的 ijkplayer,以及官网的MediaPlayer。
不过,咱们明天要讲的是视频的缓存。最近,因为咱们在开发视频方面没有思考视频的缓存问题,造成了流量的节约,而后受到用户的投诉。在视频播放中,个别有两种两种策略:先下载再播放和边播放边缓存。
通常,为了进步用户的体验,咱们会抉择边播放边缓存的策略,不过市面上大多数的播放器都是只反对视频播放,在视频缓存这块基本上没啥好的计划,比方咱们的App应用的是一个本人封装的库,相似于PlayerBase。PlayerBase是一种将解码器和播放视图组件化解决的解决方案框架,也即是一个对ExoPlayer、ijkplayer的包装库。
二、PlayerBase
PlayerBase是一种将解码器和播放视图组件化解决的解决方案框架。您须要什么解码器实现框架定义的形象引入即可,对于视图,无论是播放器内的管制视图还是业务视图,均能够做到组件化解决。并且,它反对视频跨页面无缝连接的成果,也是咱们抉择它的一个起因。
PlayerBase的应用也比较简单,应用的时候须要独自的增加解码器,具体应用哪种解码器,能够依据我的项目的须要自在的进行配置。
只应用MediaPlayer:
dependencies { //该依赖仅蕴含MediaPlayer解码 implementation 'com.kk.taurus.playerbase:playerbase:3.4.2'}
应用ExoPlayer + MediaPlayer
dependencies { //该依赖蕴含exoplayer解码和MediaPlayer解码 //留神exoplayer的最小反对SDK版本为16 implementation 'cn.jiajunhui:exoplayer:342_2132_019'}
应用ijkplayer + MediaPlayer
dependencies { //该依赖蕴含ijkplayer解码和MediaPlayer解码 implementation 'cn.jiajunhui:ijkplayer:342_088_012' //ijk官网的解码库依赖,较少格局版本且不反对HTTPS。 implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8' # Other ABIs: optional implementation 'tv.danmaku.ijk.media:ijkplayer-armv5:0.8.8' implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8' implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8' implementation 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8' }
应用ijkplayer + ExoPlayer + MediaPlayer
dependencies { //该依赖蕴含exoplayer解码和MediaPlayer解码 //留神exoplayer的最小反对SDK版本为16 implementation 'cn.jiajunhui:exoplayer:342_2132_019' //该依赖蕴含ijkplayer解码和MediaPlayer解码 implementation 'cn.jiajunhui:ijkplayer:342_088_012' //ijk官网的解码库依赖,较少格局版本且不反对HTTPS。 implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8' # Other ABIs: optional implementation 'tv.danmaku.ijk.media:ijkplayer-armv5:0.8.8' implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8' implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8' implementation 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8' }
最初,在进行代码混同时,还须要在proguard中增加如下混同规定。
-keep public class * extends android.view.View{*;}-keep public class * implements com.kk.taurus.playerbase.player.IPlayer{*;}
增加完解码器之后,接下来只须要在利用的Application中初始化解码器,而后就能够应用了。
public class App extends Application { @Override public void onCreate() { //... //如果您想应用默认的网络状态事件生产者,请增加此行配置。 //并须要增加权限 android.permission.ACCESS_NETWORK_STATE PlayerConfig.setUseDefaultNetworkEventProducer(true); //初始化库 PlayerLibrary.init(this); //如果增加了'cn.jiajunhui:exoplayer:xxxx'该依赖 ExoMediaPlayer.init(this); //如果增加了'cn.jiajunhui:ijkplayer:xxxx'该依赖 IjkPlayer.init(this); //播放记录的配置 //开启播放记录 PlayerConfig.playRecord(true); PlayRecordManager.setRecordConfig( new PlayRecordManager.RecordConfig.Builder() .setMaxRecordCount(100) //.setRecordKeyProvider() //.setOnRecordCallBack() .build()); } }
而后,在业务代码中开始播放即可。
ListPlayer.get().play(DataSource(url))
不过,有一个毛病是,PlayerBase并没有提供缓存计划,即播放过的视频再次播放的时候还是会耗费流量,这就违反了咱们的设计初衷,那有没有一种能够反对缓存,同时对PlayerBase侵入性比拟小的计划呢?答案是有的,那就是AndroidVideoCache。
三、AndroidVideoCache
3.1 基本原理
AndroidVideoCache 通过代理的策略实现一个中间层,而后咱们的网络申请会转移到本地实现的代理服务器上,这样咱们真正申请的数据就会被代理拿到,接着代理一边向本地写入数据,一边依据咱们须要的数据看是读网络数据还是读本地缓存数据,从而实现数据的复用。
通过理论测试,我发现它的流程如下:首次应用时应用的是网络的数据,前面再次应用雷同的视频时就会读取本地的。因为,AndroidVideoCache能够配置缓存文件的大小,所以,再加载视频前,它会反复后面的策略,工作原理图如下。
3.2 根本应用
和其余的插件应用流程一样,首先须要咱们在我的项目中增加AndroidVideoCache依赖。
dependencies { compile 'com.danikula:videocache:2.7.1'}
而后,在全局初始化一个本地代理服务器,咱们抉择在Application的实现类中进行全局初始化。
public class App extends Application { private HttpProxyCacheServer proxy; public static HttpProxyCacheServer getProxy(Context context) { App app = (App) context.getApplicationContext(); return app.proxy == null ? (app.proxy = app.newProxy()) : app.proxy; } private HttpProxyCacheServer newProxy() { return new HttpProxyCacheServer(this); }}
当然,初始化的代码也能够写到其余的中央,比方咱们的公共Module。有了代理服务器之后,咱们在应用的中央把网络视频url替换成上面的形式。
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState); HttpProxyCacheServer proxy = getProxy(); String proxyUrl = proxy.getProxyUrl(VIDEO_URL); videoView.setVideoPath(proxyUrl);}
当然,AndroidVideoCache还提供了很多的自定义规定,比方缓存文件的大小、文件的个数,以及缓存地位等。
private HttpProxyCacheServer newProxy() { return new HttpProxyCacheServer.Builder(this) .maxCacheSize(1024 * 1024 * 1024) .build();}private HttpProxyCacheServer newProxy() { return new HttpProxyCacheServer.Builder(this) .maxCacheFilesCount(20) .build();} private HttpProxyCacheServer newProxy() { return new HttpProxyCacheServer.Builder(this) .cacheDirectory(getVideoFile()) .maxCacheSize(512 * 1024 * 1024) .build(); } /*** 缓存门路**/ public File getVideoFile() { String path = getExternalCacheDir().getPath() + "/video"; File file = new File(path); if (!file.exists()) { file.mkdir(); } return file; }
当然,咱们还能够应用的MD5形式生成一个key作为文件的名称。
public class MyFileNameGenerator implements FileNameGenerator { public String generate(String url) { Uri uri = Uri.parse(url); String videoId = uri.getQueryParameter("videoId"); return videoId + ".mp4"; }}...HttpProxyCacheServer proxy = HttpProxyCacheServer.Builder(context) .fileNameGenerator(new MyFileNameGenerator()) .build()
除此之外,AndroidVideoCache还反对增加一个自定义的HeadersInjector,用来在申请时候增加自定义的申请头。
public class UserAgentHeadersInjector implements HeaderInjector { @Override public Map<String, String> addHeaders(String url) { return Maps.newHashMap("User-Agent", "Cool app v1.1"); }}private HttpProxyCacheServer newProxy() { return new HttpProxyCacheServer.Builder(this) .headerInjector(new UserAgentHeadersInjector()) .build();}
3.3 源码剖析
后面咱们说过,AndroidVideoCache 通过代理的策略实现一个中间层,而后再网络申请时通过本地代理服务去实现真正的申请,这样操作的益处是不会产生额定的申请,并且在缓存策略上,AndroidVideoCache应用了LruCache缓存策略算法,不必去手动保护缓存区的大小,真正做到解放双手。首先,咱们来看一下HttpProxyCacheServer类。
public class HttpProxyCacheServer { private static final Logger LOG = LoggerFactory.getLogger("HttpProxyCacheServer"); private static final String PROXY_HOST = "127.0.0.1"; private final Object clientsLock = new Object(); private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8); private final Map<String, HttpProxyCacheServerClients> clientsMap = new ConcurrentHashMap<>(); private final ServerSocket serverSocket; private final int port; private final Thread waitConnectionThread; private final Config config; private final Pinger pinger; public HttpProxyCacheServer(Context context) { this(new Builder(context).buildConfig()); } private HttpProxyCacheServer(Config config) { this.config = checkNotNull(config); try { InetAddress inetAddress = InetAddress.getByName(PROXY_HOST); this.serverSocket = new ServerSocket(0, 8, inetAddress); this.port = serverSocket.getLocalPort(); IgnoreHostProxySelector.install(PROXY_HOST, port); CountDownLatch startSignal = new CountDownLatch(1); this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal)); this.waitConnectionThread.start(); startSignal.await(); // freeze thread, wait for server starts this.pinger = new Pinger(PROXY_HOST, port); LOG.info("Proxy cache server started. Is it alive? " + isAlive()); } catch (IOException | InterruptedException e) { socketProcessor.shutdown(); throw new IllegalStateException("Error starting local proxy server", e); } } ... public static final class Builder { /** * Builds new instance of {@link HttpProxyCacheServer}. * * @return proxy cache. Only single instance should be used across whole app. */ public HttpProxyCacheServer build() { Config config = buildConfig(); return new HttpProxyCacheServer(config); } private Config buildConfig() { return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage, headerInjector); } }}
能够看到,构造函数首先应用本地的localhost地址,创立一个 ServerSocket 并随机调配了一个端口,而后通过 getLocalPort 拿到服务器端口,用来和服务器进行通信。接着,创立了一个线程 WaitRequestsRunnable,外面有一个startSignal信号变量。
@Override public void run() { startSignal.countDown(); waitForRequest(); } private void waitForRequest() { try { while (!Thread.currentThread().isInterrupted()) { Socket socket = serverSocket.accept(); LOG.debug("Accept new socket " + socket); socketProcessor.submit(new SocketProcessorRunnable(socket)); } } catch (IOException e) { onError(new ProxyCacheException("Error during waiting connection", e)); } }
服务器的整个代理的流程是,先构建一个全局的本地代理服务器 ServerSocket,指定一个随机端口,而后新开一个线程,在线程的 run 办法里通过accept() 办法监听服务器socket的入站连贯,accept() 办法会始终阻塞,直到有一个客户端尝试建设连贯。
有了代码服务器之后,接下来就是客户端的Socket。咱们先从代理替换url中央开始看:
HttpProxyCacheServer proxy = getProxy(); String proxyUrl = proxy.getProxyUrl(VIDEO_URL); videoView.setVideoPath(proxyUrl);
其中,HttpProxyCacheServer 中的 getProxyUrl()办法源码如下。
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; }
能够看到,下面的代码就是AndroidVideoCache的外围的性能:如果本地曾经缓存了,就间接应用本地的Uri,并且把工夫更新下,因为LruCache是依据文件被拜访的工夫进行排序的,如果文件没有被缓存那么就调用isAlive() 办法,isAlive()办法会ping一下指标url,确保url是一个无效的。
private boolean isAlive() { return pinger.ping(3, 70); // 70+140+280=max~500ms }
如果用户是通过代理拜访的话,就会ping不通,这样就还是应用原生的url,最初进入appendToProxyUrl ()办法外面。
private String appendToProxyUrl(String url) { return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url)); }
接着,socket会被包裹成一个runnable,发配给线程池。
socketProcessor.submit(new SocketProcessorRunnable(socket));private final class SocketProcessorRunnable implements Runnable { private final Socket socket; public SocketProcessorRunnable(Socket socket) { this.socket = socket; } @Override public void run() { processSocket(socket); } } private void processSocket(Socket socket) { try { GetRequest request = GetRequest.read(socket.getInputStream()); LOG.debug("Request to cache proxy:" + request); String url = ProxyCacheUtils.decode(request.uri); if (pinger.isPingRequest(url)) { pinger.responseToPing(socket); } else { HttpProxyCacheServerClients clients = getClients(url); clients.processRequest(request, socket); } } catch (SocketException e) { // There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458 // So just to prevent log flooding don't log stacktrace LOG.debug("Closing socket… Socket is closed by client."); } catch (ProxyCacheException | IOException e) { onError(new ProxyCacheException("Error processing request", e)); } finally { releaseSocket(socket); LOG.debug("Opened connections: " + getClientsCount()); } }
processSocket()办法会解决所有的申请进来的Socket,包含ping的和VideoView.setVideoPath(proxyUrl)的Socket,咱们重点看一下 else语句外面的代码。这里的 getClients()办法外面有一个ConcurrentHashMap,反复url返回的是同一个HttpProxyCacheServerClients。
private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException { synchronized (clientsLock) { HttpProxyCacheServerClients clients = clientsMap.get(url); if (clients == null) { clients = new HttpProxyCacheServerClients(url, config); clientsMap.put(url, clients); } return clients; } }
如果是第一次申请的url,HttpProxyCacheServerClients并被put到ConcurrentHashMap中。而真正的网络申请都在 processRequest ()办法中进行操作,并且须要传递过来一个GetRequest 对象,包含是一个url和rangeoffset以及partial的包装类。
public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException { startProcessRequest(); try { clientsCount.incrementAndGet(); proxyCache.processRequest(request, socket); } finally { finishProcessRequest(); } }
其中,startProcessRequest 办法会失去一个新的HttpProxyCache 类对象。
private synchronized void startProcessRequest() throws ProxyCacheException { proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache; } private HttpProxyCache newHttpProxyCache() throws ProxyCacheException { HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage); FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage); HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache); httpProxyCache.registerCacheListener(uiCacheListener); return httpProxyCache; }
此处,咱们构建一个基于原生url的HttpUrlSource ,这个类对象负责持有url,并开启HttpURLConnection来获取一个InputStream,这样就能够应用这个输出流来读取数据了,同时也创立了一个本地的临时文件,一个以.download结尾的临时文件,这个文件在胜利下载完后的 FileCache 类中的 complete 办法中被更名。执行完下面的操作之后,而后这个HttpProxyCache 对象就开始 调用processRequest()办法。
public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException { OutputStream out = new BufferedOutputStream(socket.getOutputStream()); String responseHeaders = newResponseHeaders(request); out.write(responseHeaders.getBytes("UTF-8")); long offset = request.rangeOffset; if (isUseCache(request)) { responseWithCache(out, offset); } else { responseWithoutCache(out, offset); } }
拿到一个OutputStream的输入流后,咱们就能够往sd卡中写数据了,如果不必缓存就走惯例逻辑,这里咱们只看走缓存的逻辑,即responseWithCache()。
private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int readBytes; while ((readBytes = read(buffer, offset, buffer.length)) != -1) { out.write(buffer, 0, readBytes); offset += readBytes; } out.flush(); }public int read(byte[] buffer, long offset, int length) throws ProxyCacheException { ProxyCacheUtils.assertBuffer(buffer, offset, length); while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) { readSourceAsync(); waitForSourceData(); checkReadSourceErrorsCount(); } int read = cache.read(buffer, offset, length); if (cache.isCompleted() && percentsAvailable != 100) { percentsAvailable = 100; onCachePercentsAvailableChanged(100); } return read; }
在while循环外面,开启了一个新的线程sourceReaderThread,其中封装了一个SourceReaderRunnable的Runnable,这个异步线程用来给cache,也就是本地文件写数据,同时还更新一下以后的缓存进度。
同时,另一个SourceReaderRunnable线程会从cache中去读数据,在缓存完结后会发送一个告诉告诉缓存完了,外界能够去调用了。
int sourceAvailable = -1; int offset = 0; try { offset = cache.available(); source.open(offset); sourceAvailable = source.length(); byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE]; int readBytes; while ((readBytes = source.read(buffer)) != -1) { synchronized (stopLock) { if (isStopped()) { return; } cache.append(buffer, readBytes); } offset += readBytes; notifyNewCacheDataAvailable(offset, sourceAvailable); } tryComplete(); onSourceRead();
到此,AndroidVideoCache的外围缓存流程就剖析完了。总的来说,AndroidVideoCache在申请时回先应用本地的代理形式,而后开启一系列的缓存逻辑,并在缓存实现后发出通知,当再次申请的时候,如果本地曾经进行了文件缓存,就会优先应用本地的数据。