第4章:媒体源的创建与配置优化

媒体源的创建,说白了就是给播放器喂「原料」的过程。原料好不好、喂得快不快,直接影响播放器能不能在用户点开视频的瞬间就亮起来。我见过不少团队在这个阶段踩坑——明明网络没问题,代码也没报错,但就是启动慢半拍。嗯,今天咱们就把这块掰开揉碎了讲清楚。

4.1 媒体源的本质与两种常用实现

ExoPlayer里,媒体源(MediaSource)就像数据流的网关。它负责把原始URI包装成播放器能理解的结构,同时管理着数据加载、缓存、自适应切换等能力。

我个人最常用的两种是:

  • ProgressiveMediaSource —— 用于MP3、MP4等渐进式下载的媒体。
  • HlsMediaSource / DashMediaSource —— 用于自适应流媒体协议。

创建时一个很关键的细节:不要在主线程上new MediaSource。我在优化一个短视频App时发现,初始化阶段直接在UI线程组装MediaSource,导致帧率掉到20fps以下。后来只是把创建挪到后台线程,首帧耗时直接降了40%。

避坑指南:

我曾经在线上环境看到过有人这么写:

// ❌ 错误示例:主线程创建
mediaSource = new ProgressiveMediaSource.Factory(...)
        .createMediaSource(Uri.parse(url));
player.setMediaSource(mediaSource);
player.prepare();

其实完全可以这样做:

// ✅ 正确姿势:预创建,或者用线程池
MediaSource.Factory factory = new ProgressiveMediaSource.Factory(dataSourceFactory);
CompletableFuture.supplyAsync(() -> factory.createMediaSource(uri))
        .thenAccept(mediaSource -> {
            player.setMediaSource(mediaSource);
            player.prepare();
        });

4.2 配置优化:从DataSourceFactory入手

媒体源的内部最终依赖DataSource.Factory来拉数据。你想想看,这个工厂决定了ExoPlayer怎么跟网络层、缓存层打交道。优化它是性价比最高的一步。

4.2.1 合理设置缓存策略

如果不用缓存,每次创建媒体源都会重新发起网络请求。重复播放同一首音频时,这个浪费就非常明显了。

策略适用场景启动耗时影响
无缓存一次性直播流正常
FileCache + LRU短视频、循环播放降低50%~80%
ContentCache离线播放无网络请求

我个人习惯用SimpleCache配合CacheDataSource.Factory。注意:缓存key不能简单用URI,得带上协议和请求头。我就遇到过因为URL动态加了时间戳参数,导致缓存失效的问题。

// 带缓存的DataSource.Factory示例
Cache cache = new SimpleCache(cacheDir, new LeastRecentlyUsedCacheEvictor(50 * 1024 * 1024));
DataSource.Factory upstreamFactory = new DefaultHttpDataSource.Factory();
DataSource.Factory cacheFactory = new CacheDataSource.Factory()
        .setCache(cache)
        .setUpstreamDataSourceFactory(upstreamFactory)
        .setCacheWriteDataSinkFactory(null);  // 只读缓存,不写回(适合预加载)

4.2.2 预创建DataSourceFactory

不要每次播放都new一个DefaultHttpDataSource.Factory。这个对象内部会持有连接池、重试策略等资源。我建议在Application启动时就初始化一次,然后作为单例传递到MediaSource里。

一个小技巧:

如果App同时播放音视频,可以分别创建两个Factory:一个针对音频小文件,设置小缓冲区(128KB);一个针对视频,设置大缓冲区(2MB)。这样既能减少内存浪费,又能应对高码率需求。

4.3 媒体源配置的关键参数

除了DataSource,MediaSource构造时还可以传入各种定制项。以下是四个我经常调优的参数:

  • LoadControl —— 控制缓冲水位。我一般把minBufferMs从默认1500ms降到800ms,换取更快首帧。
  • TrackSelector —— 指定默认音视频轨道。如果明确只播音频,传一个DefaultTrackSelector并禁用视频轨道,能省掉一段不必要的初始化时间。
  • Allocator —— 内存分配器。对于长时间播放,建议用DefaultAllocator指定块大小(如64KB),避免频繁GC。
  • DrmSessionManager —— DRM初始化。如果是普通内容,传null;如果必须带DRM,提前准备好凭证,不要等到prepare时再联网获取。
// 一个优化后的媒体源创建示例
DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
trackSelector.setParameters(trackSelector.buildUponParameters()
        .setMaxVideoSize(1280, 720)      // 限制分辨率,降低解码压力
        .setForceLowestSupportedBitrate(false));

ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(
        cacheFactory,
        DefaultExtractorsFactory.newInstance(context).setConstantBitrateSeekingEnabled(true),
        new DefaultLoadControl.Builder()
                .setBufferDurationsMs(800, 8000, 800, 2000)
                .build(),
        trackSelector,
        null,  // DrmSessionManager
        new DefaultAllocator(true, 64 * 1024)
).createMediaSource(uri);

4.4 进阶:延迟创建与预热

有时候用户还没点击播放按钮,我们就知道他要播什么了(比如Feed流自动缓存)。这时候可以提前把MediaSource甚至DataSource的TCP连接建好。说白了就是预热。

我个人习惯结合PlayerView的onAttachedToWindow回调来做:一旦视图挂载到窗口,立即异步创建MediaSource并缓存到弱引用里。等用户点击播放,直接player.setMediaSource(cachedMediaSource),几乎没有延迟。

注意内存泄漏:

提前创建的MediaSource如果被Player引用后没有及时释放,当页面被销毁时,DataSource内部可能还持有Socket连接。我遇到过因为没释放缓存工厂导致的连接池泄漏。建议用WeakReference或生命周期感知组件来管理预创建对象。

4.5 知识图谱:媒体源创建与配置优化全流程

媒体源创建与配置优化流程图 媒体源初始化优化核心路径 创建 DataSource.Factory 配置缓存策略(SimpleCache / None) 组装 MediaSource(协议 + 参数) 设置 LoadControl & TrackSelector 异步预创建 / 预热(提前连接) 关键优化点 • 单例复用Factory • 区分音频/视频缓冲区 • 预设置Track限制 • 异步创建避阻塞

4.6 实战经验:边界条件处理

说几个我踩过的真实坑:

  1. 空URI —— 用户断网后点击缓存视频,URI可能为空。一定要在创建MediaSource前判空,否则ExoPlayer会抛出NullPointerException导致应用崩溃。
  2. 无效协议 —— 有些第三方SDK传了file://但文件不存在。建议自行包装一层FileDataSource.Factory并添加文件存在性检查。
  3. 低端设备内存不足 —— 如果在创建媒体源时分配了太大的Allocator块,低端机型容易OOM。我在做短视频项目时不得不动态判断设备总内存,64KB与16KB切换。

嗯,说到检查,我习惯写一个工具类统一创建所有媒体源。这样一旦需要调整参数,改一处就行,不会漏掉。

推荐写法:

public static MediaSource createSafeSource(Uri uri, Context context) {
    if (uri == null || TextUtils.isEmpty(uri.toString())) return null;
    // 根据URI scheme选择最合适的Factory
    String scheme = uri.getScheme();
    if ("https".equals(scheme) || "http".equals(scheme)) {
        return new ProgressiveMediaSource.Factory(buildCacheFactory(context))
                .createMediaSource(uri);
    } else if ("file".equals(scheme)) {
        if (!new File(uri.getPath()).exists()) return null;
        return new ProgressiveMediaSource.Factory(new FileDataSource.Factory())
                .createMediaSource(uri);
    }
    return null;
}

总结

媒体源的创建与配置,表面上看只是几行代码的事。但实际上,它的内部连接着网络、缓存、解码器、内存。我每次做性能优化,都会先从这步开始——因为它是所有后续流程的入口,入口慢了,后面怎么提速都于事无补。

记住几个核心点:把创建挪到后台、复用Factory、提前准备缓存、根据场景调整LoadControl。只要你把这四点真正落实,首帧提升50%不是梦。