由于业务需要,要做一个视频下载缓存的功能,因为项目中有用到了GSYVideoPlayer,于是参考了GSYVideoPlayer的做法
GSYVideoPlayer 是一款优秀的开源播放器,里面的功能也比较全面,支持HTTPS,支持弹幕,支持滤镜、水印、gif截图,片头广告、中间广告,多个同时播放,支持基本的拖动,声音、亮度调节,支持边播边缓存,支持视频自带rotation的旋转,重力旋转与手动旋转的同步支持,支持列表播放 ,列表全屏动画,视频加载速度,列表小窗口支持拖动,动画效果,调整比例,多分辨率切换,支持切换播放器,进度条小窗口预览,列表切换详情页面无缝播放,rtsp、concat、mpeg。
从GSYVideoPlayer的demo可以看到,设置缓存的方式为
public static void setCacheManager(Class<? extends ICacheManager> cacheManager) {
}
GSYVideoPlayer提供了2种缓存方式
CacheFactory.setCacheManager(ExoPlayerCacheManager.class);//exo缓存模式,支持m3u8,只支持exo
CacheFactory.setCacheManager(ProxyCacheManager.class);//代理缓存模式,支持所有模式,不支持m3u8等
默认为 代理缓存模式ProxyCacheManager
/**
* 缓存管理接口
* Created by guoshuyu on 2018/5/18.
*/
public interface ICacheManager {
/**
* 开始缓存逻辑
*
* @param mediaPlayer 播放内核
* @param url 播放url
* @param header 头部信息
* @param cachePath 缓存路径,可以为空
*/
void doCacheLogic(Context context, IMediaPlayer mediaPlayer, String url, Map<String, String> header, File cachePath);
void setCacheAvailableListener(ICacheAvailableListener cacheAvailableListener);
/**
* 缓存进度接口
*/
interface ICacheAvailableListener {
void onCacheAvailable(File cacheFile, String url, int percentsAvailable);
}
}
以上是ICacheManager
的部分代码,GSYVideoPlayer 默认使用代理模式进行缓存视频,所以我们先看ProxyCacheManager 代理缓存模式
@Override
public void doCacheLogic(Context context, IMediaPlayer mediaPlayer, String originUrl, Map<String, String> header, File cachePath) {
String url = originUrl;
userAgentHeadersInjector.mMapHeadData.clear();
if (header != null) {
userAgentHeadersInjector.mMapHeadData.putAll(header);
}
if (url.startsWith("http") && !url.contains("127.0.0.1") && !url.contains(".m3u8")) {//1
HttpProxyCacheServer proxy = getProxy(context.getApplicationContext(), cachePath);//3
if (proxy != null) {
//此处转换了url,然后再赋值给mUrl。
url = proxy.getProxyUrl(url);
mCacheFile = (!url.startsWith("http"));
//注册上缓冲监听
if (!mCacheFile) {
proxy.registerCacheListener(this, originUrl);
}
}
} else if ((!url.startsWith("http") && !url.startsWith("rtmp")
&& !url.startsWith("rtsp") && !url.contains(".m3u8"))) {//2
mCacheFile = true;
}
try {
mediaPlayer.setDataSource(context, Uri.parse(url), header);
} catch (IOException e) {
e.printStackTrace();
}
}
以上为ProxyCacheManager
的开始缓存逻辑,从注释1,2处可知,如果需要缓存的链接是m3u8或者rtmp、rtsp是不支持缓存的,如果没有设置缓存存储的路径的话,就会使用默认的缓存路径,注释3处调用了一下代码
/**
获取缓存代理服务,带文件目录的
*/
public static HttpProxyCacheServer getProxy(Context context, File file) {
//如果为空,返回默认的
if (file == null) {
return getProxy(context);
}
}
/**
创建缓存代理服务
*/
public HttpProxyCacheServer newProxy(Context context) {
return new HttpProxyCacheServer.Builder(context.getApplicationContext())//4
.headerInjector(userAgentHeadersInjector).build();
}
/**
获取缓存代理服务
*/
protected static HttpProxyCacheServer getProxy(Context context) {
HttpProxyCacheServer proxy = ProxyCacheManager.instance().proxy;
return proxy == null ? (ProxyCacheManager.instance().proxy =
ProxyCacheManager.instance().newProxy(context)) : proxy;
}
从代码中得知刚开始进来的时候会创建一个HttpProxyCacheServer即缓存代理服务,注释4处调用了以下代码,
public Builder(Context context) {
this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context);
this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context);//5
this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);
this.fileNameGenerator = new Md5FileNameGenerator();
this.headerInjector = new EmptyHeadersInjector();
}
注释5处调用了以下方法,根据方法注释可知,缓存会放在SD卡里面/Android/data/[app_package_name]/cache/video-cache
这个目录,同时也有可能放在/data/data/[app_package_name]/cache/video-cache
这个目录
/**
* Returns individual application cache directory (for only video caching from Proxy). Cache directory will be
* created on SD card ("/Android/data/[app_package_name]/cache/video-cache") if card is mounted .
* Else - Android defines cache directory on device's file system.
*
* @param context Application context
* @return Cache {@link File directory}
*/
public static File getIndividualCacheDirectory(Context context) {
File cacheDir = getCacheDirectory(context, true);
return new File(cacheDir, INDIVIDUAL_DIR_NAME);
}
/**
* Returns application cache directory. Cache directory will be created on SD card
* ("/Android/data/[app_package_name]/cache") (if card is mounted and app has appropriate permission) or
* on device's file system depending incoming parameters.
*
* @param context Application context
* @param preferExternal Whether prefer external location for cache
* @return Cache {@link File directory}.
* NOTE: Can be null in some unpredictable cases (if SD card is unmounted and
* {@link Context#getCacheDir() Context.getCacheDir()} returns null).
*/
private static File getCacheDirectory(Context context, boolean preferExternal) {
File appCacheDir = null;
String externalStorageState;
try {
externalStorageState = Environment.getExternalStorageState();
} catch (NullPointerException e) { // (sh)it happens
externalStorageState = "";
}
if (preferExternal && MEDIA_MOUNTED.equals(externalStorageState)) {
appCacheDir = getExternalCacheDir(context);
}
if (appCacheDir == null) {
appCacheDir = context.getCacheDir();
}
if (appCacheDir == null) {
String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/";
HttpProxyCacheDebuger.printfWarning("Can't define system cache directory! '" + cacheDirPath + "%s' will be used.");
appCacheDir = new File(cacheDirPath);
}
return appCacheDir;
}
在手机上找到缓存的目录
从图中可以得知,完成的文件为下载的url进行md5加密的值,下载中的文件多了一个.download的后缀
exo缓存模式相对比较强大,除了可以缓存mp4这样的单个视频文件,还可以缓存m3u8这样的视频流
以下是ExoPlayerCacheManager
开始缓存的逻辑
@Override
public void doCacheLogic(Context context, IMediaPlayer mediaPlayer, String url, Map<String, String> header, File cachePath) {
if (!(mediaPlayer instanceof IjkExo2MediaPlayer)) {
throw new UnsupportedOperationException("ExoPlayerCacheManager only support IjkExo2MediaPlayer");
}
IjkExo2MediaPlayer exoPlayer = ((IjkExo2MediaPlayer) mediaPlayer);
mExoSourceManager = exoPlayer.getExoHelper();
//通过自己的内部缓存机制
exoPlayer.setCache(true);
exoPlayer.setCacheDir(cachePath);
exoPlayer.setDataSource(context, Uri.parse(url), header);//6
}
注释6是缓存进入的入口,具体实现如下:
@Override
public void setDataSource(Context context, Uri uri, Map<String, String> headers) {
if (headers != null) {
mHeaders.clear();
mHeaders.putAll(headers);
}
setDataSource(context, uri);
}
@Override
public void setDataSource(Context context, Uri uri) {
mDataSource = uri.toString();
mMediaSource = mExoHelper.getMediaSource(mDataSource, isPreview, isCache, isLooping, mCacheDir, mOverrideExtension);//7
}
继续追踪,在注释7处判断资源类型,具体代码如下
/**
* @param dataSource 链接
* @param preview 是否带上header,默认有header自动设置为true
* @param cacheEnable 是否需要缓存
* @param isLooping 是否循环
* @param cacheDir 自定义缓存目录
*/
public MediaSource getMediaSource(String dataSource, boolean preview, boolean cacheEnable, boolean isLooping, File cacheDir, @Nullable String overrideExtension) {
MediaSource mediaSource = null;
if (sExoMediaSourceInterceptListener != null) {
mediaSource = sExoMediaSourceInterceptListener.getMediaSource(dataSource, preview, cacheEnable, isLooping, cacheDir);
}
if (mediaSource != null) {
return mediaSource;
}
mDataSource = dataSource;
Uri contentUri = Uri.parse(dataSource);
int contentType = inferContentType(dataSource, overrideExtension);
switch (contentType) {
case C.TYPE_SS:
mediaSource = new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(getDataSourceFactoryCache(mAppContext, cacheEnable, preview, cacheDir)),
new DefaultDataSourceFactory(mAppContext, null,
getHttpDataSourceFactory(mAppContext, preview))).createMediaSource(contentUri);
break;
case C.TYPE_DASH:
mediaSource = new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(getDataSourceFactoryCache(mAppContext, cacheEnable, preview, cacheDir)),
new DefaultDataSourceFactory(mAppContext, null,
getHttpDataSourceFactory(mAppContext, preview))).createMediaSource(contentUri);
break;
case C.TYPE_HLS:
mediaSource = new HlsMediaSource.Factory(getDataSourceFactoryCache(mAppContext, cacheEnable, preview, cacheDir)).createMediaSource(contentUri);
break;
case TYPE_RTMP:
RtmpDataSourceFactory rtmpDataSourceFactory = new RtmpDataSourceFactory(null);
mediaSource = new ExtractorMediaSource.Factory(rtmpDataSourceFactory)
.setExtractorsFactory(new DefaultExtractorsFactory())
.createMediaSource(contentUri);
break;
case C.TYPE_OTHER:
default:
mediaSource = new ExtractorMediaSource.Factory(getDataSourceFactoryCache(mAppContext, cacheEnable, preview, cacheDir))
.setExtractorsFactory(new DefaultExtractorsFactory())
.createMediaSource(contentUri);
break;
}
if (isLooping) {
return new LoopingMediaSource(mediaSource);
}
return mediaSource;
}
/**
* 本地缓存目录
*/
public static synchronized Cache getCacheSingleInstance(Context context, File cacheDir) {
String dirs = context.getCacheDir().getAbsolutePath();
if (cacheDir != null) {
dirs = cacheDir.getAbsolutePath();
}
if (mCache == null) {
String path = dirs + File.separator + "exo";
boolean isLocked = SimpleCache.isCacheFolderLocked(new File(path));
if (!isLocked) {
mCache = new SimpleCache(new File(path), new LeastRecentlyUsedCacheEvictor(DEFAULT_MAX_SIZE));
}
}
return mCache;
}
在同一个文件中有发现有一个函数是本地缓存目录,在手机中查看这个目录
对于m3u8视频,缓存目录为/data/data/[app_package_name]/cache/exo
,每个线程保存的都以线程号命名保存到文件夹内,并且将文件改为x.y.z.v3.exo的形式,其中x代表序号,y应该是成功标志,0为成功,其他为失败,z为保存的时间戳毫秒值
类似与M3U8的处理方式,对于MP4,exo缓存模式一样会将视频分成10份进行下载,但是对于下载后的文件我并没有总结处适合的规律,可以像M3U8那样组合的规律出来
代理缓存模式不支持缓存m3u8视频流,而exo模式可以
代理缓存模式在播放mp4资源的时候,缓存是单个文件缓存的,缓存完成之后可以将视频文件单独拿出来,使用其他APP播放,而exo缓存模式会将一个mp4文件分割成为多份,并不方便再次使用
代理缓存模式在播放mp4资源的时候,如果将进度调到比较靠后的话,播放器会出现错误,然后回到最开始进行播放,而exo缓存模式可以任意调整视频进度
exo缓存模式必须将播放器的内核改为Exo2PlayerManager
或IjkPlayerManager
,代理缓存模式的并不要求