18、ExoPlayer进阶:自适应码率播放(HLS/DASH)、DRM支持、缓存策略
说实话,自适应码率播放这块,是ExoPlayer最让我觉得“真香”的地方。你想想看,用户网络环境千差万别,有的在5G满格,有的在地铁里信号断断续续。如果播放器不能自动调整码率,那体验基本就崩了。我最早做播放器的时候,用的还是硬编码切换码率的方式,那叫一个痛苦。后来切到ExoPlayer,才发现原来这些事框架已经帮你做好了。
这一章,我们就来聊聊ExoPlayer在自适应码率、DRM和缓存策略上的实战用法。说白了,就是让你的播放器更聪明、更安全、更流畅。
18.1 自适应码率播放:HLS与DASH
自适应码率,英文叫ABR(Adaptive Bitrate)。它的核心逻辑很简单:播放器根据当前网络带宽,动态选择最合适的视频分片来播放。网络好就上高清,网络差就降流畅度,中间不卡顿、不中断。
ExoPlayer对HLS和DASH的支持非常成熟。我个人的习惯是,如果面向苹果生态多,优先用HLS;如果是Android为主,DASH更灵活。但实际项目中,两者都得支持。
18.1.1 HLS播放示例
HLS的入口是.m3u8文件。ExoPlayer解析这个文件后,会自动拉取对应的TS或fMP4分片。你只需要把Uri传给播放器就行。
// 创建HLS媒体源
val hlsMediaSource = HlsMediaSource.Factory(
DefaultHttpDataSource.Factory()
).createMediaSource(MediaItem.fromUri(hlsUrl))
// 设置到播放器
player.setMediaSource(hlsMediaSource)
player.prepare()
player.playWhenReady = true
嗯,这里要注意:HLS的m3u8文件里会列出多个码率的流。ExoPlayer默认会使用自适应算法来选。如果你不想自适应,也可以强制指定一个码率。
18.1.2 DASH播放示例
DASH的入口是.mpd文件。ExoPlayer同样原生支持。代码结构和HLS几乎一样,只是MediaSource工厂不同。
// 创建DASH媒体源
val dashMediaSource = DashMediaSource.Factory(
DefaultHttpDataSource.Factory()
).createMediaSource(MediaItem.fromUri(dashUrl))
player.setMediaSource(dashMediaSource)
player.prepare()
DASH的优势在于它支持更精细的码率分段,比如按分辨率、帧率、甚至编码格式来分。我在做海外项目时,DASH用得更多,因为它的标准化程度高,兼容性好。
18.1.3 自适应算法选择
ExoPlayer内置了好几种ABR算法。默认用的是DefaultAdaptiveTrackSelectionFactory,它基于带宽估算来做决策。但说实话,默认算法不一定适合所有场景。
| 算法 | 特点 | 适用场景 |
|---|---|---|
| DefaultAdaptiveTrackSelection | 基于带宽估算,平滑切换 | 大多数通用场景 |
| RandomAdaptiveTrackSelection | 随机选择码率 | 测试或调试用 |
| FixedTrackSelection | 固定码率,不自适应 | 需要强制指定清晰度时 |
我个人建议,如果你对用户体验要求高,可以自定义一个ABR算法。比如,结合电池电量、屏幕尺寸、甚至用户是否在滑动页面来做决策。我曾经在一个短视频项目里,就自己写了一个“省电优先”的ABR策略,效果还不错。
18.2 DRM支持:保护你的内容
DRM(数字版权管理)是播放器开发里绕不开的话题。尤其是做付费视频、直播、或者企业内训视频时,没有DRM,内容分分钟被扒走。
ExoPlayer支持多种DRM方案,包括Widevine、PlayReady、ClearKey等。其中Widevine是Android生态里最主流的。它分三个安全级别:L1(硬件安全)、L2(软件安全)、L3(无安全)。
18.2.1 集成Widevine DRM
集成DRM其实不复杂,核心就是设置一个MediaItem的DRM配置。你需要从内容提供商那里拿到License URL和一些凭证。
// 构建DRM配置
val drmConfiguration = MediaItem.DrmConfiguration.Builder(
C.WIDEVINE_UUID
).setLicenseUri(licenseUrl)
.setLicenseRequestHeaders(mapOf("Authorization" to "Bearer your_token"))
.build()
// 创建MediaItem
val mediaItem = MediaItem.Builder()
.setUri(streamUrl)
.setDrmConfiguration(drmConfiguration)
.build()
player.setMediaItem(mediaItem)
player.prepare()
这里有个坑:License请求是异步的,而且可能会失败。我曾经遇到过License服务器返回了错误码,但播放器没有给出明确的错误提示,导致用户以为视频坏了。后来我加了Player.Listener里的onDrmSessionManagerError回调,才把问题抓出来。
18.2.2 多DRM方案兼容
有些平台会同时提供Widevine和PlayReady。ExoPlayer可以自动识别UUID来选择对应的DRM方案。你只需要在DrmConfiguration里指定正确的UUID即可。
我个人的做法是,在后台接口里返回DRM类型字段,前端根据这个字段动态构建配置。这样即使以后换了DRM提供商,前端代码也不用大改。
18.3 缓存策略:让播放更流畅
缓存是提升用户体验的利器。尤其是用户重复观看同一个视频时,如果每次都重新下载,既浪费流量又慢。ExoPlayer提供了非常灵活的缓存机制。
18.3.1 使用SimpleCache
ExoPlayer的缓存核心是SimpleCache。它可以把下载的分片存到本地,下次播放时直接从本地读取。
// 创建缓存
val cache = SimpleCache(
cacheDir, // 缓存目录
LeastRecentlyUsedCacheEvictor(maxCacheSize), // 淘汰策略
DatabaseProvider() // 元数据存储
)
// 创建缓存数据源工厂
val cacheDataSourceFactory = CacheDataSource.Factory()
.setCache(cache)
.setUpstreamDataSourceFactory(DefaultHttpDataSource.Factory())
// 使用缓存工厂创建媒体源
val mediaSource = HlsMediaSource.Factory(cacheDataSourceFactory)
.createMediaSource(MediaItem.fromUri(hlsUrl))
你看,代码量不大。但这里有个关键点:缓存目录要选对。我建议用context.cacheDir,而不是外部存储。因为系统在存储空间不足时,会自动清理这个目录。
18.3.2 缓存策略的进阶玩法
除了基本的缓存,你还可以做几件事:
- 预缓存: 用户还没点播放,你就提前下载一部分分片。我曾在新闻类App里用过这个策略,用户滑动到视频条目时,后台就开始缓存前几个分片,点击播放时几乎秒开。
- 缓存限速: 避免缓存占用太多带宽,影响其他网络请求。ExoPlayer的
CacheDataSource支持设置带宽限制。 - 缓存过期: 有些内容有时效性,比如直播回放。你可以定期清理过期缓存,或者根据业务逻辑手动删除。
18.4 知识体系总览
为了让你更直观地理解这一章的内容,我画了一张图。它把自适应码率、DRM和缓存策略的核心关系展示了出来。
从图上你可以看到,ExoPlayer的核心能力就是围绕这三个方向展开的。自适应码率解决网络波动问题,DRM解决内容安全问题,缓存解决重复加载问题。三者配合好了,播放体验才能上一个台阶。
好了,这一章的内容就到这里。自适应码率、DRM和缓存,每一个点单独拿出来都能写一篇长文。但实战中,你只需要记住一个原则:用户感知到的流畅度,比任何技术指标都重要。代码写得再漂亮,用户觉得卡,那就是失败。