前言

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
┌──────────────────────────┐
│      播放端               │
└──────────────────────────┘

架构亮点:

  1. 层叠加模式(video_opt=3):摄像头画面作为第 0 层,水印作为高层,SDK 内部合成后统一编码,一次编码多路输出(RTMP + 录像 + RTSP),CPU 开销最优。
  2. 推流/录像/RTSP 三合一:同一个 stream_publisher_ 实例可同时执行 RTMP 推流、本地录像和 RTSP 服务,共享同一路编码数据。
  3. 线程安全设计: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("停止推送");

流程说明:

  1. SetURL 设置推流地址,SDK 内部解析 rtmp://host:port/app/streamName
  2. StartPublisher 触发底层 TCP 连接、RTMP 握手和 publish 命令发送,该操作异步完成
  3. 连接结果通过 EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTED / CONNECTION_FAILED 事件回调通知
  4. 摄像头和水印数据在 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 直播推流的工程实现。核心要点:

  1. 协议理解是基础:掌握 RTMP 握手、AMF 命令、FLV AudioTag/VideoTag 的结构,有助于在抓包调试时快速定位问题。
  2. 层叠加模式是关键:video_opt=3 开启层模式后,水印合成、多路输出(RTMP + 录像 + RTSP)均由 SDK 内部处理,性能优于应用层图像合成。
  3. 方向处理要准确:前后摄旋转公式不同,竖横屏切换时水印坐标系随之变化,必须通过 OrientationDetector 回调及时同步。
  4. 线程安全不可忽视:音频线程、摄像头回调线程、水印线程并发投递数据,LibPublisherWrapper 的读写锁机制保证了 native handle 的生命周期安全。
  5. 生命周期顺序很重要: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博客

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐