19、网络流媒体播放:HTTP Live Streaming(HLS)、DASH 协议、缓存策略
说到网络流媒体,我估计做 Android 多媒体开发的同行们,十有八九都跟 HLS 和 DASH 打过交道。这两个协议,说白了就是解决「怎么让用户流畅地看视频」这个核心问题。我自己在几年前接手过一个直播项目,那时候对 HLS 的理解还停留在「切文件、拉文件」的层面,结果线上出了不少卡顿和起播慢的问题。嗯,从那以后,我算是把这两个协议彻底啃了一遍。
今天我们就来聊聊 HLS 和 DASH 的核心原理,以及在实际开发中怎么设计缓存策略。你想想看,用户在地铁里刷视频,网络忽快忽慢,如果播放器不能智能切换码率、不能合理缓存,那体验基本就崩了。
19.1 HLS 协议:苹果的「切片」哲学
HLS(HTTP Live Streaming)是苹果搞出来的。它的思路很简单:把一整段视频切成很多小片段,每个片段几秒钟。播放器拿到一个索引文件(m3u8),就知道去哪里下载这些片段。
我习惯把 HLS 的架构分成三层:
- 索引层:m3u8 文件,里面记录了视频片段的 URL 列表。
- 切片层:实际的 TS 或 fMP4 文件,每个文件几秒到十几秒。
- 播放层:播放器按顺序下载并播放这些切片。
举个例子,一个典型的 m3u8 文件长这样:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
segment_000.ts
#EXTINF:10.0,
segment_001.ts
#EXTINF:10.0,
segment_002.ts
这里有个坑,我曾经踩过:EXT-X-TARGETDURATION 这个字段,它表示每个切片的最大时长。如果服务器端生成的切片时长不稳定,播放器可能会提前请求下一个切片,导致 404 错误。所以,我建议在服务端做切片时,尽量保持时长一致,误差控制在 0.5 秒以内。
19.2 DASH 协议:MPEG 的「自适应」方案
DASH(Dynamic Adaptive Streaming over HTTP)是 MPEG 推出的标准。跟 HLS 相比,DASH 更灵活,支持多种编码格式和封装方式。它的核心是 MPD(Media Presentation Description)文件,相当于 HLS 的 m3u8。
我个人觉得 DASH 最大的优势是码率自适应做得更精细。MPD 文件里可以描述多个码率的视频流,播放器根据当前网络带宽,动态选择最合适的码率。举个例子:
<AdaptationSet mimeType="video/mp4">
<Representation id="1" bandwidth="500000" width="640" height="360">
<SegmentTemplate duration="2" startNumber="1" media="seg_$Number$.m4s" />
</Representation>
<Representation id="2" bandwidth="1000000" width="1280" height="720">
<SegmentTemplate duration="2" startNumber="1" media="seg_$Number$.m4s" />
</Representation>
</AdaptationSet>
这里有两个码率:500kbps 和 1000kbps。播放器会监测下载速度,如果带宽足够,就切到高码率;如果网络变差,就降回低码率。我在项目中遇到过一个问题:切换码率时画面会闪一下。后来发现是解码器在切换分辨率时需要重新初始化。解决办法是使用 fMP4 格式,它支持在同一个解码会话中切换分辨率,减少黑屏时间。
19.3 缓存策略:让播放更流畅
缓存策略,说白了就是「什么时候下载、下载多少、什么时候扔掉」。我见过不少开发者直接把所有切片都下载到本地,结果内存爆了。嗯,这里要注意,缓存不是越多越好,而是要恰到好处。
我总结了一套比较实用的缓存策略,分为三个层面:
19.3.1 内存缓存
内存缓存用于存放即将播放的切片。我习惯用 LRU(最近最少使用) 算法来管理。比如,设置最大缓存 50MB,当新切片加入时,如果超过限制,就移除最久没被访问的切片。
public class VideoCacheManager {
private final LruCache<String, byte[]> cache;
public VideoCacheManager(int maxSize) {
// 最大缓存 50MB
this.cache = new LruCache<>(maxSize);
}
public void put(String url, byte[] data) {
cache.put(url, data);
}
public byte[] get(String url) {
return cache.get(url);
}
}
这里有个细节:不要缓存整个视频。你想想看,用户可能只看了前 30 秒就划走了,缓存整个视频完全是浪费。我一般只缓存当前播放位置前后各 30 秒的切片。
19.3.2 磁盘缓存
磁盘缓存用于持久化存储,适合用户反复观看的内容。比如,用户看了一个教程视频,下次打开时可以直接从磁盘读取,不用重新下载。
我建议使用 OkHttp 的缓存机制,它支持磁盘缓存,而且可以设置缓存大小和过期时间。举个例子:
Cache diskCache = new Cache(cacheDir, 100 * 1024 * 1024); // 100MB
OkHttpClient client = new OkHttpClient.Builder()
.cache(diskCache)
.build();
不过要注意,HLS 和 DASH 的切片文件通常不设置 Cache-Control 头,所以 OkHttp 默认不会缓存。你需要手动配置拦截器,或者自己实现一个简单的文件缓存。
19.3.3 预加载策略
预加载是提升起播速度的关键。我习惯在用户点击播放按钮之前,就开始下载第一个切片。比如,在列表页展示视频封面时,后台悄悄下载第一个切片。这样用户点击播放时,几乎可以秒开。
我曾经在项目中用 ExoPlayer 的 PreloadManager 来实现预加载。它支持同时预加载多个视频,而且可以控制预加载的优先级。核心代码大概这样:
PreloadManager preloadManager = new PreloadManager.Builder(context)
.setMaxPreloadCount(3) // 最多预加载 3 个视频
.setPreloadDuration(5000) // 每个视频预加载 5 秒
.build();
preloadManager.startPreload(videoUrl);
19.4 知识体系总览
下面这张图,我把 HLS、DASH 和缓存策略的核心逻辑串起来了。你可以看到,从协议选择到缓存实现,每一步都有对应的技术决策。
19.5 避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路:
- HLS 的 EXT-X-DISCONTINUITY:当视频流发生编码格式变化时(比如从 H.264 切到 H.265),m3u8 里会插入这个标签。如果播放器没处理好,画面会花屏。我建议在解析 m3u8 时,遇到这个标签就重置解码器。
- DASH 的 SegmentTimeline:有些 MPD 文件使用 SegmentTimeline 来描述切片时长,而不是固定的 duration。如果解析逻辑写死了,可能会漏掉切片。我习惯用 ExoPlayer 的 DashChunkSource,它已经处理好了这些细节。
- 缓存过期:磁盘缓存如果设置不当,会导致用户看到旧内容。我一般给缓存设置一个最大存活时间,比如 24 小时。超过时间的切片,强制重新下载。
- 内存泄漏:缓存管理器的生命周期一定要跟 Activity 或 Fragment 绑定。我曾经在 Application 里持有一个全局缓存,结果导致内存泄漏。后来改用 WeakReference 来持有缓存数据,问题才解决。
好了,关于 HLS、DASH 和缓存策略,今天就聊到这里。这些内容在实际项目中非常实用,尤其是缓存策略,直接决定了用户的播放体验。如果你在实现过程中遇到问题,欢迎随时交流。