第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 知识图谱:媒体源创建与配置优化全流程
4.6 实战经验:边界条件处理
说几个我踩过的真实坑:
- 空URI —— 用户断网后点击缓存视频,URI可能为空。一定要在创建MediaSource前判空,否则ExoPlayer会抛出
NullPointerException导致应用崩溃。 - 无效协议 —— 有些第三方SDK传了
file://但文件不存在。建议自行包装一层FileDataSource.Factory并添加文件存在性检查。 - 低端设备内存不足 —— 如果在创建媒体源时分配了太大的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%不是梦。