10、在线音乐集成:ExoPlayer接入、HLS/DASH流播放、缓存策略
在线音乐播放,是车载多媒体应用的核心功能之一。你想想看,用户开车时最常用的场景是什么?导航、听歌、接电话。其中听歌这块,如果体验做不好,用户分分钟就想卸载你的App。
这一章,我们来聊聊怎么在Android车载系统里集成ExoPlayer,搞定HLS和DASH这种自适应码率流,再配上合理的缓存策略。嗯,这里面的坑不少,我一个个说。
10.1 为什么选ExoPlayer?
Android原生有个MediaPlayer,但说实话,在车载场景下它不够灵活。我早期做的一个项目,用MediaPlayer播HLS流,结果在切换音轨时直接崩溃,查了半天发现是底层解码器不支持。
ExoPlayer是Google官方的开源播放器,优势很明显:
- 可定制性强:你可以自己写渲染器、加载控制逻辑
- 支持格式多:HLS、DASH、SmoothStreaming,甚至RTMP都能搞定
- 缓存机制完善:自带CacheDataSource,省去自己造轮子
- 更新活跃:Google一直在维护,bug修复快
我个人习惯,只要是车载项目,一律用ExoPlayer。省心。
10.2 ExoPlayer接入:从依赖到初始化
先加依赖。在build.gradle里:
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
然后初始化播放器。我建议把播放器做成单例,因为车载系统资源有限,多个播放器实例容易导致内存问题。
// 创建ExoPlayer实例
ExoPlayer player = new ExoPlayer.Builder(context)
.setRenderersFactory(new DefaultRenderersFactory(context))
.setMediaSourceFactory(new DefaultMediaSourceFactory(context))
.build();
// 设置播放器到SurfaceView或TextureView
playerView.setPlayer(player);
这里有个细节:DefaultRenderersFactory默认会启用硬件加速解码。在车载平台上,有些SoC的硬件解码器对HLS支持不好。我曾经遇到过一个案子,用瑞芯微的芯片,硬解HLS时画面花屏,改成软解就正常了。
new DefaultRenderersFactory(context).setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF)
10.3 HLS和DASH流播放
HLS和DASH都是自适应码率流协议。说白了,就是根据网络状况自动切换清晰度。在车上,网络信号时好时坏,这个功能特别重要。
10.3.1 HLS播放
HLS是Apple推出的,但Android上也能播。ExoPlayer支持得很好。
// 构建HLS媒体源
MediaItem mediaItem = new MediaItem.Builder()
.setUri("https://example.com/stream.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build();
player.setMediaItem(mediaItem);
player.prepare();
player.play();
HLS的m3u8文件里会列出不同码率的子流。ExoPlayer默认会根据带宽自动选择。但我建议你手动设置一下TrackSelectionParameters,限制最大码率。为什么?因为车载流量通常有限,用户不想听首歌把套餐听没了。
TrackSelectionParameters parameters = player.getTrackSelectionParameters()
.buildUpon()
.setMaxVideoBitrate(2_000_000) // 限制最大2Mbps
.setMaxAudioBitrate(320_000) // 音频320kbps足够
.build();
player.setTrackSelectionParameters(parameters);
10.3.2 DASH播放
DASH是MPEG标准,国内很多音乐平台在用。用法和HLS类似,只是MIME类型不同。
MediaItem mediaItem = new MediaItem.Builder()
.setUri("https://example.com/stream.mpd")
.setMimeType(MimeTypes.APPLICATION_MPD)
.build();
DASH的mpd文件里包含分段信息。ExoPlayer会解析它,然后按需下载。我个人觉得DASH比HLS更灵活,因为它支持更精细的码率切换粒度。但HLS的生态更成熟,很多CDN默认就支持。
10.4 缓存策略:让播放更流畅
车载场景下,网络不稳定是常态。进隧道、过山区,信号说断就断。所以缓存策略必须做好。
ExoPlayer提供了CacheDataSource,可以轻松实现缓存。
10.4.1 简单缓存实现
// 创建缓存目录
File cacheDir = new File(context.getCacheDir(), "exo_cache");
SimpleCache cache = new SimpleCache(cacheDir, new LeastRecentlyUsedCacheEvictor(1024 * 1024 * 200)); // 200MB缓存
// 创建CacheDataSource.Factory
CacheDataSource.Factory cacheFactory = new CacheDataSource.Factory()
.setCache(cache)
.setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory());
// 构建媒体源时使用缓存工厂
MediaSource mediaSource = new HlsMediaSource.Factory(cacheFactory)
.createMediaSource(mediaItem);
这里我用了LeastRecentlyUsedCacheEvictor,意思是当缓存满了,优先删除最久没用的数据。200MB的缓存大小,对于音乐来说够用了。一首高品质MP3大概10MB,能缓存20首歌。
10.4.2 预缓存策略
用户切歌时,如果等网络下载完再播放,体验很差。我建议做预缓存:在当前歌曲播放时,提前缓存下一首。
// 预缓存下一首
CacheDataSource.Factory cacheFactory = new CacheDataSource.Factory()
.setCache(cache)
.setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory());
CacheUtil.cache(
mediaItem,
cacheFactory,
cache,
new CacheUtil.CachingProgressListener() {
@Override
public void onCachingCompleted() {
Log.d("Cache", "预缓存完成");
}
}
);
注意,预缓存不要占用太多带宽。我一般限制预缓存速度为当前播放速度的50%,避免抢带宽导致当前歌曲卡顿。
10.4.3 缓存过期策略
音乐缓存不能无限增长。我建议设置过期时间,比如7天没听的歌自动清理。
// 自定义缓存过期逻辑
public class ExpiringCacheEvictor implements CacheEvictor {
private static final long EXPIRY_MS = 7 * 24 * 60 * 60 * 1000L; // 7天
@Override
public void onCacheInitialized() {}
@Override
public void onStartFile(Cache cache, String key, long position, long length) {}
@Override
public void onSpanAdded(Cache cache, CacheSpan span) {
// 检查所有缓存,删除过期的
for (CacheSpan s : cache.getCachedSpans()) {
if (System.currentTimeMillis() - s.lastAccessTimestamp > EXPIRY_MS) {
cache.removeSpan(s);
}
}
}
@Override
public void onSpanRemoved(Cache cache, CacheSpan span) {}
@Override
public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {}
}
嗯,这个逻辑有点粗糙,但够用。实际项目中,我还会结合数据库记录每首歌的缓存时间,更精确地控制。
10.5 知识体系总览
下面这张图,是我梳理的本章核心逻辑。你看一眼,心里就有数了。
10.6 避坑指南
最后,分享几个我踩过的坑。
坑1:HLS的TS分片不连续
我曾经遇到一个情况,服务端生成的HLS流,TS分片之间时间戳不连续,导致播放时声音画面不同步。解决办法是让服务端用hls_segment_type mpegts并保证PCR连续。如果服务端改不了,可以在客户端用HlsMediaSource.Factory设置setAllowChunklessPreparation(true),让ExoPlayer自己处理。
坑2:DASH的SegmentBase模式
有些DASH流用的是SegmentBase模式,而不是SegmentList。ExoPlayer默认对SegmentBase支持不太好,需要手动设置DashMediaSource.Factory的setLivePresentationDelayMs。我建议如果遇到DASH播不了,先检查mpd文件里的@type属性。
坑3:缓存文件被系统清理
车载系统有时会清理应用缓存。如果你的缓存目录在getCacheDir()下,系统可能随时删掉。我建议把缓存放在getExternalFilesDir("music_cache"),这样系统不会乱动。但要注意,外部存储可能被用户卸载SD卡时清空。
好了,这一章的内容就这些。ExoPlayer的集成其实不难,难的是把各种边界情况处理好。你只要把HLS/DASH的解析逻辑搞明白,缓存策略设计合理,车载音乐播放的体验就能上一个台阶。
最后提醒:调试时多看看ExoPlayer的日志,它打印的信息非常详细。遇到问题先搜ExoPlayerImpl的log,大部分坑都能找到答案。
公众号:蓝海资料掘金营,微信deep3321