ExoPlayer 深度使用:自定义渲染器、DASH/HLS/SmoothStreaming、DRM支持

说实话,ExoPlayer 这玩意儿,我用得越久越觉得它香。官方 MediaPlayer 能做的事它都能做,官方做不了的,它也能做。今天咱们就聊聊它的几个高阶玩法——自定义渲染器、多协议流媒体支持,还有 DRM 那点事。

一、自定义渲染器:让 ExoPlayer 听懂你的“方言”

ExoPlayer 默认支持的音视频格式其实挺全的,但总有例外。我记得有一次项目里需要播放一个特殊编码的音频流,ExoPlayer 直接罢工了。怎么办?自己写渲染器呗。

说白了,渲染器就是 ExoPlayer 里的“翻译官”。它负责把解码后的数据交给硬件去展示或播放。自定义渲染器,就是让你自己定义这个“翻译”过程。

核心思路:继承 BaseRenderer,实现几个关键方法。

public class MyAudioRenderer extends BaseRenderer {

    public MyAudioRenderer() {
        super(C.TRACK_TYPE_AUDIO);
    }

    @Override
    public void render(long positionUs, long elapsedRealtimeUs) {
        // 在这里处理解码后的音频数据
        // 我习惯在这里做音效处理或数据转发
    }

    @Override
    protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
        // 流切换时的处理
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public boolean isEnded() {
        return false;
    }
}

你可能会问:“我什么时候需要自定义渲染器?”嗯,场景其实不少:

  • 你想在播放前对音频做实时变声处理
  • 你需要把视频帧数据传给第三方美颜 SDK
  • 你想把渲染后的数据同时写入本地文件做录制

我曾经在一个直播项目里,就是靠自定义渲染器把视频帧抽出来,做了个实时人脸贴纸功能。效果还不错,就是调试时有点头疼。

小技巧:自定义渲染器里别忘了处理 reset()flush(),否则播放器 Seek 时容易出问题。我踩过这个坑。

二、DASH / HLS / SmoothStreaming:自适应流媒体三兄弟

这三个协议,说白了都是“把大文件切成小片,根据网速动态切换清晰度”。ExoPlayer 对它们的支持可以说是原生级别的,但配置上还是有些门道。

2.1 DASH(Dynamic Adaptive Streaming over HTTP)

DASH 是 Google 亲儿子,ExoPlayer 对它的支持最完善。配置起来也很简单:

// 创建 DASH 媒体源
DashMediaSource.Factory dashFactory = new DashMediaSource.Factory(
        new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
        buildDataSourceFactory()
);
DashMediaSource dashSource = dashFactory.createMediaSource(MediaItem.fromUri(dashUrl));

我个人习惯在 DASH 流里重点关注 SegmentBase 的配置。有些 CDN 返回的 MPD 文件里,segment 的 URL 模板写得不规范,会导致播放卡顿。遇到这种情况,我一般会手动解析 MPD 并修正 URL。

2.2 HLS(HTTP Live Streaming)

苹果家的协议,但在 Android 上 ExoPlayer 照样玩得转。HLS 的配置稍微复杂一点,主要是因为它有 .m3u8 索引文件:

// 创建 HLS 媒体源
HlsMediaSource.Factory hlsFactory = new HlsMediaSource.Factory(mediaDataSourceFactory);
HlsMediaSource hlsSource = hlsFactory.createMediaSource(MediaItem.fromUri(hlsUrl));

这里有个坑——HLS 的 TS 分片有时会带奇怪的 PTS 跳变。ExoPlayer 默认会尝试修复,但如果你发现播放时画面突然卡一下,可以试试关闭 allowChunklessPreparation

hlsFactory.setAllowChunklessPreparation(false);

2.3 SmoothStreaming

微软家的协议,现在用得少了,但一些老项目还在用。ExoPlayer 支持它,不过需要额外引入依赖:

implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.19.1'

配置方式和前两个类似,我就不重复贴代码了。我想说的是,SmoothStreaming 的 Manifest 文件(.ismc)解析起来比 DASH 的 MPD 要简单,但它的分片命名规则比较死板,遇到自定义命名的 CDN 就得自己写 UriBuilder

协议 ExoPlayer 支持度 常见坑点 我的建议
DASH ★★★★★ MPD 中 Segment 模板不规范 优先使用,兼容性最好
HLS ★★★★☆ TS 分片 PTS 跳变 注意关闭 chunkless preparation
SmoothStreaming ★★★☆☆ 分片命名规则死板 老项目维护时再用

三、DRM 支持:给内容加把锁

DRM 这东西,说白了就是版权保护。ExoPlayer 支持 Widevine、PlayReady、ClearKey 等主流方案。配置起来其实不复杂,但坑不少。

3.1 Widevine 配置

Widevine 是 Android 上最常用的 DRM 方案。ExoPlayer 通过 MediaItemDrmConfiguration 来配置:

MediaItem mediaItem = new MediaItem.Builder()
        .setUri(videoUrl)
        .setDrmConfiguration(
                new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
                        .setLicenseUri(licenseUrl)
                        .setLicenseRequestHeaders(licenseHeaders)
                        .build()
        )
        .build();

这里有个关键点——licenseUrl 是获取许可证的地址。我遇到过好几次,许可证服务器要求 POST 请求带特定的 Header,但 ExoPlayer 默认只带了基本的 User-Agent。解决办法是自定义 HttpDataSource.Factory

HttpDataSource.Factory dataSourceFactory = new DefaultHttpDataSource.Factory()
        .setDefaultRequestProperties(licenseHeaders);

3.2 离线 DRM 播放

有些场景需要用户下载视频后离线观看。ExoPlayer 的 DownloadManager 支持离线 DRM,但需要额外处理许可证的持久化:

// 下载时保存许可证
DrmSessionManager drmSessionManager = new DefaultDrmSessionManager.Builder()
        .setUuidAndExoMediaDrmProvider(C.WIDEVINE_UUID, FrameworkMediaDrm.DEFAULT_PROVIDER)
        .setMultiSession(true)
        .build(drmDataSourceFactory);

// 播放时恢复许可证
// 注意:许可证的持久化需要 License 服务器支持 offline 模式

警告:不是所有 Widevine 服务器都支持离线许可证。我曾经对接过一个厂商,他们的 License 服务器只支持 streaming 模式,离线播放时直接报错。所以,做离线 DRM 前一定要先确认服务器能力。

3.3 ClearKey 方案

如果你只是做本地测试,ClearKey 是最简单的 DRM 方案。它不需要 License 服务器,直接在客户端配置密钥:

MediaItem mediaItem = new MediaItem.Builder()
        .setUri(videoUrl)
        .setDrmConfiguration(
                new MediaItem.DrmConfiguration.Builder(C.CLEARKEY_UUID)
                        .setKeySetId(keySetId)
                        .build()
        )
        .build();

嗯,ClearKey 说白了就是“明文密钥”,安全性不高,但用来调试 DRM 流程非常方便。

四、知识体系总览

说了这么多,我画了张图帮你理清思路。ExoPlayer 深度使用的核心就这三块:

ExoPlayer 深度使用 自定义渲染器 继承 BaseRenderer 实现 render() / isReady() / isEnded() 处理 reset() / flush() 避免 Seek 问题 DASH / HLS / SmoothStreaming DASH:MPD 解析 + Segment 模板 HLS:m3u8 索引 + TS 分片处理 SmoothStreaming:ismc 解析 + UriBuilder DRM 支持 Widevine:License URL + Header 配置 离线 DRM:许可证持久化 + DownloadManager ClearKey:本地密钥配置(调试用) 核心:理解协议差异 → 掌握渲染器扩展 → 搞定 DRM 流程

你看,这三块内容其实是环环相扣的。自定义渲染器解决的是“怎么播”的问题,多协议支持解决的是“播什么”的问题,DRM 解决的是“能不能播”的问题。把它们串起来,你就能应对绝大多数播放场景了。

我的经验:刚开始接触 ExoPlayer 深度定制时,别急着写代码。先把 ExoPlayer 的源码结构摸一遍,重点看 RendererMediaSourceDrmSessionManager 这三个类的继承关系。搞懂了它们,你就能像搭积木一样组合各种功能了。

好了,今天就聊到这儿。ExoPlayer 这东西,越用越觉得它设计得巧妙。下次遇到播放相关的需求,别急着找第三方库,先想想 ExoPlayer 能不能自己搞定——很多时候,它比你想象的更强大。

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