21、Android ExoPlayer集成RTMP:ExoPlayer架构、RTMP扩展库集成、自定义RTMP数据源
聊到Android上的播放器,ExoPlayer绝对是绕不开的。我最早接触它的时候,Google刚把它从官方的Demo项目里拎出来,变成一个独立的库。说实话,那时候MediaPlayer还能打,但ExoPlayer的扩展性让我眼前一亮——尤其是它对RTMP这类协议的支持方式。
嗯,今天我们就来拆解一下,怎么让ExoPlayer老老实实地播放RTMP流。
ExoPlayer的架构:它到底怎么工作的?
先简单过一下ExoPlayer的架构。你想想看,一个播放器无非就是:拿数据、解封装、解码、渲染。ExoPlayer把这几个步骤拆成了几个核心组件:
- MediaSource:数据源,负责提供媒体数据块。这是我们的切入点。
- Renderer:渲染器,处理音频、视频、字幕等轨道的渲染。
- TrackSelector:轨道选择器,决定播放哪条音视频轨道。
- LoadControl:加载控制,管理缓冲策略。
ExoPlayer不像MediaPlayer那样把一切都封装死。它给了你一个骨架,你可以往里面塞不同的“器官”。比如,你想支持RTMP,那就得在MediaSource这一层动手脚。
核心思路:ExoPlayer本身不直接支持RTMP。它通过扩展库或自定义MediaSource来接入。说白了,就是把RTMP流包装成ExoPlayer能理解的格式。
RTMP扩展库集成:最省力的方式
Google官方其实提供了一个RTMP扩展库——extension-rtmp。这个库封装了RTMP协议的底层细节,你只需要在Gradle里加一行依赖,然后创建一个RtmpDataSourceFactory就行。
我在项目中用过这个方案,集成起来确实快。但有个坑——这个库依赖了librtmp的JNI绑定,所以编译时要注意NDK版本。我曾经因为NDK版本不匹配,折腾了半天才发现是so库没加载成功。
集成步骤
- 在
build.gradle中添加依赖:
implementation 'com.google.android.exoplayer:extension-rtmp:2.19.1'
- 创建播放器时,使用
RtmpDataSourceFactory:
RtmpDataSourceFactory rtmpDataSourceFactory = new RtmpDataSourceFactory();
MediaSource mediaSource = new ProgressiveMediaSource.Factory(rtmpDataSourceFactory)
.createMediaSource(MediaItem.fromUri("rtmp://your-stream-url"));
player.setMediaSource(mediaSource);
player.prepare();
player.play();
就这么简单?嗯,对于大多数标准RTMP流,确实够了。但如果你遇到需要自定义握手、添加鉴权参数、或者处理特殊编码的情况,这个扩展库可能就不够灵活了。
我的建议:如果只是播放公开的RTMP流,直接用extension-rtmp。但如果流有特殊要求,比如需要携带自定义的AMF数据,那就得考虑自定义数据源了。
自定义RTMP数据源:从零开始造轮子
为什么要自定义?说白了,官方扩展库把底层细节都藏起来了。你没法控制连接过程、没法插入自定义逻辑。我遇到过的一个场景是:RTMP流需要先通过HTTP获取一个动态的token,然后把这个token拼接到RTMP URL的query参数里。官方库不支持这种操作。
所以,我们需要实现一个自定义的DataSource。ExoPlayer的DataSource接口很简单,核心就几个方法:
open(DataSpec):打开数据源,返回数据长度。read(byte[], int, int):读取数据块。close():关闭连接。
我们要做的,就是在这个接口背后,用Java/Kotlin实现RTMP协议的握手、连接、接收音视频数据。
实现思路
我不会把完整的RTMP协议实现贴出来(那得写一本书),但核心流程是这样的:
- 握手:客户端发送C0、C1、C2,服务端回复S0、S1、S2。
- 连接:发送
connect命令,带上应用名(比如live)。 - 创建流:发送
createStream命令,获取流ID。 - 播放:发送
play命令,开始接收音视频数据。 - 读取数据:循环读取RTMP chunk,解析出音频或视频数据,通过
read()返回给ExoPlayer。
注意:RTMP协议是TCP长连接,数据是分块的(chunk)。每个chunk有头部和负载。解析时要注意chunk stream ID和message type ID。我刚开始写的时候,经常把音频和视频的chunk搞混,导致播放器只出画面没声音。
代码骨架
public class RtmpDataSource implements DataSource {
private Socket socket;
private InputStream inputStream;
private OutputStream outputStream;
@Override
public long open(DataSpec dataSpec) throws IOException {
// 1. 解析URL,获取主机、端口、应用名、流名
// 2. 建立TCP连接
// 3. 完成RTMP握手
// 4. 发送connect命令
// 5. 发送createStream命令
// 6. 发送play命令
// 7. 返回数据长度(通常返回C.LENGTH_UNSET,因为RTMP是实时流)
return C.LENGTH_UNSET;
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
// 1. 从socket读取一个RTMP chunk
// 2. 解析chunk头部,判断是音频还是视频
// 3. 如果是视频,提取H.264/H.265的NALU
// 4. 如果是音频,提取AAC/MP3的帧数据
// 5. 将数据拷贝到buffer中返回
// 注意:ExoPlayer期望返回的是原始媒体数据(不含RTMP头部)
return bytesRead;
}
@Override
public void close() throws IOException {
// 发送close命令,关闭socket
}
}
你看,核心就是read()方法。ExoPlayer会不断调用它来获取数据。你的任务就是把RTMP流里的音视频数据“剥”出来,塞给ExoPlayer。
架构图:ExoPlayer + RTMP 数据流
下面这张图展示了整个数据流动的过程。我画的时候特意把自定义数据源的位置标红了,方便你理解它在整个架构中的角色。
避坑指南:我踩过的几个坑
做RTMP集成,有几个地方特别容易出问题。我列出来,你遇到了可以少走弯路。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 播放卡顿、缓冲频繁 | RTMP数据读取速度跟不上解码速度,或者网络抖动 | 调整LoadControl的缓冲策略,增加最小缓冲时长 |
| 只有画面没有声音 | RTMP chunk解析时,音频数据被错误地当作视频数据处理了 | 检查chunk头部的message type ID,音频是8,视频是9 |
| 连接超时 | RTMP握手阶段,服务端没有及时响应 | 设置Socket超时时间,并在超时后重试 |
| 花屏或绿屏 | H.264的SPS/PPS数据没有正确传递给解码器 | 在第一个视频chunk中提取SPS/PPS,通过MediaFormat设置给MediaCodec |
调试技巧:我习惯在read()方法里加日志,打印每次读取的字节数和chunk类型。这样能快速定位是数据没读到,还是解析逻辑有问题。
总结
ExoPlayer集成RTMP,说白了就两条路:要么用官方扩展库省心省力,要么自己写DataSource掌控一切。我个人建议,如果是商业项目,优先考虑自定义数据源——虽然开发成本高一点,但后期维护和扩展会灵活很多。
嗯,RTMP这块水挺深的,但只要你把握手、chunk解析、数据提取这几个环节搞明白,剩下的就是体力活了。希望今天的内容能帮你少踩几个坑。
公众号:蓝海资料掘金营,微信deep3321