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首歌。

小技巧:缓存目录最好放在外部存储(如SD卡),避免占用系统分区空间。车载设备通常有专门的媒体存储分区。

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 知识体系总览

下面这张图,是我梳理的本章核心逻辑。你看一眼,心里就有数了。

在线音乐集成核心架构 ExoPlayer 播放器 HLS 流播放 DASH 流播放 缓存策略 • m3u8 解析 • 自适应码率切换 • TrackSelectionParameters • mpd 解析 • 分段下载 • 码率自适应 • CacheDataSource • 预缓存 • 过期策略 关键:硬件解码兼容性 + 缓存大小控制 + 带宽管理

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.FactorysetLivePresentationDelayMs。我建议如果遇到DASH播不了,先检查mpd文件里的@type属性。

坑3:缓存文件被系统清理

车载系统有时会清理应用缓存。如果你的缓存目录在getCacheDir()下,系统可能随时删掉。我建议把缓存放在getExternalFilesDir("music_cache"),这样系统不会乱动。但要注意,外部存储可能被用户卸载SD卡时清空。

好了,这一章的内容就这些。ExoPlayer的集成其实不难,难的是把各种边界情况处理好。你只要把HLS/DASH的解析逻辑搞明白,缓存策略设计合理,车载音乐播放的体验就能上一个台阶。

最后提醒:调试时多看看ExoPlayer的日志,它打印的信息非常详细。遇到问题先搜ExoPlayerImpl的log,大部分坑都能找到答案。


公众号:蓝海资料掘金营,微信deep3321