基于SmartMediaKit实现Android平台RTMP直播推流——从协议规范到实战代码全解析
前言
RTMP(Real-Time Messaging Protocol)自 Adobe 发布以来,历经二十余年仍是直播推流领域使用最广泛的协议之一。从手机直播、游戏直播到企业视频会议,绝大多数直播平台的推流端依然以 RTMP 作为首选接入协议。
本文将结合大牛直播SDK(SmartMediaKit)的实际 Android Demo 源码,从 RTMP 协议规范出发,系统讲解如何在 Android 上实现一套摄像头采集 → YUV 图像投递 → 音频采集 → 层叠水印 → RTMP 推流的完整链路,并深入分析各环节关键技术细节。
一、RTMP 协议规范回顾
1.1 协议定位
RTMP 是 Adobe 设计的应用层协议,运行于 TCP 之上,默认端口 1935。其核心设计目标是在 Flash Player 与媒体服务器之间传递低延迟的音视频数据。尽管 Flash 已成历史,但 RTMP 的推流端地位至今未被动摇,原因在于:
- 低延迟:端到端延迟通常在 1~3 秒,大牛直播SDK的RTMP推拉流方案,可以做到100-200ms延迟,优于 HLS 的 5~30 秒
- 服务器生态成熟:SRS、nginx-rtmp、Wowza、阿里云/腾讯云直播均原生支持
- 推流协议标准化:几乎所有直播平台均提供
rtmp://推流地址
1.2 握手与连接建立
RTMP 连接建立分为两阶段:
阶段一:握手(Handshake)
Client Server
| |
|---- C0 (1 byte version) ---> |
|---- C1 (1536 bytes) -------> |
|<--- S0 + S1 + S2 -----------|
|---- C2 (echo of S1) -------> |
| 握手完成 |
- C0/S0:各 1 字节,表示 RTMP 版本(通常为 0x03)
- C1/S1:各 1536 字节,含时间戳和随机数据
- C2/S2:各 1536 字节,回显对端的 C1/S1
阶段二:连接控制消息
握手完成后,客户端发送一系列 AMF 编码的控制消息:
Client Server
|---- connect(app) ----------> | 建立虚拟连接
|<--- _result(NetConnection.Connect.Success) ---|
|---- createStream ----------> | 创建流通道
|<--- _result(streamId) ------|
|---- publish(streamName) ---> | 开始发布
|<--- onStatus(NetStream.Publish.Start) --------|
|===== 音视频数据 FLV Tags ====> |
1.3 消息结构
RTMP 数据以消息(Message)为单位传输,大消息会被拆分为块(Chunk):
| 字段 | 说明 |
|---|---|
| Chunk Stream ID | 标识消息所属的逻辑通道(音频、视频、控制等) |
| Message Type ID | 0x08=音频, 0x09=视频, 0x12=数据消息(AMF), 0x14=命令消息 |
| Timestamp | 消息时间戳,单位毫秒 |
| Message Stream ID | 流 ID,通常为 1 |
| Payload | FLV AudioTag / VideoTag 格式的音视频数据 |
默认 Chunk Size 为 128 字节,可通过 Set Chunk Size 控制消息调整(通常设为 4096 或更大以减少分包开销)。
1.4 音视频数据封装
视频 Tag(FLV VideoTag):
[FrameType(4bit) | CodecID(4bit)] [AVCPacketType(1byte)] [CompositionTime(3bytes)] [Data]
FrameType: 1=关键帧, 2=非关键帧
CodecID: 7=H.264, 12=H.265(Enhanced RTMP)
AVCPacketType: 0=AVC sequence header(SPS/PPS), 1=AVC NALU, 2=AVC end of sequence
音频 Tag(FLV AudioTag):
[SoundFormat(4bit) | SoundRate(2bit) | SoundSize(1bit) | SoundType(1bit)] [AACPacketType(1byte)] [Data]
SoundFormat: 10=AAC, 7=G.711 A-law(PCMA)
AACPacketType: 0=AAC sequence header, 1=AAC raw
1.5 Enhanced RTMP(H.265 支持)
传统 RTMP 规范只定义了 H.264 和 AAC 的封装。为支持 H.265/HEVC,业界引入了 Enhanced RTMP 扩展(由 Veovera Software Organization 推动),通过在 VideoTag 中使用新的 fourCC 字段标识 HEVC,目前主流直播平台和 SRS 4.x+ 均已支持。大牛直播 SDK 默认开启 Enhanced RTMP,可通过 DisableEnhancedRTMP 接口关闭。
二、大牛直播 SDK RTMP 推流架构
大牛直播 SDK 的 RTMP 推流整体架构如下:
┌─────────────────────────────────────────────────────────────┐
│ Android App (MainActivity) │
│ │
│ Camera2Helper │
│ └── onCameraImageData ──▶ PostLayerImageYUV420888ByteBuffer│
│ (YUV_420_888 每帧) (Layer 0, 底层画面) │
│ │
│ NTAudioRecordV2 │
│ └── onNTAudioRecordV2Frame ──▶ OnPCMData │
│ (PCM 每 10ms) │
│ │
│ LayerPostThread (400ms 定时刷新) │
│ ├── PostLayerBitmap (时间戳文字, Layer 1) │
│ ├── PostLayerBitmap (文字水印一, Layer 2) │
│ ├── PostLayerBitmap (文字水印二, Layer 3) │
│ └── PostLayerImageRGBA8888ByteBuffer (图片水印, Layer 4) │
│ │
│ LibPublisherWrapper (stream_publisher_) │
│ ├── SetURL(rtmp://...) │
│ ├── StartPublisher() ──▶ RTMP 推流 │
│ ├── StartRecorder() ──▶ 本地录像(可同时进行) │
│ └── StartRtspStream()──▶ RTSP 服务(可同时进行) │
└─────────────────────────────────────────────────────────────┘
↓ TCP/RTMP
┌──────────────────────────┐
│ RTMP 流媒体服务器 │
│ (SRS / nginx-rtmp / CDN) │
└──────────────────────────┘
↓ HLS / HTTP-FLV / RTMP
┌──────────────────────────┐
│ 播放端 │
└──────────────────────────┘
架构亮点:
- 层叠加模式(video_opt=3):摄像头画面作为第 0 层,水印作为高层,SDK 内部合成后统一编码,一次编码多路输出(RTMP + 录像 + RTSP),CPU 开销最优。
- 推流/录像/RTSP 三合一:同一个
stream_publisher_实例可同时执行 RTMP 推流、本地录像和 RTSP 服务,共享同一路编码数据。 - 线程安全设计:
LibPublisherWrapper内部使用读写锁(ReentrantReadWriteLock)保护 native handle,摄像头回调线程、音频线程、水印线程并发投递数据时不会发生竞争。
三、推流初始化与配置

3.1 打开推流实例
// MainActivity.InitAndSetConfig()
private void InitAndSetConfig() {
if (null == libPublisher) return;
if (!stream_publisher_.empty()) return; // 避免重复初始化
// audio_opt=1: 推送编码前 PCM 音频
// video_opt=3: 层叠加模式(支持水印合成)
long handle = libPublisher.SmartPublisherOpen(
context_, 1, 3, video_width_, video_height_);
if (0 == handle) {
Log.e(TAG, "sdk open failed!");
return;
}
int fps = 25;
int gop = fps * 3; // GOP = 3秒一个关键帧,兼顾延迟与质量
initialize_publisher(libPublisher, handle, video_width_, video_height_, fps, gop);
stream_publisher_.set(libPublisher, handle);
}
SmartPublisherOpen 的 video_opt 参数决定了整个编码模式:
1:传统接口,推送编码前 YUV 数据(单帧模式)2:传输编码后的 H.264 数据3:层叠加模式,Demo 采用此模式,支持水印合成
3.2 视频编码配置
Demo 支持三种视频编码方案,通过界面 Spinner 切换:
① 软编码 H.264(默认,videoEncodeType == 0)
// VBR 模式:在质量约束下动态调整码率,比 CBR 更适合直播场景
int is_enable_vbr = 1;
int video_quality = LibPublisherWrapper.estimate_video_software_quality(
width, height, true); // H.264 软编质量参数
int vbr_max_kbps = LibPublisherWrapper.estimate_video_vbr_max_kbps(
width, height, fps);
lib_publisher.SmartPublisherSetSwVBRMode(handle, is_enable_vbr,
video_quality, vbr_max_kbps);
lib_publisher.SmartPublisherSetSWVideoEncoderProfile(handle, 3); // High Profile
lib_publisher.SmartPublisherSetSWVideoEncoderSpeed(handle, 2); // 编码速度(1-6)
lib_publisher.SmartPublisherSetGopInterval(handle, gop);
lib_publisher.SmartPublisherSetFPS(handle, fps);
② 硬编码 H.264(videoEncodeType == 1)
int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(
width, height, fps, true);
if (lib_publisher.SetSmartPublisherVideoHWEncoder(handle, kbps) == 0) {
lib_publisher.SetNativeMediaNDK(handle, 0);
lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // VBR
lib_publisher.SetVideoHWEncoderQuality(handle, 39);
lib_publisher.SetAVCHWEncoderProfile(handle, 0x08); // High Profile
lib_publisher.SetAVCHWEncoderLevel(handle, 0x1000); // Level 4.1
Log.i(TAG, "H.264 hardware encoder enabled, kbps=" + kbps);
}
③ 硬编码 H.265(videoEncodeType == 2)
int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(
width, height, fps, false); // H.265 码率约为 H.264 的 70%
if (lib_publisher.SetSmartPublisherVideoHevcHWEncoder(handle, kbps) == 0) {
lib_publisher.SetNativeMediaNDK(handle, 0);
lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // VBR
lib_publisher.SetVideoHWEncoderQuality(handle, 39);
Log.i(TAG, "H.265 hardware encoder enabled, kbps=" + kbps);
}
关于 Enhanced RTMP:使用 H.265 硬编码时,SDK 默认启用 Enhanced RTMP 扩展封装 HEVC,服务端需支持 Enhanced RTMP(如 SRS 4.x+)。如服务端不支持,可调用
DisableEnhancedRTMP(handle, 1)关闭。
3.3 音频编码配置
// is_pcma_ 由界面 "当前PCMA编码 / 当前AAC编码" 按钮控制
if (is_pcma_) {
lib_publisher.SmartPublisherSetAudioCodecType(handle, 3); // PCMA (G.711 A-law)
} else {
lib_publisher.SmartPublisherSetAudioCodecType(handle, 1); // AAC
}
对应的音频采集采样率必须与编码类型匹配:
// startAudioRecorder()
if (!audio_recorder_.Start(is_pcma_ ? 8000 : 44100, 1)) {
// 启动失败处理
}
从 RTMP 协议角度:
- AAC:FLV AudioTag SoundFormat=10,需先发送 AudioSpecificConfig 序列头
- PCMA:FLV AudioTag SoundFormat=7,SoundRate=0(5.5kHz 标记,实际以 8kHz 传输),无需序列头
SDK 已按 FLV 规范自动处理序列头的发送时机,开发者无需手动介入。
3.4 音频处理增强
// 噪音抑制(推荐开启,改善弱网和嘈杂环境下的音质)
lib_publisher.SmartPublisherSetNoiseSuppression(handle, 1);
// AGC 自动增益控制(根据实际场景决定是否开启)
lib_publisher.SmartPublisherSetAGC(handle, 0);
// 回音消除(delay=0 表示由 SDK 自动估算延迟)
lib_publisher.SmartPublisherSetEchoCancellation(handle, 1, 0);
3.5 事件回调注册
lib_publisher.SetSmartPublisherEventCallbackV2(handle,
new EventHandlerPublisherV2().set(handler_, record_executor_));
关键事件 ID 及其含义:
| 事件 ID | 含义 |
|---|---|
EVENT_DANIULIVE_ERC_PUBLISHER_STARTED |
推流实例已启动 |
EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTING |
正在连接服务器 |
EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTED |
连接成功,开始推流 |
EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTION_FAILED |
连接失败(网络问题或 URL 错误) |
EVENT_DANIULIVE_ERC_PUBLISHER_DISCONNECTED |
连接断开(需业务层处理重连) |
EVENT_DANIULIVE_ERC_PUBLISHER_SEND_DELAY |
发送时延,param1=延迟毫秒数,param2=积压帧数 |
EVENT_DANIULIVE_ERC_PUBLISHER_STOP |
推流已停止 |
// EventHandlerPublisherV2.onNTSmartEventCallbackV2()
switch (id) {
case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTED:
publisher_event = "连接成功..";
break;
case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_SEND_DELAY:
publisher_event = "发送时延: " + param1 + " 帧数:" + param2;
break;
// ...
}
// 通过 Handler 切换到主线程更新 UI
handler.sendMessage(message);
四、启动与停止 RTMP 推流
4.1 启动推流
// ButtonStartPushListener.onClick()
InitAndSetConfig(); // 初始化并配置 SDK 实例
String rtmp_pusher_url = "rtmp://player.daniulive.com:1935/hls/stream"
+ (int)(System.currentTimeMillis() % 1000000);
if (!stream_publisher_.SetURL(rtmp_pusher_url)) {
Log.e(TAG, "Failed to set publish stream URL..");
}
boolean start_ret = stream_publisher_.StartPublisher();
if (!start_ret) {
stream_publisher_.try_release();
Log.e(TAG, "Failed to start push stream..");
return;
}
startAudioRecorder(); // 启动麦克风采集
startLayerPostThread(); // 启动水印刷新线程
btnRTMPPusher.setText("停止推送");
流程说明:
SetURL设置推流地址,SDK 内部解析rtmp://host:port/app/streamNameStartPublisher触发底层 TCP 连接、RTMP 握手和 publish 命令发送,该操作异步完成- 连接结果通过
EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTED/CONNECTION_FAILED事件回调通知 - 摄像头和水印数据在
StartPublisher之前即可开始投递(SDK 内部有缓冲队列)
4.2 停止推流
// stopPush()
private void stopPush() {
stream_publisher_.StopPublisher();
// try_release:仅在无任何推流/录像任务时才真正释放 native handle
// 若同时开启了录像,则 release 不会执行,录像继续进行
stream_publisher_.try_release();
if (!stream_publisher_.is_publishing())
stopAudioRecorder(); // 无任何输出时停止录音
}
try_release 的安全释放逻辑:
// LibPublisherWrapper.try_release()
public boolean try_release() {
if (empty()) return false;
if (is_publishing()) {
// 还有其他推流任务(如 RTSP / 录像),不释放
Log.i(TAG, "try_release it is publishing, native_handle:" + get());
return false;
}
// 双重检查 + 写锁保护
write_lock_.lock();
try {
if (is_publishing()) return false;
handle = this.native_handle_;
this.native_handle_ = 0;
} finally {
write_lock_.unlock();
}
lib_publisher_.SmartPublisherClose(handle);
return true;
}
安卓采集摄像头和麦克风实现低延迟RTMP推流
五、摄像头采集与图像投递
5.1 Camera2 关键配置
Demo 在 Camera2Helper.createCameraPreviewSession() 中使用 TEMPLATE_RECORD 模板,并针对视频推流场景做了专项优化:
// 使用录制模板,AE 策略更稳定,适合持续输出场景
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(
CameraDevice.TEMPLATE_RECORD);
// 选择最佳帧率范围(兼顾流畅度和曝光质量)
CameraEnumerationAndroid.CaptureFormat.FramerateRange best_fps_range
= getBestFpsRange(camera_characteristics);
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
new Range<>(best_fps_range.min / fps_unit_factor_,
best_fps_range.max / fps_unit_factor_));
// AE 模式:自动曝光,不锁定
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON);
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false);
// 稳像:优先光学防抖,其次视频防抖
chooseStabilizationMode(camera_characteristics, mPreviewRequestBuilder);
// 对焦:连续视频自动对焦
chooseFocusMode(camera_characteristics, mPreviewRequestBuilder);
5.2 图像旋转处理
RTMP 推流的视频方向问题是移动端开发最常见的坑。Demo 通过 OrientationDetector + getCameraImageRotationDegree 组合解决:
// Camera2Helper.getCameraImageRotationDegree()
// 前后摄旋转逻辑不同(前置有镜像效果,旋转方向相反)
if (CAMERA_ID_BACK.equals(camera_id)) {
degree = (mSensorOrientation + device_degree + 360) % 360;
} else {
degree = (mSensorOrientation - device_degree + 360) % 360;
}
图像方向锁定:Demo 提供"锁定/解锁图像方向"功能,锁定后推流画面固定为锁定时刻的方向,适合固定机位监控场景:
// ButtonCameraImageOrientationLockListener
is_need_lock_image_orientation_ = !is_need_lock_image_orientation_;
if (cur_image_orientation_ >= 0)
cur_image_orientation_ = orientation_; // 解锁时更新为当前方向
cameraImageRotationDegree_ = camera2Helper.getCameraImageRotationDegree(
cur_image_orientation_ < 0 ? orientation_ : cur_image_orientation_);
5.3 YUV 图像投递
// MainActivity.onCameraImageData()
@Override
public void onCameraImageData(Image image) {
if (image.getFormat() != ImageFormat.YUV_420_888) return;
Image.Plane[] planes = image.getPlanes();
int w = image.getWidth(), h = image.getHeight();
int y_offset = 0, u_offset = 0, v_offset = 0;
// 处理 Crop 矩形(部分 SoC 摄像头存在非零 CropRect)
Rect crop_rect = image.getCropRect();
if (crop_rect != null && !crop_rect.isEmpty()) {
w = crop_rect.width();
h = crop_rect.height();
y_offset += crop_rect.top * planes[0].getRowStride()
+ crop_rect.left * planes[0].getPixelStride();
u_offset += (crop_rect.top / 2) * planes[1].getRowStride()
+ (crop_rect.left / 2) * planes[1].getPixelStride();
v_offset += (crop_rect.top / 2) * planes[2].getRowStride()
+ (crop_rect.left / 2) * planes[2].getPixelStride();
}
int rotation_degree = cameraImageRotationDegree_;
if (rotation_degree < 0) return; // 方向未就绪,丢帧保护
// 同时投递到推流实例和快照实例(两者共享摄像头数据)
for (LibPublisherWrapper i : publisher_array_)
i.PostLayerImageYUV420888ByteBuffer(
0, 0, 0, // index=0(底层), left=0, top=0
planes[0].getBuffer(), y_offset, planes[0].getRowStride(),
planes[1].getBuffer(), u_offset, planes[1].getRowStride(),
planes[2].getBuffer(), v_offset, planes[2].getRowStride(),
planes[1].getPixelStride(),
w, h,
0, 0, // 不翻转
0, 0, 3, // 不缩放,缩放质量=3
rotation_degree);
}
六、音频采集与投递
6.1 采集回调
// NTAudioRecordV2CallbackImpl
@Override
public void onNTAudioRecordV2Frame(ByteBuffer data, int size,
int sampleRate, int channel, int per_channel_sample_number) {
// per_channel_sample_number = sampleRate / 100(每 10ms 一帧)
LibPublisherWrapper publisher_0 = get_publisher_0();
if (publisher_0 != null)
publisher_0.OnPCMData(data, size, sampleRate, channel,
per_channel_sample_number);
}
6.2 线程安全的 PCM 投递
// LibPublisherWrapper.OnPCMData()
public boolean OnPCMData(ByteBuffer pcm_data, int size,
int sample_rate, int channel, int per_channel_sample_number) {
if (!check_native_handle() || !is_publishing()) return false;
// tryLock 避免在推流停止瞬间发生 native handle 失效的竞争
if (!read_lock_.tryLock()) return false;
try {
if (!check_native_handle() || !is_publishing()) return false;
return OK == lib_publisher_.SmartPublisherOnPCMData(
get(), pcm_data, size, sample_rate, channel,
per_channel_sample_number);
} catch (Exception e) {
Log.e(TAG, "OnPCMData Exception:", e);
return false;
} finally {
read_lock_.unlock();
}
}
读写锁设计保证:多个数据投递线程(音频、视频、水印)可并发持有读锁;release() 需要持有写锁,确保在数据投递过程中 native handle 不会被意外释放。
6.3 实时静音
// ButtonMuteListener
is_mute = !is_mute;
stream_publisher_.SetMute(is_mute);
// 底层调用:SmartPublisherSetMute(handle, isMute ? 1 : 0)
// 静音后音频数据继续采集投递,SDK 内部静默处理(不中断推流)
七、层叠加水印系统
7.1 水印类型与层分配
// LayerPostThread 中的层索引常量
private final int timestamp_index_ = 1; // 时间戳
private final int text1_index_ = 2; // 文字水印一
private final int text2_index_ = 3; // 文字水印二(右下角)
private final int picture_index_ = 4; // 图片水印
private final int rectangle_index_ = 5; // 演示矩形(渐变透明度)
Demo 通过 Spinner 控制水印组合:图片水印、全部水印、文字水印、不加水印四种模式。
7.2 时间戳水印
// 每 400ms 刷新一次,生成当前时间的文字 Bitmap
Bitmap text_bitmap = makeTextBitmap(
makeTimestampString(), // "yyyy-MM-dd HH:mm:ss"
getFontSize(video_w), // 自适应字号
Color.argb(255, 0, 0, 0), // 黑色字体
true, // 白色描边(提升可读性)
Color.argb(255, 255, 255, 255),
true // 粗体
);
for (LibPublisherWrapper i : publisher_list)
i.PostLayerBitmap(timestamp_index_, left, top, text_bitmap,
0, 0, 0, 0, // 不裁剪
0, 0, // 不翻转
0, 0, 0, 0); // 不缩放,不旋转
text_bitmap.recycle(); // 立即回收
7.3 图片水印(自适应缩放)
图片水印从 assets 中加载,当图片尺寸超出视频剩余区域时自动缩放:
// post_picture_layer() 核心缩放逻辑
final float r_w = video_w - left; // 剩余可用宽度
final float r_h = video_h - top; // 剩余可用高度
if (w > r_w || h > r_h) {
float s_w = w, s_h = h;
// 逐步缩放到 0.85^n,最多迭代 10 次(约缩小到原始的 20%)
for (int i = 0; i < 10; ++i) {
s_w *= 0.85f;
s_h *= 0.85f;
if (s_w < r_w && s_h < r_h) break;
}
scale_w = align((int)(s_w + 0.5f), 2);
scale_h = align((int)(s_h + 0.5f), 2);
scale_filter_mode = 3; // 高质量缩放
}
for (LibPublisherWrapper i : publisher_list)
i.PostLayerImageRGBA8888ByteBuffer(picture_index_, left, top,
buffer, 0, bitmap.getRowBytes(), w, h,
0, 0, scale_w, scale_h, scale_filter_mode, 0);
7.4 水印坐标与视频尺寸同步
竖屏推流时视频实际宽高为 (video_height_, video_width_) 而非 (video_width_, video_height_),必须在方向变化时同步通知 LayerPostThread:
// onOrientationChanged() 中
int rotation_degree = cameraImageRotationDegree_;
if (rotation_degree < 0)
layer_post_thread_.update_video_size(0, 0);
else if (90 == rotation_degree || 270 == rotation_degree)
layer_post_thread_.update_video_size(video_height_, video_width_);
else
layer_post_thread_.update_video_size(video_width_, video_height_);
八、录像与推流同步进行

Demo 支持推流与本地录像同时进行,共享同一路编码数据:
// ButtonStartRecorderListener.onClick()
InitAndSetConfig(); // 与推流共用同一个 publisher 实例
ConfigRecorderParam(); // 设置录像目录和文件大小限制
stream_publisher_.StartRecorder(); // 启动录像
startAudioRecorder(); // 若音频未启动则启动
录像参数配置:
// ConfigRecorderParam()
libPublisher.SmartPublisherCreateFileDirectory(recDir); // 创建目录
stream_publisher_.SetRecorderDirectory(recDir); // 设置存储目录
stream_publisher_.SetRecorderFileMaxSize(200); // 单文件最大 200MB,超出自动切分
录像事件回调处理:
case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RECORDER_START_NEW_FILE:
publisher_event = "开始一个新的录像文件: " + param3;
break;
case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_ONE_RECORDER_FILE_FINISHED:
// 录像文件完成后异步重命名(加入时间戳和时长信息)
RecordFileFinishedHandler handler = new RecordFileFinishedHandler()
.set(handle, param3, param1); // param1=时长(ms), param3=文件路径
if (param2 > 0)
handler.set_begin_time(param2); // param2=开始时间戳
executor.execute(handler);
break;
录像文件自动重命名规则(RecordHelper.rename_file):
原始文件名: 2024-01-15-10-30-45-123.mp4
重命名后: 20240115-103045-123-to-103115-456-durms30333-sz52428800-r.mp4
↑ 开始时间 ↑ 结束时间 ↑ 时长(ms) ↑ 文件大小(bytes)
九、分辨率动态切换
Demo 支持推流过程中无缝切换分辨率:
// resolutionSelector Spinner 回调
void SwitchResolution(int position) {
switch (position) {
case 0: video_width_ = 1280; video_height_ = 720; break; // 720P
case 1: video_width_ = 640; video_height_ = 480; break; // 480P
case 2: video_width_ = 1920; video_height_ = 1080; break; // 1080P
}
if (camera2Helper != null)
camera2Helper.switchResolution(video_width_, video_height_);
}
switchResolution 内部先调用 stop() 释放当前摄像头会话,再以新分辨率调用 start(),摄像头重开后 SDK 会自动适配新的图像尺寸。
注意:切换分辨率会导致短暂的画面中断,建议在推流开始前选好分辨率,推流中切换会使观看端出现短暂黑屏。Demo 中对此有防护:
resolutionSelector.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(...) {
if (!stream_publisher_.empty()) {
Log.e(TAG, "Could not switch resolution during publishing..");
return; // 推流中禁止切换
}
SwitchResolution(position);
}
});
十、前后摄像头切换
// ButtonSwitchCameraListener
camera2Helper.switchCamera();
cameraId = camera2Helper.getCameraId();
// Camera2Helper.switchCamera()
public void switchCamera() {
if (CAMERA_ID_BACK.equals(mCameraId)) {
mSpecificCameraId = CAMERA_ID_FRONT; // "1"
} else if (CAMERA_ID_FRONT.equals(mCameraId)) {
mSpecificCameraId = CAMERA_ID_BACK; // "0"
}
stop(); // 释放当前摄像头
start(mVideoWidth, mVideoHeight); // 以新摄像头重启
}
切换后 cameraImageRotationDegree_ 需要重新计算(前后摄的 sensor 方向不同),这在 onCameraOpened 回调中自动完成:
@Override
public void onCameraOpened(CameraDevice cameraDevice, String cameraId,
final Size previewSize, boolean isMirror) {
runOnUiThread(() -> {
cameraImageRotationDegree_ = camera2Helper.getCameraImageRotationDegree(
cur_image_orientation_ < 0 ? orientation_ : cur_image_orientation_);
// 同步更新水印线程的视频尺寸
// ...
});
}
十一、完整生命周期管理
11.1 标准推流完整流程
onCreate
└── 加载 so 库(System.loadLibrary("SmartPublisher"))
└── InitRtspServer(如需 RTSP 服务,全局一次)
└── initView / initCamera
onGlobalLayout(TextureView 布局完成)
└── 请求权限 → initCamera → Camera2Helper.start()
onCameraOpened
└── 计算 cameraImageRotationDegree_
└── 创建预览 ImageView
[用户点击推流按钮]
└── InitAndSetConfig(SmartPublisherOpen + 参数配置)
└── SetURL + StartPublisher
└── startAudioRecorder
└── startLayerPostThread
[推流中]
└── onCameraImageData → PostLayerImageYUV420888ByteBuffer(每帧)
└── onNTAudioRecordV2Frame → OnPCMData(每 10ms)
└── LayerPostThread → PostLayerBitmap(每 400ms)
└── 事件回调 → UI 状态显示
[用户点击停止]
└── StopPublisher → try_release → stopAudioRecorder
└── stopLayerPostThread
onPause → camera2Helper.stop()
onResume → camera2Helper.start()
onDestroy
└── 按顺序释放所有资源
└── deinitialize_sdk(UnInitRtspServer)
11.2 onDestroy 完整释放
@Override
protected void onDestroy() {
record_executor_.cancel_tasks(); // 取消录像文件重命名任务
stopAudioRecorder(); // 停止麦克风
if (snap_shot_impl_ != null) {
snap_shot_impl_.stop();
snap_shot_impl_ = null;
}
snap_shot_publisher_.release(); // 快照实例
stopPush(); // 停止 RTMP 推流
stopRecorder(); // 停止录像
stopRtspPublisher(); // 停止 RTSP 流
stream_publisher_.release(); // 强制释放推流实例
rtsp_server_.reset(); // 停止并释放 RTSP 服务
LibPublisherWrapper.RTSPServer.deinitialize_sdk(libPublisher); // UnInitRtspServer
stopLayerPostThread(); // 停止水印线程
camera2Helper.release(); // 释放摄像头
record_executor_.shutdown(60, TimeUnit.SECONDS); // 等待重命名任务完成
super.onDestroy();
}
十二、码率估算参考表
LibPublisherWrapper 内置了基于分辨率的码率估算,开发者也可参考此表自定义:
| 分辨率 | H.264 硬编 @25fps | H.265 硬编 @25fps | 软编 VBR 最大码率 |
|---|---|---|---|
| 640×480 | 1200 kbps | 800 kbps | 800 kbps |
| 1280×720 | 2100 kbps | 1500 kbps | 1700 kbps |
| 1920×1080 | 4200 kbps | 2800 kbps | 3100 kbps |
以上为 25fps 基准值,SDK 内部按实际帧率等比修正:
kbps = kbps * fps / 25.0
十三、常见问题排查
13.1 推流连接失败
- 确认推流 URL 格式正确:
rtmp://host:port/app/streamName - 检查服务器防火墙是否放通 1935 端口
- 查看
EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTION_FAILED事件是否触发 - 网络切换(WiFi ↔ 4G)后需重新调用
StopPublisher → StartPublisher
13.2 推流无画面(黑屏)
- 检查
cameraImageRotationDegree_是否始终为 -1(OrientationDetector 未触发) - 竖屏设备且未旋转时
orientation_初始值为 0,可在onCreate中强制赋初值 - 确认
PostLayerImageYUV420888ByteBuffer的 Layer 0 数据有正常投递
13.3 音画不同步
- PCMA 必须用 8000Hz 采样;AAC 推荐 44100Hz
per_channel_sample_number必须等于sampleRate / 100- 确认
SmartPublisherSetFPS设置的帧率与摄像头实际输出帧率一致
13.4 发送时延告警
EVENT_DANIULIVE_ERC_PUBLISHER_SEND_DELAY中param1持续增大,说明网络带宽不足- 可降低视频分辨率或码率(通过
switchResolution切到更低档位) - 检查手机是否处于后台省电模式(CPU 降频导致编码跟不上)
13.5 H.265 推流服务端不识别
- 服务端需支持 Enhanced RTMP(SRS 4.x+、nginx-rtmp 需打 patch)
- 若服务端不支持,调用
libPublisher.DisableEnhancedRTMP(handle, 1)关闭扩展 - 部分 CDN 平台尚不支持 H.265 直播,注意提前确认
十四、总结
本文从 RTMP 协议的握手流程、消息结构、FLV 封装格式出发,结合大牛直播 SDK 的实际 Android Demo 源码,完整梳理了一套 RTMP 直播推流的工程实现。核心要点:
- 协议理解是基础:掌握 RTMP 握手、AMF 命令、FLV AudioTag/VideoTag 的结构,有助于在抓包调试时快速定位问题。
- 层叠加模式是关键:
video_opt=3开启层模式后,水印合成、多路输出(RTMP + 录像 + RTSP)均由 SDK 内部处理,性能优于应用层图像合成。 - 方向处理要准确:前后摄旋转公式不同,竖横屏切换时水印坐标系随之变化,必须通过
OrientationDetector回调及时同步。 - 线程安全不可忽视:音频线程、摄像头回调线程、水印线程并发投递数据,
LibPublisherWrapper的读写锁机制保证了 native handle 的生命周期安全。 - 生命周期顺序很重要:
onDestroy中必须按照"停流 → 释放 Publisher → 释放 RTSPServer → deinitialize_sdk"的顺序操作,否则可能出现 native crash。
参考资料
- Adobe RTMP Specification 1.0
- Enhanced RTMP Specification (Veovera Software Organization)
- FLV/F4V File Format Specification Version 10.1
- RFC 6184: RTP Payload Format for H.264 Video
- RFC 7798: RTP Payload Format for HEVC
- 大牛直播SDK官网
📎 CSDN官方博客:音视频牛哥-CSDN博客
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)