Android 端构建高性能 RTSP 转 RTMP|轻量级RTSP服务 网关:透传与二次编码深度实践

在移动端音视频应用中,将外部 RTSP 摄像头流(如海康、大华 IPC)接入手机,并将其转发至 RTMP 服务器(如 Nginx、SRS)或在本地开启 RTSP 服务供其他设备拉流,是一个非常典型的"单兵作战"或"移动网关"场景。

本文结合 SmartPlayer 的核心代码,深入拆解基于大牛直播 SDK 实现的两种核心转发模式:数据透传(Pass-through)二次编码(Transcoding),并探讨如何利用内置的轻量级 RTSP 服务模块实现本地分发。

核心架构:Player 与 Publisher 的联动

SmartPlayer.java 中,我们维护了两个核心对象:

  1. Player (拉流端): LibPlayerWrapper mPlayerWrapper,负责拉取 RTSP 流,根据模式决定是只输出编码数据还是解码出 YUV 数据。

  2. Publisher (推流端): LibPublisherWrapper mStreamPublisher,负责接收来自 Player 的数据,并将其推送到 RTMP 服务器或发布为本地 RTSP 流。

1. 模式切换的"总开关"

我们在 UI 上通过 Spinner 控件控制转发模式,这直接决定了底层数据流的走向。

java 复制代码
// SmartPlayer.java 中 setupSpinners 方法片段
mSpTransferMode.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        boolean newRelayMode = (position == 0); // 0: 透传模式, 1: 二次编码

        if (isRelayMode != newRelayMode) {
            // ... 状态重置逻辑
            isRelayMode = newRelayMode;
            Log.i(TAG, "传输模式切换为: " + (isRelayMode ? "透传" : "二次编码"));
        }

        if (isRelayMode) {
            // 透传模式:直接转发编码数据 (mAudioOpt=2, mVideoOpt=2 表示编码后数据)
            mAudioOpt = 2;
            mVideoOpt = 2;
        } else {
            // 二次编码模式:需要重新采集或处理数据 (mVideoOpt=3 表示层叠加模式)
            mAudioOpt = 0; 
            mVideoOpt = 3;
        }
    }
    // ...
});

模式一:极速转发------透传模式 (Pass-through)

核心逻辑: 透传模式下,Player 不进行解码,Publisher 不进行编码。Player 将拉取到的 H.264/H.265 和 AAC 数据包原封不动地通过回调抛出,Publisher 接收后直接打包发送。这种模式 CPU 占用极低,延迟最小。

数据流向:

RTSP Source -> Player Demuxer -> Callback (Encoded Data) -> Publisher Muxer -> RTMP/RTSP Dest

代码实现:

SmartPlayer.java 中,当 isRelayMode 为 true 时,我们不设置 ExternalRender(因为不需要 YUV 数据),而是注册 PlayerVideoDataCallbackPlayerAudioDataCallback

视频数据回调实现:

java 复制代码
// SmartPlayer.java 内部类 PlayerVideoDataCallback
class PlayerVideoDataCallback implements NTVideoDataCallback {
    private WeakReference<LibPublisherWrapper> publisher_;
    private int video_buffer_size = 0;
    private ByteBuffer video_buffer_ = null;

    public PlayerVideoDataCallback(LibPublisherWrapper publisher) {
        if (publisher != null)
            publisher_ = new WeakReference<>(publisher);
    }

    @Override
    public ByteBuffer getVideoByteBuffer(int size) {
        // 动态分配复用 Buffer,减少 GC
        if (size < 1) return null;
        if (size <= video_buffer_size && video_buffer_ != null) {
            return video_buffer_;
        }
        video_buffer_size = size + 1024;
        video_buffer_size = (video_buffer_size + 0xf) & (~0xf);
        video_buffer_ = ByteBuffer.allocateDirect(video_buffer_size);
        return video_buffer_;
    }

    @Override
    public void onVideoDataCallback(int ret, int video_codec_id, int sample_size, int is_key_frame, long timestamp, int width, int height, long presentation_timestamp) {
        if (video_buffer_ == null) return;

        LibPublisherWrapper publisher = publisher_.get();
        if (null == publisher) return;

        if (!publisher.is_publishing()) return;

        video_buffer_.rewind();

        // 核心:直接投递编码后的数据给 Publisher
        publisher.PostVideoEncodedData(video_codec_id, video_buffer_, sample_size, is_key_frame, timestamp, presentation_timestamp);
    }
}

同理,音频也通过 PlayerAudioDataCallback 直接透传 PostAudioEncodedData


模式二:灵活处理------二次编码模式 (Transcoding)

核心逻辑: 二次编码模式下,Player 将视频解码为 YUV (I420) 数据。我们在中间环节可以对 YUV 数据进行处理(如添加水印、文字、裁剪等),然后交给 Publisher 重新编码发送。

数据流向:

RTSP Source -> Player Decoder -> YUV Callback -> LayerPostThread (加水印) -> Publisher Encoder -> RTMP/RTSP Dest

代码实现:

1. 启动播放时的配置:startPlayLogic 中,如果不是透传模式,我们需要创建一个 I420ExternalRender 来接收解码后的数据。

java 复制代码
// SmartPlayer.java
private boolean startPlayLogic() {
    if (isPlaying) return false;
    // ... 初始化校验
    
    NTExternalRender externalRender = null;
    if (!isRelayMode) {
        Log.i(TAG, "二次编码模式: 设置 ExternalRender");
        // 创建 I420 渲染器,关联 Publisher 列表
        externalRender = new I420ExternalRender(mPublisherArray);
    }

    // 启动播放器,传入 externalRender
    boolean ret = mPlayerWrapper.StartPlayer(
            mSurfaceView,
            null,
            externalRender, 
            1,    // render_scale_mode
            true, // is_fast_startup
            isHardwareDecoder,
            true  // is_low_latency
    );
    // ...
}

2. 数据的桥接与渲染: I420ExternalRender 实现了 NTExternalRender 接口,它从 Player 获取解码后的 I420 数据,并调用 Publisher 的 PostLayerImageI420ByteBuffer 方法。这里配合 LayerPostThread 实现了强大的图层叠加功能。

java 复制代码
// SmartPlayer.java 内部类 I420ExternalRender
private static class I420ExternalRender implements NTExternalRender {
    // ... 成员变量省略

    @Override
    public void onNTRenderFrame(int width, int height, long timestamp) {
        if (y_buffer_ == null) return;
        y_buffer_.rewind();
        u_buffer_.rewind();
        v_buffer_.rewind();

        if (publisher_list_ != null) {
            for (WeakReference<LibPublisherWrapper> ref : publisher_list_) {
                LibPublisherWrapper p = ref.get();
                if (p != null && !p.empty()) {
                    // 将解码后的 YUV 数据投递到视频层 (Layer 0)
                    // 后续 LayerPostThread 会在此基础上叠加水印层
                    p.PostLayerImageI420ByteBuffer(0, 0, 0, y_buffer_, 0, y_row_bytes_, u_buffer_, 0, u_row_bytes_, v_buffer_, 0, v_row_bytes_, width_, height_, 0, 0, 0, 0, 0, 0);
                }
            }
        }
    }
    // ... 内存分配逻辑省略
}

3. 水印与图层合成: 在二次编码模式下,我们启动了 LayerPostThread,它负责定期更新动态水印(如时间戳、图片 Logo)。

java 复制代码
// SmartPlayer.java
private boolean startPushRtmpLogic() {
    initAndSetConfig();
    // ... SetURL logic
    
    if (!isRelayMode) {
        // 非透传模式下,需要启动音频录制(因为不透传音频流)和图层处理线程
        startAudioRecorder();
        startLayerPostThread();
    }
    return true;
}

进阶功能:内置轻量级 RTSP 服务

除了推送到 RTMP 服务器,SmartPlayer 还能利用 SmartPublisherJniV2 开启一个本地的 RTSP Server。这意味着手机不仅是转发器,还是一个标准的 RTSP 流媒体服务器。

1. 开启 RTSP 服务

首先需要初始化并启动 RTSP 服务监听端口(如 18554)。

java 复制代码
// SmartPlayer.java
private void handleRtspService() {
    if (isRTSPServiceRunning) {
        // ... 停止服务逻辑
    } else {
        mRtspServerHandle = libPublisher.OpenRtspServer(0);
        if (mRtspServerHandle == 0) return;
        
        libPublisher.SetRtspServerPort(mRtspServerHandle, 18554);
        
        if (libPublisher.StartRtspServer(mRtspServerHandle, 0) == 0) {
            isRTSPServiceRunning = true;
            // ... UI 更新
        } 
        // ... Error handling
    }
}

2. 发布流到 RTSP 服务

启动服务后,我们需要将 Publisher 当前的流(无论是透传来的还是二次编码的)"挂载"到 RTSP 服务上。

java 复制代码
// SmartPlayer.java
private void handleRtspPublish() {
    if (mStreamPublisher.is_rtsp_publishing()) {
        // ... 停止发布逻辑
    } else {
        initAndSetConfig();
        
        // 设置 RTSP 流名称,例如 stream1
        // 最终地址将是 rtsp://ip:18554/stream1
        mStreamPublisher.SetRtspStreamName("stream1");
        
        mStreamPublisher.ClearRtspStreamServer();
        // 将流关联到之前创建的 Server Handle
        mStreamPublisher.AddRtspStreamServer(mRtspServerHandle);
        
        if (mStreamPublisher.StartRtspStream()) {
            if (!isRelayMode) {
                // 二次编码模式需采集
                startAudioRecorder();
                startLayerPostThread();
            }
            // ... UI 更新
        }
    }
}

总结

通过 SmartPlayer 的实现,我们清晰地看到了大牛直播 SDK 在处理音视频转发时的灵活性:

  • 透传模式是性能首选,适合纯粹的网关应用,CPU 占用极低,画质无损。

  • 二次编码模式是功能首选,虽然牺牲了一定的 CPU 资源,但换来了对视频内容的完全控制权(水印、AI 处理接口等)。

  • RTSP 服务模块则让 Android 设备具备了服务端的各个能力,极大地扩展了应用场景。

开发者可以根据实际的业务需求(是对延迟敏感,还是需要添加品牌 Logo),在代码中灵活切换 isRelayMode,构建最适合自己的移动端音视频应用。

📎 CSDN官方博客:音视频牛哥-CSDN博客

相关推荐
方白羽2 小时前
Android 16 (Target 36) 应用适配指南
android·app·客户端
微爱帮监所写信寄信3 小时前
微爱帮监狱寄信写信系统后台PHP框架优化实战手册
android·开发语言·人工智能·网络协议·微信·https·php
妫以明3 小时前
FFmpeg:视频处理界的瑞士军刀
ffmpeg·音视频
娅娅梨3 小时前
音视频-YUV格式
音视频
私人珍藏库3 小时前
[Android] 无印2.2视频解析去水印工具,支持多个平台 2025.12.29更新
android·app·安卓·工具·软件·音乐·music
雨声不在3 小时前
多进程的多语言切换
android
aitoolhub3 小时前
AI视频生成:核心技术框架与工作逻辑
人工智能·计算机视觉·aigc·音视频·设计语言
Yang-Never4 小时前
Android 内存泄漏 -> ViewModel持有Activity/Fragment导致的内存泄漏
android·java·开发语言·kotlin·android studio
Android_xiong_st4 小时前
(原创)Android遍历文件方法walk函数介绍
android