GSYVideoPlayer 视频缓存简单源码解析

3,634 阅读6分钟

由于业务需要,要做一个视频下载缓存的功能,因为项目中有用到了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

ICacheManager 分析


/**
 * 缓存管理接口
 * 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 代理缓存模式

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 <i>("/Android/data/[app_package_name]/cache/video-cache")</i> 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
     * <i>("/Android/data/[app_package_name]/cache")</i> (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}.<br />
     * <b>NOTE:</b> 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的后缀

ExoPlayerCacheManager exo缓存模式

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视频

exo模式缓存目录
exo模式缓存目录
对于m3u8视频,缓存目录为/data/data/[app_package_name]/cache/exo,每个线程保存的都以线程号命名保存到文件夹内,并且将文件改为x.y.z.v3.exo的形式,其中x代表序号,y应该是成功标志,0为成功,其他为失败,z为保存的时间戳毫秒值

MP4视频

类似与M3U8的处理方式,对于MP4,exo缓存模式一样会将视频分成10份进行下载,但是对于下载后的文件我并没有总结处适合的规律,可以像M3U8那样组合的规律出来

两种缓存模式的优缺点

代理缓存模式不支持缓存m3u8视频流,而exo模式可以 代理缓存模式在播放mp4资源的时候,缓存是单个文件缓存的,缓存完成之后可以将视频文件单独拿出来,使用其他APP播放,而exo缓存模式会将一个mp4文件分割成为多份,并不方便再次使用 代理缓存模式在播放mp4资源的时候,如果将进度调到比较靠后的话,播放器会出现错误,然后回到最开始进行播放,而exo缓存模式可以任意调整视频进度 exo缓存模式必须将播放器的内核改为Exo2PlayerManagerIjkPlayerManager,代理缓存模式的并不要求