摘要:本文以大牛直播SDK(SmartMediaKit)为基础,结合RTMP协议规范,详细讲解如何在Android平台上实现超低延迟的RTMP/RTSP直播播放器,包括架构设计、核心模块封装、参数调优、录像管理以及回放播放等完整工程实践,所有代码均来自真实Demo工程。
一、前言:直播播放的延迟困局与RTMP的价值
在移动直播、工业监控、智慧教育等实时性要求极高的场景中,端到端延迟是衡量播放器优劣最核心的指标。延迟过高不仅破坏互动体验,在远程监控、应急调度等场景中甚至会带来安全隐患。
主流传输协议的延迟对比如下:
| 协议 | 典型延迟 | 特点 |
|---|---|---|
| HLS | 10~30s | 分片式HTTP,CDN友好,高延迟 |
| DASH | 3~8s | 自适应码率,延迟较高 |
| WebRTC | <1s | 超低延迟,P2P为主,穿透复杂 |
| RTMP | 0.5~3s | 基于TCP长连接,低延迟,成熟稳定,大牛直播SDK的可以做到100-200ms延迟 |
| RTSP | 0.5~2s | 支持UDP/TCP,安防设备标准协议,大牛直播SDK的可以做到100-200ms延迟 |
RTMP(Real-Time Messaging Protocol)最初由Macromedia设计,后被Adobe收购并公开规范。尽管Adobe官方已停止维护Flash,RTMP在直播推流/拉流领域依然是事实标准,原因在于:
- 基于TCP长连接:连接建立后复用同一条TCP链路,消除了HTTP短连接的握手开销;
- 分块传输机制(Chunk Stream):将大消息切分为固定大小的Chunk(默认128字节),允许多路消息交错传输,降低大包阻塞风险;
- 流控与时序保障:内置流控窗口(Window Acknowledgement Size)和带宽控制(Set Peer Bandwidth)消息,保障弱网稳定性;
- Enhanced RTMP扩展:现代SDK已支持HEVC/AV1编码的Enhanced RTMP,突破了原规范只支持H.264/AAC的限制。
本文将结合实际工程代码,以RTMP为例,深入讲解如何基于大牛直播SDK在Android上构建一套工程级的RTMP超低延迟播放器。
Android平台RTMP直播播放器延迟测试
二、RTMP协议核心规范回顾
在进入代码讲解之前,有必要梳理RTMP核心流程,因为SDK的很多参数设计直接对应协议层的行为。
2.1 握手阶段(Handshake)
RTMP握手分三个阶段:
Client Server
|------ C0 (1 byte version) ------>|
|------ C1 (1536 bytes) ---------> |
|<------ S0+S1+S2 (3073 bytes)---|
|------ C2 (1536 bytes) ---------> |
- C0/S0 :版本标识,通常为
0x03(RTMP 3.0); - C1/S1:包含时间戳(4字节)+ 随机数(1528字节),用于混淆;
- C2/S2:对方 C1/S1 的回显,握手完成后进入消息交互阶段。
完成握手后,客户端发送 connect 命令消息,建立逻辑连接(Application Level),之后通过 createStream → play 消息序列拉流。
2.2 消息分块(Chunking)
RTMP最重要的优化机制是分块传输。每个Chunk由 Chunk Header + Chunk Data 组成:
+------------------+ +------------------+
| Chunk Header | | Chunk Data |
| Basic Header | | (payload data) |
| Message Header | | |
| Extended Time | | |
+------------------+ +------------------+
Basic Header中的 csid(Chunk Stream ID) 标识消息流,fmt字段(2 bit)决定 Message Header 的长度(完整/相对/仅时间戳/无),极大节省了头部开销。
默认Chunk Size为128字节 ,可通过 Set Chunk Size 控制消息调整,大牛直播SDK在实现时会自动协商最优的Chunk Size,开发者无需手动干预。
2.3 消息类型与时序
播放器收到的核心消息类型:
| Message Type ID | 类型 | 用途 |
|---|---|---|
| 1 | Set Chunk Size | 协商分块大小 |
| 3 | Acknowledgement | 确认窗口字节数 |
| 5 | Window Ack Size | 设置确认窗口 |
| 6 | Set Peer Bandwidth | 设置带宽 |
| 8 | Audio | 音频数据(FLV AudioTag格式) |
| 9 | Video | 视频数据(FLV VideoTag格式) |
| 18 | AMF0 Data | 元数据(onMetaData) |
| 20 | AMF0 Command | 命令消息(connect/play等) |
音视频包的时间戳遵循**DTS(解码时间戳)**语义,SDK内部会根据PTS/DTS差值做音视频同步与抖动缓冲处理,这正是"缓冲时间"参数(SmartPlayerSetBuffer)发挥作用的地方。
2.4 Enhanced RTMP 与 HEVC 支持
原始RTMP规范的 VideoTagHeader 中,CodecID = 7 固定表示 AVC/H.264。Enhanced RTMP(由 Veovera 发布的扩展规范)通过修改 VideoTagHeader 的高4位来扩展编码器支持:
ExVideoTagHeader:
- IsExHeader = 1 (bit 7 set)
- FrameType (bits 4-6)
- PacketType (bits 0-3)
- FourCC (4 bytes): 'hvc1', 'av01', 'vp09' etc.
大牛直播SDK默认开启 Enhanced RTMP 支持,可通过 DisableEnhancedRTMP 接口关闭(向后兼容老服务端):
java
// SDK默认开启 Enhanced RTMP,如需关闭:
libPlayer.DisableEnhancedRTMP(handle, 1);
三、工程架构设计

3.1 整体模块划分
基于实际工程,播放器的完整模块架构如下:
bash
┌───────────────────────────────────────────────────────┐
│ SmartPlayer (Activity) │
│ UI控制层:播放控制 / 录像 / 快照 / 音量 / 翻转旋转 │
└──────────────────────┬────────────────────────────────┘
│
┌──────────────────────▼────────────────────────────────┐
│ LibPlayerWrapper (封装层) │
│ 状态管理:is_playing / is_pulling / is_recording │
│ 读写锁保护:并发安全的Start/Stop操作 │
│ 生命周期:set / release / try_release / close │
└──────────────────────┬────────────────────────────────┘
│
┌──────────────────────▼────────────────────────────────┐
│ SmartPlayerJniV2 (JNI接口层) │
│ Native方法:SmartPlayerOpen / StartPlay / StopPlay │
│ 参数配置:Buffer / 硬解码 / 低延迟 / 快速启动 │
│ 回调设置:事件回调 / UserData / 音视频数据回调 │
└──────────────────────┬────────────────────────────────┘
│
┌──────────────────────▼────────────────────────────────┐
│ libSmartPlayer.so (Native核心) │
│ 协议栈:RTMP/RTSP解析 / 音视频解复用 │
│ 解码器:H.264/H.265 软/硬解码 │
│ 渲染层:OpenGL ES / MediaCodec Surface渲染 │
└───────────────────────────────────────────────────────┘
┌─────────────────┐ ┌──────────────────────────────┐
│ RecorderManager │ │ RecorderPlayback │
│ 录像文件管理 │ │ 本地录像回放(VideoView) │
│ 异步加载/删除 │ │ 精准Seek / 进度控制 │
└─────────────────┘ └──────────────────────────────┘
3.2 核心设计原则
状态机保护 :播放(Playing)、拉流(Pulling)、录像(Recording)三个状态相互独立,通过 volatile 标记 + 读写锁保证多线程安全,避免重复Start/Stop导致的Native层崩溃。
资源生命周期管理 :LibPlayerWrapper 实现了 AutoCloseable,同时重写了 finalize() 作为兜底保护,确保Native句柄不泄漏。
弱引用Handler防内存泄漏 :UI回调通过静态内部类 MainHandler + WeakReference 持有 Activity,规避了内部类导致的内存泄漏问题。
四、核心封装层:LibPlayerWrapper 详解
LibPlayerWrapper 是整个播放器最核心的封装类,负责管理Native句柄的生命周期、运行状态以及线程安全。
4.1 状态管理与读写锁
java
public class LibPlayerWrapper implements AutoCloseable {
private final ReadWriteLock rw_lock_ = new ReentrantReadWriteLock(true);
private final java.util.concurrent.locks.Lock write_lock_ = rw_lock_.writeLock();
private final java.util.concurrent.locks.Lock read_lock_ = rw_lock_.readLock();
private SmartPlayerJniV2 lib_player_;
private volatile long native_handle_;
// 三个独立的运行状态标志
private volatile boolean is_playing_;
private volatile boolean is_pulling_;
private volatile boolean is_recording_;
使用 ReentrantReadWriteLock 的意义在于:状态读取(is_playing等)频率远高于状态写入,读写锁允许多个读操作并发执行,只在状态变更时加写锁,显著降低了锁竞争。
volatile 关键字保证了多线程可见性,但对复合操作(read-check-write)仍需要锁保护。
4.2 安全的 Start/Stop 模式
以 StartPlayer 和 StopPlayer 为例,展示标准的并发安全模式:
java
public boolean StartPlayer(SurfaceView view, Surface surface, NTExternalRender external_render,
int render_scale_mode, boolean is_fast_startup,
boolean is_hardware_decoder, boolean is_low_latency,
boolean is_hw_render_mode) {
// 1. 前置检查:句柄有效 + 未处于播放状态
if (!check_native_handle()) return false;
if (is_playing()) {
Log.e(TAG, "already playing, native_handle:" + get());
return false;
}
// 2. 配置播放参数
lib_player_.SmartPlayerSetSurface(get(), view);
lib_player_.SmartPlayerSetExternalRender(get(), external_render);
lib_player_.SmartPlayerSetRenderScaleMode(get(), render_scale_mode);
lib_player_.SmartPlayerSetFastStartup(get(), is_fast_startup ? 1 : 0);
lib_player_.SmartPlayerSetAudioOutputType(get(), 1);
if (is_hardware_decoder) {
int isSupportH264HwDecoder = lib_player_.SetSmartPlayerVideoHWDecoder(get(), 1);
int isSupportHevcHwDecoder = lib_player_.SetSmartPlayerVideoHevcHWDecoder(get(), 1);
Log.i(TAG, "isSupportH264HwDecoder: " + isSupportH264HwDecoder
+ ", isSupportHevcHwDecoder: " + isSupportHevcHwDecoder);
lib_player_.SmartPlayerSetHWRenderMode(get(), is_hw_render_mode ? 1 : 0);
}
lib_player_.SmartPlayerSetLowLatencyMode(get(), is_low_latency ? 1 : 0);
// 3. 调用Native启动
int ret = lib_player_.SmartPlayerStartPlay(get());
if (ret != OK) {
Log.e(TAG, "call SmartPlayerStartPlay failed, native_handle:" + get() + ", ret:" + ret);
return false;
}
// 4. 状态变更加写锁
write_lock_.lock();
try {
this.is_playing_ = true;
} finally {
write_lock_.unlock();
}
Log.i(TAG, "call SmartPlayerStartPlay OK, native_handle:" + get());
return true;
}
Stop操作采用"先置标志再调Native"的方式,通过本地 is_need_call 变量确保Native的 Stop 只会被调用一次:
java
public boolean StopPlayer() {
if (!check_native_handle()) return false;
if (!is_playing()) {
Log.w(TAG, "it's not playing, native_handle:" + get());
return false;
}
boolean is_need_call = false;
write_lock_.lock();
try {
if (this.is_playing_) {
this.is_playing_ = false;
is_need_call = true; // 只有真正持有"正在播放"状态才执行Stop
}
} finally {
write_lock_.unlock();
}
if (is_need_call)
lib_player_.SmartPlayerStopPlay(get());
return true;
}
4.3 高频路径的无阻塞设计
PostVideoPacketByteBuffer 是外部数据注入的高频接口(用于自定义Live Source场景),采用 tryLock() 而非 lock(),保证在锁被写操作持有时立即返回,不阻塞数据投递线程:
java
public boolean PostVideoPacketByteBuffer(int codec_id,
java.nio.ByteBuffer packet, int offset, int size, long timestamp_ms,
int is_timestamp_discontinuity, int is_key,
byte[] extra_data, int extra_data_size, int width, int height) {
if (!check_native_handle() || !is_running()) return false;
// tryLock:获取读锁失败立即返回,不阻塞
if (!read_lock_.tryLock()) return false;
try {
if (!check_native_handle() || !is_running()) return false;
return OK == lib_player_.PostVideoPacketByteBuffer(get(), codec_id,
packet, offset, size, timestamp_ms, is_timestamp_discontinuity,
is_key, extra_data, extra_data_size, width, height);
} catch (Exception e) {
Log.e(TAG, "PostVideoPacketByteBuffer Exception:", e);
return false;
} finally {
read_lock_.unlock();
}
}
4.4 资源释放的多重保障
release() 是主动释放路径,try_release() 提供非阻塞的尝试释放(适合在不确定是否运行的场景),finalize() 作为最后的兜底:
java
@Override
public void close() {
release();
}
public void release() {
if (empty()) return;
// 先停止所有运行中的任务
if (is_playing()) StopPlayer();
if (is_pulling()) StopPullStream();
if (is_recording()) StopRecorder();
long handle;
write_lock_.lock();
try {
handle = this.native_handle_;
this.native_handle_ = 0; // 先清零句柄,防止重入
clear_all_running_flags();
} finally {
write_lock_.unlock();
}
if (lib_player_ != null && handle != 0)
lib_player_.SmartPlayerClose(handle);
}
五、播放器初始化与参数配置
5.1 完整初始化流程
在 SmartPlayer.java 中,initPlayer() 负责创建Native实例并完成全量参数配置:
java
private boolean initPlayer() {
// Step 1: 创建Native播放实例,获取句柄
long handle = libPlayer.SmartPlayerOpen(appContext);
if (handle == 0) {
Log.e(TAG, "SmartPlayerOpen 失败");
showToast("初始化播放器失败");
return false;
}
// Step 2: 绑定到 Wrapper,后续操作通过 Wrapper 进行
playerWrapper.set(libPlayer, handle);
// Step 3: 注册事件回调
libPlayer.SetSmartPlayerEventCallbackV2(handle, new PlayerEventCallback());
// Step 4: 网络与协议参数配置
libPlayer.SmartPlayerSetBuffer(handle, playBuffer); // 缓冲时间(ms)
libPlayer.SmartPlayerSetReportDownloadSpeed(handle, 1, 2); // 每2秒上报一次下载速度
libPlayer.SmartPlayerSetRTSPTimeout(handle, 10); // RTSP超时10秒
libPlayer.SmartPlayerSetRTSPAutoSwitchTcpUdp(handle, 1); // 开启TCP/UDP自动切换
// Step 5: 功能开关
libPlayer.SmartPlayerSaveImageFlag(handle, 1); // 启用快照功能
// Step 6: 设置播放URL
setPlaybackUrl(playbackUrl);
setEncryptionKey();
return true;
}
5.2 关键参数详解
缓冲时间与延迟的平衡
java
libPlayer.SmartPlayerSetBuffer(handle, playBuffer);
// playBuffer: 0~5000ms,默认200ms
缓冲时间直接决定了播放延迟和流畅性的平衡:
- 0ms(超低延迟模式):几乎无缓冲,网络抖动时容易卡顿,适合局域网/专网环境;
- 100~300ms:适合质量较好的公网直播,兼顾延迟和流畅;
- 500ms以上:抗网络抖动能力强,但延迟相应增加。
低延迟模式下,SDK会配合 SmartPlayerSetLowLatencyMode 进一步优化:
java
lib_player_.SmartPlayerSetLowLatencyMode(get(), is_low_latency ? 1 : 0);
if (isLowLatency) playBuffer = 0; // 低延迟模式下缓冲设为0
快速启动(秒开)
java
lib_player_.SmartPlayerSetFastStartup(get(), is_fast_startup ? 1 : 0);
Fast Startup(秒开)针对推流端缓存了GOP(Group of Pictures)的场景。播放端连接后可以立即获取到最近一个完整IDR帧开始解码,无需等待下一个关键帧,显著降低首帧延迟。
在RTMP协议层,这依赖服务端(如SRS/NGINX-RTMP)开启 GOP Cache 功能,客户端 connect + play 后服务端立即推送缓存的GOP数据。
硬件解码与渲染模式
java
if (is_hardware_decoder) {
// H.264 硬解码
int isSupportH264HwDecoder = lib_player_.SetSmartPlayerVideoHWDecoder(get(), 1);
// H.265(HEVC) 硬解码
int isSupportHevcHwDecoder = lib_player_.SetSmartPlayerVideoHevcHWDecoder(get(), 1);
Log.i(TAG, "isSupportH264HwDecoder: " + isSupportH264HwDecoder
+ ", isSupportHevcHwDecoder: " + isSupportHevcHwDecoder);
// HW Render Mode: MediaCodec直接渲染到Surface,兼容性更好
lib_player_.SmartPlayerSetHWRenderMode(get(), is_hw_render_mode ? 1 : 0);
}
硬解码模式(MediaCodec)与软解码(FFmpeg)的选择:
| 对比维度 | 软解码 | 硬解码 |
|---|---|---|
| CPU占用 | 高 | 低 |
| 兼容性 | 极好 | 依赖芯片厂商实现 |
| 功耗 | 高 | 低 |
| 4K支持 | 受CPU限制 | 较好 |
| YUV回调 | 支持 | HW Render模式下不支持 |
HW Render Mode (SmartPlayerSetHWRenderMode)是硬解码下的特殊渲染模式,MediaCodec直接将解码输出写入Surface,绕过了SDK的渲染管线,兼容性和效率更佳,但代价是无法获取YUV/RGB回调帧数据(快照功能在此模式下不可用)。
5.3 RTSP 特殊配置
RTSP协议在安防摄像头场景中大量使用,有以下特殊配置需要注意:
java
// 设置超时(单位秒,避免无法连接时长时间阻塞)
libPlayer.SmartPlayerSetRTSPTimeout(handle, 10);
// TCP/UDP自动切换:部分摄像头只支持其中一种
libPlayer.SmartPlayerSetRTSPAutoSwitchTcpUdp(handle, 1);
当RTSP URL包含用户名密码(如 rtsp://admin:password@192.168.0.1/stream)时,SDK会自动解析。若需独立设置认证信息(URL不含密码),使用专用接口:
java
// URL不含认证信息时,单独设置
libPlayer.SetRTSPAuthenticationInfo(playerWrapper.get(), username, password);
代码中还提供了 parseAndSetRtspUrl() 方法,展示了如何手动解析RTSP URL中的认证信息并分离URL(适合含特殊字符需URL解码的场景):
java
private void parseAndSetRtspUrl(String url) {
final String rtspPrefix = "rtsp://";
String afterPrefix = url.substring(rtspPrefix.length());
int slashPos = afterPrefix.indexOf('/');
int atPos = slashPos > -1
? afterPrefix.lastIndexOf('@', slashPos)
: afterPrefix.lastIndexOf('@');
if (atPos <= 0) {
// 无认证信息,直接设置
libPlayer.SmartPlayerSetUrl(playerWrapper.get(), url);
return;
}
String credentials = afterPrefix.substring(0, atPos);
String cleanUrl = rtspPrefix + afterPrefix.substring(atPos + 1);
// 解析用户名密码,并处理URL编码(如%40 -> @)
try {
if (username != null && !username.isEmpty())
username = java.net.URLDecoder.decode(username, "UTF-8");
if (password != null && !password.isEmpty())
password = java.net.URLDecoder.decode(password, "UTF-8");
} catch (Exception e) {
Log.e(TAG, "URL解码失败", e);
}
libPlayer.SmartPlayerSetUrl(playerWrapper.get(), cleanUrl);
libPlayer.SetRTSPAuthenticationInfo(playerWrapper.get(), username, password);
}
六、视频渲染:SurfaceView 与 GLSurfaceView 的选择
6.1 渲染器创建
java
private void createSurfaceView() {
Log.i(TAG, "createSurfaceView, manufacturer: " + Build.MANUFACTURER);
// isHardwareRenderMode=false -> GLSurfaceView(支持YUV回调、快照、色彩调节)
// isHardwareRenderMode=true -> SurfaceView(MediaCodec直渲,兼容性更好)
if (isHardwareRenderMode) {
surfaceView = NTRenderer.CreateRenderer(this, false);
} else {
surfaceView = NTRenderer.CreateRenderer(this, true);
}
if (isHardwareRenderMode) {
// HW Render 模式需要监听Surface生命周期,在重建时更新Surface
SurfaceHolder holder = surfaceView.getHolder();
if (holder != null) holder.addCallback(this);
}
videoContainer.addView(surfaceView, 0);
}
HW Render 模式下,当 Activity 旋转或回到前台时,Surface 可能重建,需要及时通知SDK更新:
java
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (isHardwareDecoder && isHardwareRenderMode && playerWrapper.is_playing()) {
libPlayer.SmartPlayerUpdateHWRenderSurface(playerWrapper.get());
}
}
6.2 外部渲染回调(External Render)
SDK支持将解码后的原始帧数据通过回调交给应用层处理,适合AI算法、美颜、水印等场景。支持两种格式:
RGBA格式回调(RGBAExternalRender):
java
private static class RGBAExternalRender implements NTExternalRender {
@Override
public int getNTFrameFormat() {
return NT_FRAME_FORMAT_RGBA; // 返回期望的格式
}
@Override
public void onNTFrameSizeChanged(int width, int height) {
// 分辨率变化时重新分配Buffer
row_bytes_ = width * 4;
rgba_buffer_ = ByteBuffer.allocateDirect(row_bytes_ * height);
}
@Override
public ByteBuffer getNTPlaneByteBuffer(int index) {
if (index == 0) return rgba_buffer_;
return null;
}
@Override
public int getNTPlanePerRowBytes(int index) {
if (index == 0) return row_bytes_;
return 0;
}
public void onNTRenderFrame(int width, int height, long timestamp) {
rgba_buffer_.rewind();
// 此处可以对RGBA数据进行处理,如AI推理、截图等
}
}
I420(YUV420P)格式回调(I420ExternalRender):
java
private static class I420ExternalRender implements NTExternalRender {
@Override
public int getNTFrameFormat() {
return NT_FRAME_FORMAT_I420;
}
@Override
public void onNTFrameSizeChanged(int width, int height) {
y_row_bytes_ = width;
u_row_bytes_ = (width + 1) / 2;
v_row_bytes_ = (width + 1) / 2;
y_buffer_ = ByteBuffer.allocateDirect(y_row_bytes_ * height);
u_buffer_ = ByteBuffer.allocateDirect(u_row_bytes_ * ((height + 1) / 2));
v_buffer_ = ByteBuffer.allocateDirect(v_row_bytes_ * ((height + 1) / 2));
}
@Override
public ByteBuffer getNTPlaneByteBuffer(int index) {
switch (index) {
case 0: return y_buffer_; // Y平面
case 1: return u_buffer_; // U平面
case 2: return v_buffer_; // V平面
default: return null;
}
}
public void onNTRenderFrame(int width, int height, long timestamp) {
y_buffer_.rewind();
u_buffer_.rewind();
v_buffer_.rewind();
// 处理YUV数据,如保存JPEG(需要先转NV21格式)
}
}
启用外部渲染时,在 StartPlayer 中传入实例:
java
// 使用外部渲染(I420格式回调)
boolean success = playerWrapper.StartPlayer(surfaceView, null,
new I420ExternalRender(imageSavePath), 1,
isFastStartup, isHardwareDecoder, isLowLatency, isHardwareRenderMode);
七、事件回调系统
7.1 播放事件回调实现
事件回调通过 NTSmartEventCallbackV2 接口异步通知应用层:
java
private class PlayerEventCallback implements NTSmartEventCallbackV2 {
@Override
public void onNTSmartEventCallbackV2(long handle, int id, long param1,
long param2, String param3, String param4, Object param5) {
String event = formatEventMessage(id, param1, param2, param3);
if (!event.isEmpty()) {
Log.i(TAG, event);
// 通过 WeakReference Handler 更新UI,防止内存泄漏
handler.obtainMessage(MSG_PLAYER_EVENT, event).sendToTarget();
}
}
}
关键事件列表及其含义:
java
private String formatEventMessage(int id, long param1, long param2, String param3) {
switch (id) {
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STARTED:
return "开始播放";
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTING:
return "连接中...";
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTION_FAILED:
return "连接失败"; // 网络不通或地址错误
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTED:
return "已连接";
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_DISCONNECTED:
return "连接断开"; // 网络中断,SDK内部会自动重连
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STOP:
return "已停止";
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RESOLUTION_INFO:
return "分辨率: " + param1 + "×" + param2; // 视频分辨率变化
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_NO_MEDIADATA_RECEIVED:
return "无数据,检查URL"; // URL正确但无数据推入
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_SWITCH_URL:
return "切换URL中...";
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CAPTURE_IMAGE:
return "快照: " + (param1 == 0 ? "成功" : "失败");
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RECORDER_START_NEW_FILE:
return "录像: " + param3; // param3为新文件路径
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_ONE_RECORDER_FILE_FINISHED:
return "录像完成";
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_DOWNLOAD_SPEED:
return "速度: " + (param1 * 8 / 1000) + " kbps"; // param1单位Byte/s
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RTSP_STATUS_CODE:
return "RTSP错误: " + param1; // RTSP错误状态码
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_NEED_KEY:
return "需要解密Key";
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_KEY_ERROR:
return "Key错误";
default:
return "";
}
}
7.2 SEI UserData 回调
RTMP中可以在H.264的SEI(Supplemental Enhancement Information)NAL单元中携带自定义数据,常用于字幕、弹幕时间戳同步等场景。SDK通过 NTUserDataCallback 接口回调:
java
private class UserDataCallback implements NTUserDataCallback {
private int bufferSize = 0;
private ByteBuffer buffer;
@Override
public ByteBuffer getUserDataByteBuffer(int size) {
// SDK会先调用此方法获取Buffer,避免重复分配
if (size < 1) return null;
if (size <= bufferSize && buffer != null) return buffer;
bufferSize = size + 512;
buffer = ByteBuffer.allocateDirect(bufferSize);
return buffer;
}
@Override
public void onUserDataCallback(int ret, int dataType, int size,
long timestamp, long reserve1, long reserve2) {
// dataType=2 时为字符串类型数据
if (dataType == 2 && buffer != null) {
buffer.rewind();
byte[] data = new byte[size];
buffer.get(data);
handler.obtainMessage(MSG_USER_DATA, new String(data)).sendToTarget();
}
}
}
7.3 防内存泄漏的 Handler 实现
java
private static class MainHandler extends Handler {
private final WeakReference<SmartPlayer> activityRef;
MainHandler(SmartPlayer activity) {
this.activityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
SmartPlayer activity = activityRef.get();
if (activity == null) return; // Activity已被回收,安全退出
if (msg.what == MSG_PLAYER_EVENT) {
activity.txtEventMsg.setText("Event: " + msg.obj);
} else if (msg.what == MSG_USER_DATA) {
activity.txtUserDataMsg.setText("数据: " + msg.obj);
}
}
}
Android平台Unity3D下RTMP播放器延迟测试
八、录像功能实现
8.1 录像启动与停止
录像功能可以与播放功能同时启用,也可以独立启动(只录像不播放),完全解耦:
java
private void startRecording() {
if (playbackUrl == null || playbackUrl.isEmpty()) {
showToast("请先输入播放URL");
return;
}
// 若播放器尚未初始化(只录不播场景),先初始化
if (playerWrapper.empty() && !initPlayer()) return;
// 配置录像参数
if (recordDir != null && !recordDir.isEmpty()) {
// 创建录像目录(如不存在)
if (libPlayer.SmartPlayerCreateFileDirectory(recordDir) == 0) {
playerWrapper.SetRecorderDirectory(recordDir);
playerWrapper.SetRecorderFileMaxSize(200); // 单文件最大200MB,自动切片
}
}
// 启动录像,true表示非AAC音频自动转码为AAC
if (!playerWrapper.StartRecorder(true)) {
Log.e(TAG, "StartRecorder 失败");
showToast("录像失败");
return;
}
updateButtonStates();
}
录像文件以 .mp4 格式保存,达到 SetRecorderFileMaxSize 设置的大小后自动切片为新文件,并通过事件回调 EVENT_DANIULIVE_ERC_PLAYER_RECORDER_START_NEW_FILE 通知应用层新文件路径。
停止时,先判断是否有其他功能在运行,若无则释放播放器实例:
java
private void stopRecording() {
if (!playerWrapper.is_recording()) return;
playerWrapper.StopRecorder();
// 所有功能都停止后才释放实例
if (!playerWrapper.is_running()) {
playerWrapper.release();
}
updateButtonStates();
}
九、录像管理模块(RecorderManager)
RecorderManager 负责展示录像文件列表,支持点击播放和批量删除。其核心设计亮点是异步加载 ,避免在主线程中执行 MediaMetadataRetriever 这类耗时操作。
9.1 异步文件加载
java
private void refreshFileList() {
// 1. 显示加载进度条
mProgressBar.setVisibility(View.VISIBLE);
recFileListView.setVisibility(View.INVISIBLE);
mTvNoFiles.setVisibility(View.GONE);
// 2. 启动后台线程执行耗时的文件读取+元数据解析
new Thread(new Runnable() {
@Override
public void run() {
final ArrayList<ArrayList<String>> resultList = loadFileListInBackground();
// 3. 切换回主线程更新UI
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isFinishing()) return; // 防止Activity已退出后更新UI崩溃
fileList = resultList;
updateListView();
mProgressBar.setVisibility(View.GONE);
}
});
}
}).start();
}
9.2 文件元数据读取(后台线程)
java
private ArrayList<ArrayList<String>> loadFileListInBackground() {
ArrayList<ArrayList<String>> tempList = new ArrayList<>();
// ...目录校验代码...
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
for (File recFile : files) {
if (!recFile.getName().endsWith(".mp4")) continue;
String sizeStr = getFileSizeString(recFile.length());
// 读取MP4时长(耗时操作,必须在后台线程执行)
String durationStr = "--:--";
try {
retriever.setDataSource(recFile.getAbsolutePath());
String time = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_DURATION);
if (time != null) {
durationStr = formatDuration(Long.parseLong(time));
}
} catch (Exception e) { /* 读取失败忽略,显示"--:--" */ }
String metaInfo = "大小: " + sizeStr + " | 时长: " + durationStr;
ArrayList<String> item = new ArrayList<>();
item.add(recFile.getName());
item.add(recFile.getAbsolutePath());
item.add(metaInfo);
tempList.add(item);
}
// 按文件名(时间戳命名)倒序排列,最新文件在最上方
Collections.sort(tempList, new Comparator<ArrayList<String>>() {
@Override
public int compare(ArrayList<String> o1, ArrayList<String> o2) {
return o2.get(0).compareTo(o1.get(0));
}
});
} finally {
try { retriever.release(); } catch (Exception ex) {}
}
return tempList;
}
十、本地录像回放(RecorderPlayback)
RecorderPlayback 使用系统 VideoView 实现本地MP4文件回放,重点实现了精准 Seek 和完整的播放控制UI。
10.1 精准 Seek 实现
Android 8.0(API 26)引入了 MediaPlayer.SEEK_CLOSEST,可定位到最近的任意帧而不只是关键帧,避免拖动进度条后画面跳跃的问题:
java
seekBarVideo.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// 拖动开始时停止自动更新进度,防止进度条跳动
mHandler.removeMessages(MSG_UPDATE_PROGRESS);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
int targetPosition = seekBar.getProgress();
if (playVideoView != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mMediaPlayer != null) {
try {
// Android 8.0+ 精准Seek:定位到最近帧
mMediaPlayer.seekTo(targetPosition, MediaPlayer.SEEK_CLOSEST);
} catch (Exception e) {
playVideoView.seekTo(targetPosition); // 降级到普通Seek
}
} else {
// 旧版本:只能Seek到关键帧
playVideoView.seekTo(targetPosition);
}
if (!isPaused && !playVideoView.isPlaying()) {
playVideoView.start();
}
}
mHandler.sendEmptyMessage(MSG_UPDATE_PROGRESS); // 恢复进度更新
}
});
mMediaPlayer 引用在 onPrepared 回调中获取:
java
playVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mMediaPlayer = mp; // 保存引用,用于精准Seek
int duration = playVideoView.getDuration();
seekBarVideo.setMax(duration);
tvTotalTime.setText(formatTime(duration));
playVideoView.start();
isPaused = false;
updatePlayPauseIcon();
mHandler.sendEmptyMessage(MSG_UPDATE_PROGRESS);
}
});
10.2 进度条自动更新
java
private static final int MSG_UPDATE_PROGRESS = 1;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_UPDATE_PROGRESS) {
updateProgress();
// 每500ms轮询一次进度
mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, 500);
}
}
};
private void updateProgress() {
if (playVideoView != null && playVideoView.isPlaying()) {
int currentPosition = playVideoView.getCurrentPosition();
seekBarVideo.setProgress(currentPosition);
tvCurrentTime.setText(formatTime(currentPosition));
}
}
10.3 文件删除与资源清理
删除文件时需要先停止播放并清理资源,防止文件被占用导致删除失败:
java
private void deleteCurrentFile() {
try {
File file = new File(recorderFilePath);
if (file.exists() && file.isFile()) {
if (file.delete()) {
if (playVideoView != null && playVideoView.isPlaying()) {
playVideoView.stopPlayback();
}
// 清理 Handler 和 MediaPlayer 引用
mHandler.removeCallbacksAndMessages(null);
mMediaPlayer = null;
// 通知 RecorderManager 刷新列表
setResult(RESULT_OK);
finish();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
RecorderManager 通过 onActivityResult 监听删除结果并刷新列表:
java
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_PLAYBACK && resultCode == RESULT_OK) {
refreshFileList(); // 异步重新加载文件列表
}
}
十一、横竖屏自适应与全屏体验
11.1 横竖屏布局切换
java
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updateLayoutForOrientation(newConfig.orientation);
}
private void updateLayoutForOrientation(int orientation) {
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 横屏:隐藏控制面板,视频全屏
controlPanel.setVisibility(View.GONE);
btnBackPortrait.setVisibility(View.VISIBLE);
LinearLayout.LayoutParams params =
(LinearLayout.LayoutParams) videoContainer.getLayoutParams();
params.weight = 0;
params.height = LinearLayout.LayoutParams.MATCH_PARENT;
videoContainer.setLayoutParams(params);
} else {
// 竖屏:显示控制面板,视频占50%高度
controlPanel.setVisibility(View.VISIBLE);
btnBackPortrait.setVisibility(View.GONE);
LinearLayout.LayoutParams params =
(LinearLayout.LayoutParams) videoContainer.getLayoutParams();
params.weight = 1;
params.height = 0;
videoContainer.setLayoutParams(params);
}
hideSystemUI();
}
11.2 沉浸式全屏
java
private void hideSystemUI() {
if (Build.VERSION.SDK_INT >= 19) {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); // 滑入后自动重新隐藏
}
}
十二、实时功能:快照、翻转、旋转与音量控制
12.1 快照
快照功能支持 JPEG 和 PNG 格式,通过事件回调异步通知结果:
java
private void captureImage() {
if (!playerWrapper.is_playing()) {
showToast("请先开始播放");
return;
}
if (imageDateFormat == null)
imageDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss_SSS", Locale.getDefault());
// 格式:0=JPEG,1=PNG;quality仅对JPEG有效
String imagePath = imageSavePath + "/" + imageDateFormat.format(new Date()) + ".jpeg";
boolean success = playerWrapper.CaptureImage(0, 100, imagePath, "snapshot");
Log.i(TAG, "快照: " + imagePath + ", success: " + success);
showToast(success ? "快照已保存" : "快照失败");
}
注意:快照功能在 HW Render Mode 下不可用,因为MediaCodec直渲绕过了SDK的帧缓冲。
12.2 翻转与旋转
java
private void toggleFlipVertical() {
isFlipVertical = !isFlipVertical;
btnFlipVertical.setText(isFlipVertical ? "取消垂直" : "垂直翻转");
if (!playerWrapper.empty())
libPlayer.SmartPlayerSetFlipVertical(playerWrapper.get(), isFlipVertical ? 1 : 0);
}
private void toggleRotation() {
// 每次点击顺时针旋转90度:0 -> 90 -> 180 -> 270 -> 0
rotateDegrees = (rotateDegrees + 90) % 360;
btnRotation.setText("旋转" + rotateDegrees + "°");
if (!playerWrapper.empty())
libPlayer.SmartPlayerSetRotation(playerWrapper.get(), rotateDegrees);
}
12.3 动态切换播放URL
无需停止重启,直接切换到新的RTMP/RTSP流:
java
private void toggleSwitchUrl() {
if (playerWrapper.empty()) {
showToast("请先开始播放");
return;
}
switchUrlFlag = !switchUrlFlag;
libPlayer.SmartPlayerSwitchPlaybackUrl(playerWrapper.get(),
switchUrlFlag ? switchUrl : playbackUrl);
}
在RTMP层,SDK会在新连接建立成功后再关闭旧连接,保证切换过程中播放不中断(无缝切换)。
安卓RTMP播放器同时播放4路RTMP流延迟测试
十三、性能优化与常见问题
13.1 延迟优化清单
| 优化项 | 建议设置 | 效果 |
|---|---|---|
| 缓冲时间 | 0~100ms(局域网) | 降低播放延迟 |
| 低延迟模式 | 开启 | SDK内部优化缓冲策略 |
| 快速启动 | 服务端开启GOP Cache | 降低首帧延迟 |
| 硬件解码 | 中高端设备建议开启 | 降低CPU占用,减少解码耗时 |
| RTSP协议 | 局域网摄像头建议TCP | 避免UDP丢包重传延迟 |
13.2 常见问题排查
问题1:连接成功但无画面(NO_MEDIADATA_RECEIVED)
- 推流端未开始推流
- 服务端URL路径错误(注意大小写)
- 防火墙端口未开放(RTMP默认1935,RTSP默认554)
问题2:画面有声音无视频或有视频无声音
- 检查编码格式:原始RTMP只支持H.264视频和AAC/MP3音频
- Enhanced RTMP可支持H.265,但需要服务端也支持
问题3:硬解码失败,自动降级软解码
- 部分国产SoC的H.265硬解码存在兼容性问题
- 可通过事件回调中的
isSupportHevcHwDecoder返回值判断是否支持
问题4:录像文件无法播放
- 确认录像目录有写权限(Android 10+ 注意分区存储限制)
- 检查
SmartPlayerSetRecorderAudioTranscodeAAC是否正确设置(音频格式兼容性)
十四、完整流程总结
bash
应用启动
│
▼
initPlayer()
├── SmartPlayerOpen() 获取Native句柄
├── SetSmartPlayerEventCallbackV2() 注册事件回调
├── SmartPlayerSetBuffer() 配置缓冲
├── SmartPlayerSetRTSPTimeout() 配置超时
└── SmartPlayerSetUrl() 设置播放URL
│
▼
startPlayback() / startRecording()
├── LibPlayerWrapper.StartPlayer()
│ ├── SmartPlayerSetSurface()
│ ├── SetSmartPlayerVideoHWDecoder() (可选)
│ ├── SmartPlayerSetLowLatencyMode()
│ └── SmartPlayerStartPlay() → 开始拉流解码渲染
│
└── LibPlayerWrapper.StartRecorder()
├── SmartPlayerSetRecorderDirectory()
├── SmartPlayerSetRecorderFileMaxSize()
└── SmartPlayerStartRecorder() → 开始写MP4文件
│
▼
运行中实时操作
├── SetMute() / SmartPlayerSetAudioVolume()
├── SmartPlayerSetFlipVertical/Horizontal()
├── SmartPlayerSetRotation()
├── CaptureImage()
└── SmartPlayerSwitchPlaybackUrl()
│
▼
stopPlayback() / stopRecording()
└── LibPlayerWrapper.release()
└── SmartPlayerClose() 释放Native资源
十五、总结
本文以大牛直播SDK的真实Demo代码为基础,结合RTMP协议规范,从以下维度完整呈现了一个工程级Android RTMP播放器的实现:
- 协议层:RTMP握手、Chunk机制、消息类型、Enhanced RTMP扩展;
- 架构层 :
LibPlayerWrapper的状态机设计、读写锁并发控制、资源安全释放; - 参数调优:缓冲时间、低延迟模式、快速启动、硬件解码的选择策略;
- 渲染层:GLSurfaceView vs MediaCodec直渲,RGBA/I420外部渲染回调;
- 录像系统:录像启动/停止、自动切片、加密流处理;
- 录像管理:异步文件加载、元数据读取、删除与列表刷新;
- 回放功能:VideoView+精准Seek(API 26+)、进度条控制;
- 体验优化:横竖屏自适应、沉浸式全屏、实时翻转旋转快照。
大牛直播SDK将复杂的协议实现、解码、渲染封装在Native层,通过清晰的JNI接口暴露给Android层,配合 LibPlayerWrapper 的状态管理封装,使得开发者可以以极少的代码量实现功能完整、性能优秀的直播播放器。
📎 CSDN官方博客:音视频牛哥-CSDN博客