视频缓存AndroidVideoCache
今天分享一个开源库 AndroidVideoCache 。这个库主要是做视频缓存管理功能,支持边下边播,离线播放,缓存管理等。
用过MediaPlayer的小伙伴都知道,可以支持在线播放和播放本地资源,但是不支持缓存,下载后的数据直接交给播放器缓冲区,数据使用完了以后直接淘汰掉。
这样很消耗用户流量,这个时候AndroidVideoCache就派上用场了
AndroidVideoCache的用法
1.添加依赖 compile 'com.danikula:videocache:2.7.1'
2.在Application里面创建全局单例 HttpProxyCacheServer,代码如下:
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);}
}
3.在给播放器设置url的时候通过生成代理url来实现视频的缓存,示例代码如下
private void startVideo() {//拿到全局的单例 HttpProxyCacheServerHttpProxyCacheServer proxy = App.getProxy(getActivity());//注册下载缓存监听proxy.registerCacheListener(this, url);//生成代理urlString proxyUrl = proxy.getProxyUrl(url);//给播放器设置播放路径的时候设置为上一步生成的proxyUrlvideoView.setVideoPath(proxyUrl);videoView.start();}
以上就是AndroidVideoCache的使用,是不是特简单!当然它还可以设置缓存的大小,缓存路径、缓存文件的数量等,在初始化的时候设置即可,代码如下
private HttpProxyCacheServer newProxy() {return new HttpProxyCacheServer.Builder(this).cacheDirectory(Utils.getVideoCacheDir(this))//缓存路径.maxCacheFilesCount(100)//最大缓存文件数量.maxCacheSize(1024 * 1024 * 1024) // 最大缓存大小 1 Gb for cache.build();}
1.基本原理
AndroidVideoCache 通过代理的策略将我们的网络请求代理到本地服务,本地服务先判断是否有本地缓存,如果有本地缓存那么直接将本地缓存返回,如果没有本地缓存,那么ProxyServer会使用原视频url去远程服务器RemoteServer请求视频数据,获取到RemoteServer返回的数据后再缓存到本地(需要缓存则缓存),再从本地返回给播放器播放。这样就做到了数据的复用。
分析源码当然要找一个入口,这里我们的入口当然是初始化HttpProxyCacheServer的地方,以上面的Builder方式初始化为例,我们看看HttpProxyCacheServer.Builder代码
public Builder(Context context) {this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context);//设置缓存路径this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context);//设置缓存策略,采用限制大小的LRU策略this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);//主要用来生成缓存文件名this.fileNameGenerator = new Md5FileNameGenerator();//默认不添加Http头信息this.headerInjector = new EmptyHeadersInjector();}
这里我们只分析this.sourceInfoStorage,其它的都已备注,没什么好解释的。我们看看SourceInfoStorageFactory
.newSourceInfoStorage做了什么,其实它只是new 了一个DatabaseSourceInfoStorage,源码如下
public class SourceInfoStorageFactory {public static SourceInfoStorage newSourceInfoStorage(Context context) {return new DatabaseSourceInfoStorage(context);}public static SourceInfoStorage newEmptySourceInfoStorage() {return new NoSourceInfoStorage();}
}
通过名字我们可以看出,这个用到了SQLite,我们再跟进去看看DatabaseSourceInfoStorage的构造,就会发现缓存的信息其实是存储在数据库里面的
class DatabaseSourceInfoStorage extends SQLiteOpenHelper implements SourceInfoStorage {private static final String TABLE = "SourceInfo";private static final String COLUMN_ID = "_id";private static final String COLUMN_URL = "url";private static final String COLUMN_LENGTH = "length";private static final String COLUMN_MIME = "mime";private static final String[] ALL_COLUMNS = new String[]{COLUMN_ID, COLUMN_URL, COLUMN_LENGTH, COLUMN_MIME};private static final String CREATE_SQL ="CREATE TABLE " + TABLE + " (" +COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +COLUMN_URL + " TEXT NOT NULL," +COLUMN_MIME + " TEXT," +COLUMN_LENGTH + " INTEGER" +");";DatabaseSourceInfoStorage(Context context) {super(context, "AndroidVideoCache.db", null, 1);checkNotNull(context);}@Overridepublic void onCreate(SQLiteDatabase db) {checkNotNull(db);db.execSQL(CREATE_SQL);}@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {throw new IllegalStateException("Should not be called. There is no any migration");}@Overridepublic SourceInfo get(String url) {checkNotNull(url);Cursor cursor = null;try {cursor = getReadableDatabase().query(TABLE, ALL_COLUMNS, COLUMN_URL + "=?", new String[]{url}, null, null, null);return cursor == null || !cursor.moveToFirst() ? null : convert(cursor);} finally {if (cursor != null) {cursor.close();}}}@Overridepublic void put(String url, SourceInfo sourceInfo) {checkAllNotNull(url, sourceInfo);SourceInfo sourceInfoFromDb = get(url);boolean exist = sourceInfoFromDb != null;ContentValues contentValues = convert(sourceInfo);if (exist) {getWritableDatabase().update(TABLE, contentValues, COLUMN_URL + "=?", new String[]{url});} else {getWritableDatabase().insert(TABLE, null, contentValues);}}@Overridepublic void release() {close();}private SourceInfo convert(Cursor cursor) {return new SourceInfo(cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_URL)),cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_LENGTH)),cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MIME)));}private ContentValues convert(SourceInfo sourceInfo) {ContentValues values = new ContentValues();values.put(COLUMN_URL, sourceInfo.url);values.put(COLUMN_LENGTH, sourceInfo.length);values.put(COLUMN_MIME, sourceInfo.mime);return values;}
}
这个代码非常明了,其实就是做的数据库的初始化工作,数据库里面存的字段主要是url、length、mime ,SourceInfo这个类也仅仅是对这3个字段的封装,get put都是基于SourceInfo的,仅仅是方便而已,无需解释,这个主要是用来查找和保存缓存的信息
Builder走完后就到了build方法,build方法里面其实就是创建HttpProxyCacheServer的实例了,代码如下
private HttpProxyCacheServer(Config config) {this.config = checkNotNull(config);try {//PROXY_HOST为127.0.0.1其实就是拿的localhostInetAddress inetAddress = InetAddress.getByName(PROXY_HOST);//通过localhost生成一个ServerSocket,localPort传0的话系统会随机分配一个端口号this.serverSocket = new ServerSocket(0, 8, inetAddress);//拿到系统分配的端口号this.port = serverSocket.getLocalPort();//将localhost添加到IgnoreHostProxySelectorIgnoreHostProxySelector.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 startsthis.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);}}
这里我们主要分析this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal)); 其它的都已注释。我们跟进去看看 WaitRequestRunnalbe里面做了什么
private final class WaitRequestsRunnable implements Runnable {private final CountDownLatch startSignal;public WaitRequestsRunnable(CountDownLatch startSignal) {this.startSignal = startSignal;}@Overridepublic void run() {startSignal.countDown();//信号量主要是为了保证这个run方法先执行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));}}
我们可以看到waitForRequest 里面监听到请求后调用了socketProcessor.submit(new SocketProcessorRunnable(socket));
到这里服务器socket一套比较清晰了,整理一下就是先构建一个全局的一个本地代理服务器 ServerSocket
,指定一个随机端口,然后新开一个线程,在线程的 run
方法里,通过accept() 方法监听这个服务器socket的入站连接,accept() 方法会一直阻塞,直到有一个客户端尝试建立连接。
有了服务器,然后就是客户端的socket,先从使用时代理替换url地方开始看
public String getProxyUrl(String url) {return getProxyUrl(url, true);}public String getProxyUrl(String url, boolean allowCachedFileUri) {if (allowCachedFileUri && isCached(url)) {//如果视频已缓存,直接返回本地视频uriFile cacheFile = getCacheFile(url);//touchFileSafely只是更新视频的lastModifyTime,因为是LRUCachetouchFileSafely(cacheFile);return Uri.fromFile(cacheFile).toString();}return isAlive() ? appendToProxyUrl(url) : url;}private String appendToProxyUrl(String url) {return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));}
整个策略就是如果本地已经缓存了,就直接那本地地址的Uri,并且touch一下文件,把时间更新后最新,因为后面LruCache是根据文件被访问的时间进行排序的,如果文件没有被缓存那么就会先走一下 isAlive()
方法, 这里会ping一下目标url,确保url是一个有效的,如果用户是通过代理访问的话,就会ping不通,这样就还是原生url,正常情况都会进入这个 appendToProxyUrl
方法里面。
这里拼接出来一个带有127.0.0.1目标地址及端口并携带原url的新地址,这个请求的话就会被我们的服务器socket监听到,也就是前面的accept() 会继续往下走,这里接收到的socket就是我们所请求的客户端socket。
socketProcessor.submit(new SocketProcessorRunnable(socket));
整个socket会被包裹成一个runnable,发配给线程池。这个 runnable 的 run
方法中所做的事情就是调用了一个方法:
private void processSocket(Socket socket) {try {//1.操作socket调用GetRequest.read()GetRequest request = GetRequest.read(socket.getInputStream());LOG.debug("Request to cache proxy:" + request);String url = ProxyCacheUtils.decode(request.uri);//获取到原生的请求urlif (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 stacktraceLOG.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());}}
前面ping的过程其实也被会这个socket监听并且走进来这一段,不过这个比较简单,就不分析了,我们直接看里面的 else 框内的代码,这里一个 getClients
就是一个ConcurrentHashMap,重复url返回的是同一个HttpProxyCacheServerClients 。
如果是第一次就会根据url构建出一个HttpProxyCacheServerClients并被put到ConcurrentHashMap中,真正的操作都在这个客户端的 processRequest
操作中,并且传递过去一个是request,这是一个GetRequest 对象,是一个url和rangeoffset以及partial的包装类,另一个就是客户端socket。
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 类,也注册了一个CacheListener,这个listener可以用来回调进度。
做完这一切之后,然后这个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);}}
这里我们用传过来的那个客户端socket,拿到一个OutputStream输出流,这样我们就能往里面写数据了,如果不用缓存就走常规逻辑,这里我们只看走缓存的行为。
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();}
构造一个8 * 1024字节的buffer,这里的read方法,实际上是调用的父类ProxyCache的实现
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,也就是本地文件写数据,同时还更新一下当前的缓存进度
private synchronized void readSourceAsync() throws ProxyCacheException {boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED;if (!stopped && !cache.isCompleted() && !readingInProgress) {//6.具体的缓存操作在SourceReaderRunnable里面sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for " + source);sourceReaderThread.start();}}private class SourceReaderRunnable implements Runnable {@Overridepublic void run() {//7.到此,我们会发现所有的缓存操作都是方法readSource()做的了readSource();}}private void readSource() {long sourceAvailable = -1;long 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();//通知更新进度} catch (Throwable e) {readSourceErrorsCount.incrementAndGet();onError(e);} finally {closeSource();notifyNewCacheDataAvailable(offset, sourceAvailable);}}
同时我们的另一个线程也会从cache中去读数据,在缓存结束后同样也会发送一个通知通知自己已经缓存完了,回调由外界控制。
以上差不多就是总体代码,这里我们在请求远程URL时将文件写到本地fileCache中,然后读数据从本地读取,写入到客户端socket里面,服务器Socket主要还是一个代理的作用,从中间拦截掉网络请求,然后实现对socket的读取和写入。
注意:对于m3u8格式的视频,url缓存是不一样的, m3u8是切片的数据 这个需要特殊的处理方式 缓存需要考虑切片索引以及视频文件拼接
视频缓存AndroidVideoCache相关推荐
- 反思 | Android 音视频缓存机制的系统性设计
反思 系列博客是我的一种新学习方式的尝试,该系列起源和目录请参考 这里 . 背景 在自媒体的时代,音视频播放 俨然已成为内容类型 APP 最基础的能力,对于 Android 开发者而言,无论是 Goo ...
- android视频缓存框架 [AndroidVideoCache](https://github.com/danikula/AndroidVideoCache) 源码解析与评估
文章目录 android视频缓存框架 [AndroidVideoCache](https://github.com/danikula/AndroidVideoCache) 源码解析与评估 引言 使用方 ...
- 音视频开发(三十九):Android视频缓存之AndroidVideoCache
目录 背景 AndroidVideoCache简单使用 实现原理 源码分析 AndroidVideoCache的不足 一.背景 播放音视频时,播放器数据的请求是由播放器内部发起的,我们只是提供了一个u ...
- 浅谈Android视频缓存库
背景 我们都了解播放器的作用就是把音视频压缩数据转换成原始的音视频数据渲染出来,这样我们就可以看到画面.听到声音了.这里的播放器就存在两个问题,第一个问题是视频源存在云端,我们每次看完视频之后重新观看 ...
- 电脑端bilibili视频缓存合并视频的解惑
文章目录 前言 一.为什么不能直接修改文件格式 二.怎么删 三.怎么合并 总结 前言 与手机同分辨率的缓存相比,电脑端视频码率更高,相应的清晰度更高,相应的文件占用空间也更大.但电脑端bilibili ...
- 微信看一看小程序视频缓存到手机的位置
使用华为p30pro手机,鸿蒙系统,找到了微信看一看小视频缓存的文件,路径 /Android/data/com.tencent.mm/cache/videocache/下,找到了appbrand1和a ...
- 视频缓存合成工具分享
具体操作步骤: 1:确保缓存的视频已缓存完成(这里使用的是腾讯视频) 2:打开缓存视频合成软件即可查看到检测的可合成视频(可显示来源及文件大小等) 如果是M3U8类似的没有则可以扫描一下 继续说正常视 ...
- uc浏览器视频缓存合并工具
1.该软件用于将uc浏览器中零散的视频缓存切片处理成完整的视频文件. 开发语言:C# 开发工具: Visual Studio 2017 Community 实例图示: 程序代码下载地址 windows ...
- 2022年11月21 B站视频缓存二进制解析
B站电脑视频缓存解析 二进制文件分析 步骤1,删除开头9个0: 步骤2,"$"(24) 换成空格(20); 步骤3,删除"avc1"(61 76 63 31) ...
最新文章
- Flyweight 享元模式
- PowerShell针对SCVMM批量导出模板的描述信息以及导入
- OpenCV cv :: UMat与DirectX10曲面的互操作性的实例(附完整代码)
- DNS详解: A记录,子域名,CNAME别名,PTR,MX,TXT,SRV,TTL
- 前端学习(752):全局变量和局部变量
- Oracle图形化管理工具——OEM
- 面试时如何在众多Java工程师中脱颖而出
- Exchange Server 2013 OWA IIS重定向
- 实验三十三、标准访问控制列表的配置
- js运算符优先级速查表
- windows安装pdf虚拟打印机
- git恢复已删除的本地分支
- 三角形边长求高的c语言函数公式,三角形边长计算公式
- php redis 搜索,一步步实现 Redis 搜索引擎
- Jvav语言(0.1)版
- netty4 io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1
- [C++] 栈的压入、弹出序列
- ubuntu16.04上搭建stm32f4开发环境
- psycopg2 , pymysql 连接数据库 操作
- 【简单实用】一台主机两个人使用,互不影响~~~