一、技术背景
随着内网无纸化办公、电子教室等应用场景对超低延迟音视频传输需求的日益增长,为避免用户或开发者单独部署 RTSP 或 RTMP 服务,大牛直播 SDK 推出了轻量级 RTSP 服务 SDK。该 SDK 能够将本地音视频数据(如摄像头、麦克风等)进行编码后,汇聚到内置 RTSP 服务中,对外提供可供拉流的 RTSP URL,适用于内网环境下对并发要求不高的场景。

二、技术特点

(一)支持的编码格式
- 视频编码:支持 H.264/H.265(Android H.265 硬编码)。
- 音频编码:支持 G.711 A 律、AAC。
(二)功能特性
- 协议支持:支持 RTSP 协议。
- 音量调节:Android 平台采集端支持实时音量调节。
- 视频编码:支持 H.264 特定机型硬编码及 H.265 特定机型硬编码。
- 音视频类型:支持纯音频、纯视频及音视频组合。
- 摄像头切换:支持采集过程中前后摄像头实时切换。
- 参数设置:支持帧率、关键帧间隔(GOP)、码率(bit-rate)设置。
- 水印功能:支持动态文字水印、png 水印。
- 快照功能:支持实时快照。
- 降噪处理:支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD 检测。
- 外部数据对接:支持 YUV 数据(外部编码前视频数据)、PCM 数据(外部编码前音频数据)、外部 H.264、H.265 数据(外部编码后视频数据)以及外部 AAC 数据(外部编码后音频数据)对接。
- 录像功能:支持与录像 SDK 组合使用,实现录像相关功能。
- 其他:支持 RTSP 端口设置、RTSP 鉴权用户名及密码设置、获取当前 RTSP 服务会话连接数,兼容 Android 5.1 及以上版本。
三、技术对接
(一)系统要求
- SDK 支持版本:Android 5.1 及以上版本。
- 支持的 CPU 架构:armv7、arm64、x86、x86_64。
(二)准备工作
- 文件放置:确保 SmartPublisherJniV2.java 放置于 com.daniulive.smartpublisher 包名下(可在其他包名下调用)。
- 库文件添加:将 smartavengine.jar 添加至工程中,并拷贝 至工程目录。
- 权限配置:在 AndroidManifest.xml 中添加相关权限,具体如下:
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />加载 so 库:
static {
    System.loadLibrary("SmartPublisher");
}配置 32/64 位库:在 build.gradle 中进行如下配置:
splits {
    abi {
        enable true
        reset()
        include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
        universalApk true
    }
}修改 app-name:如需集成至自有系统进行测试,可使用大牛直播 SDK 的 app name,授权版则按照授权 app name 正常使用。修改 app-name 可在 strings.xml 中进行如下操作:
xml复制
<string name="app_name">SmartPublisherSDKDemo</string>(三)接口设计
1. SmartRTSPServerSDK 接口
| 调用描述 | 接口 | 接口描述 | 
| 初始化 RTSP Server | InitRtspServer | 初始化 RTSP 服务器(与 UnInitRtspServer 配对使用,启动多个 RTSP 服务也只需调用一次,需在 OpenRtspServer 之前调用) | 
| 创建一个 rtsp server | OpenRtspServer | 创建一个 RTSP 服务器,返回 RTSP 服务器句柄 | 
| 设置端口 | SetRtspServerPort | 设置 RTSP 服务器监听端口,在 StartRtspServer 之前必须设置 | 
| 设置鉴权用户名、密码 | SetRtspServerUserNamePassword | 设置 RTSP 服务器鉴权用户名和密码,可选设置 | 
| 获取 rtsp server 当前会话数 | GetRtspServerClientSessionNumbers | 获取 RTSP 服务器当前的客户会话数,此接口必须在 StartRtspServer 之后调用 | 
| 启动 rtsp server | StartRtspServer | 启动 RTSP 服务器 | 
| 停止 rtsp server | StopRtspServer | 停止 RTSP 服务器 | 
| 关闭 rtsp server | CloseRtspServer | 关闭 RTSP 服务器 | 
| UnInit rtsp server | UnInitRtspServer | 反初始化 RTSP 服务器(与 InitRtspServer 配对使用,启动多个 RTSP 服务也只需调用一次) | 
2. SmartRTSPServerSDK 供 Publisher 调用的接口
| 调用描述 | 接口 | 接口描述 | 
| 设置 rtsp 的流名称 | SetRtspStreamName | 设置 RTSP 的流名称 | 
| 给要发布的 rtsp 流设置 rtsp server | AddRtspStreamServer | 给要发布的 RTSP 流设置 RTSP 服务器,一个流可发布到多个 RTSP 服务器上,服务器的创建启动参考 OpenRtspServer 和 StartRtspServer 接口 | 
| 清除设置的 rtsp server | ClearRtspStreamServer | 清除设置的 RTSP 服务器 | 
| 启动 rtsp 流 | StartRtspStream | 启动 RTSP 流 | 
| 停止 rtsp 流 | StopRtspStream | 停止 RTSP 流 | 
(四)接口调用详解
1. 初始化 SDK
在应用的 onCreate() 方法中,调用 LibPublisherWrapper 的 initialize_sdk() 方法进行 SDK 初始化:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    context_ = this.getApplicationContext();
    libPublisher = new SmartPublisherJniV2();
    LibPublisherWrapper.RTSPServer.initialize_sdk(libPublisher, context_);
}封装代码如下:
public static boolean initialize_sdk(SmartPublisherJniV2 lib_publisher, android.content.Context context) {
    return sdk_context_.initialize(lib_publisher, context);
}具体实现逻辑:
public boolean initialize(SmartPublisherJniV2 lib_publisher, android.content.Context context) {
    if (initialized_) return initialized_result_;
    if (null == lib_publisher) return false;
    if (null == context) return false;
    synchronized (this) {
        if (initialized_) return initialized_result_;
        try {
            int sdk_ret = lib_publisher.InitRtspServer(context);
            if (0 == sdk_ret) {
                initialized_result_ = true;
            } else {
                initialized_result_ = false;
                Log.e(TAG, "call sdk InitRtspServer failed, ret:" + sdk_ret);
            }
        } catch (Exception e) {
            initialized_result_ = false;
            Log.e(TAG, "call sdk InitRtspServer Exception:", e);
        }
        initialized_ = true;
        return initialized_result_;
    }
}2. 启动与停止 RTSP 服务
通过按钮点击事件启动或停止 RTSP 服务:
class ButtonRtspServiceListener implements View.OnClickListener {
    public void onClick(View v) {
        if (!rtsp_server_.empty()) {
            rtsp_server_.reset();
            btnRtspService.setText("启动RTSP服务");
            btnRtspPublisher.setEnabled(false);
            return;
        }
        Log.i(TAG, "onClick start rtsp service..");
        int port = 8554;
        String user_name = null;
        String password = null;
        LibPublisherWrapper.RTSPServer.Handle server_handle = LibPublisherWrapper.RTSPServer.create_and_start_server(libPublisher, port, user_name, password);
        if (null == server_handle) {
            Log.e(TAG, "启动rtsp server失败! 请检查设置的端口是否被占用!");
            return;
        }
        rtsp_server_.reset(server_handle);
        btnRtspService.setText("停止RTSP服务");
        btnRtspPublisher.setEnabled(true);
    }
}3. 发布与停止 RTSP 流
同样通过按钮点击事件控制 RTSP 流的发布与停止:
class ButtonRtspPublisherListener implements View.OnClickListener {
    public void onClick(View v) {
        if (stream_publisher_.is_rtsp_publishing()) {
            stopRtspPublisher();
            btnRtspPublisher.setText("发布RTSP流");
            btnGetRtspSessionNumbers.setEnabled(false);
            btnRtspService.setEnabled(true);
            return;
        }
        Log.i(TAG, "onClick start rtsp publisher..");
        InitAndSetConfig();
        String rtsp_stream_name = "stream1";
        stream_publisher_.SetRtspStreamName(rtsp_stream_name);
        stream_publisher_.ClearRtspStreamServer();
        stream_publisher_.AddRtspStreamServer(rtsp_server_.get_native());
        if (!stream_publisher_.StartRtspStream()) {
            stream_publisher_.try_release();
            Log.e(TAG, "调用发布rtsp流接口失败!");
            return;
        }
        startAudioRecorder();
        startLayerPostThread();
        btnRtspPublisher.setText("停止RTSP流");
        btnGetRtspSessionNumbers.setEnabled(true);
        btnRtspService.setEnabled(false);
    }
}停止 RTSP 流的实现:
private void stopRtspPublisher() {
    stream_publisher_.StopRtspStream();
    stream_publisher_.try_release();
    if (!stream_publisher_.is_publishing()) {
        stopAudioRecorder();
    }
}4. 配置与初始化
在发布 RTSP 流之前,需要进行相关配置与初始化:
private void InitAndSetConfig() {
    if (null == libPublisher) return;
    if (!stream_publisher_.empty()) return;
    Log.i(TAG, "InitAndSetConfig video width: " + video_width_ + ", height" + video_height_ + " imageRotationDegree:" + cameraImageRotationDegree_);
    int audio_opt = 1;
    long handle = libPublisher.SmartPublisherOpen(context_, audio_opt, 3, video_width_, video_height_);
    if (0 == handle) {
        Log.e(TAG, "sdk open failed!");
        return;
    }
    Log.i(TAG, "publisherHandle=" + handle);
    int fps = 25;
    int gop = fps * 3;
    initialize_publisher(libPublisher, handle, video_width_, video_height_, fps, gop);
    stream_publisher_.set(libPublisher, handle);
}初始化编码参数等设置:
private boolean initialize_publisher(SmartPublisherJniV2 lib_publisher, long handle, int width, int height, int fps, int gop) {
    // 编码类型设置
    if (videoEncodeType == 1) {
        // H.264 硬件编码设置
    } else if (videoEncodeType == 2) {
        // HEVC 硬件编码设置
    }
    // 软件编码可变比特率模式设置
    boolean is_sw_vbr_mode = true;
    if (is_sw_vbr_mode) {
        int is_enable_vbr = 1;
        int video_quality = LibPublisherWrapper.estimate_video_software_quality(width, height, true);
        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);
    }
    // 音频编码类型设置
    if (is_pcma_) {
        lib_publisher.SmartPublisherSetAudioCodecType(handle, 3);
    } else {
        lib_publisher.SmartPublisherSetAudioCodecType(handle, 1);
    }
    // 其他参数设置
    lib_publisher.SetSmartPublisherEventCallbackV2(handle, new EventHandlerPublisherV2().set(handler_, record_executor_));
    lib_publisher.SmartPublisherSetSWVideoEncoderProfile(handle, 3);
    lib_publisher.SmartPublisherSetSWVideoEncoderSpeed(handle, 2);
    lib_publisher.SmartPublisherSetGopInterval(handle, gop);
    lib_publisher.SmartPublisherSetFPS(handle, fps);
    boolean is_noise_suppression = true;
    lib_publisher.SmartPublisherSetNoiseSuppression(handle, is_noise_suppression ? 1 : 0);
    boolean is_agc = false;
    lib_publisher.SmartPublisherSetAGC(handle, is_agc ? 1 : 0);
    int echo_cancel_delay = 0;
    lib_publisher.SmartPublisherSetEchoCancellation(handle, 1, echo_cancel_delay);
    return true;
}5. 获取 RTSP 会话数
提供获取当前 RTSP 会话数的功能:
class ButtonGetRtspSessionNumbersListener implements View.OnClickListener {
    public void onClick(View v) {
        if (rtsp_server_.is_running()) {
            int session_numbers = rtsp_server_.get_client_session_number();
            Log.i(TAG, "GetRtspSessionNumbers: " + session_numbers);
            PopRtspSessionNumberDialog(session_numbers);
        }
    }
}封装实现:
public int get_client_session_number() {
    if (!is_running()) return 0;
    if (null == lib_publisher_) return 0;
    long handle = native_handle_.get();
    if (0 == handle) return 0;
    try {
        int ret = lib_publisher_.GetRtspServerClientSessionNumbers(handle);
        return ret;
    } catch (Exception e) {
        Log.e(TAG, "RTSPServer.Handle.get_client_session_number Exception:", e);
        return 0;
    }
}6. 数据投递
以 Camera2 采集为例,进行数据投递:
@Override
public void onCameraImageData(Image image) {
    // 数据处理与投递
    for (LibPublisherWrapper i : publisher_array_) {
        i.PostLayerImageYUV420888ByteBuffer(0, 0, 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,
            scale_w, scale_h, scale_filter_mode, rotation_degree);
    }
}音频采集与投递:
void startAudioRecorder() {
    if (audio_recorder_ != null) return;
    audio_recorder_ = new NTAudioRecordV2(this);
    Log.i(TAG, "startAudioRecorder call audio_recorder_.start()+++...");
    audio_recorder_callback_ = new NTAudioRecordV2CallbackImpl(stream_publisher_, null);
    audio_recorder_.AddCallback(audio_recorder_callback_);
    if (!audio_recorder_.Start(is_pcma_ ? 8000 : 44100, 1)) {
        audio_recorder_.RemoveCallback(audio_recorder_callback_);
        audio_recorder_callback_ = null;
        audio_recorder_ = null;
        Log.e(TAG, "startAudioRecorder start failed.");
    } else {
        Log.i(TAG, "startAudioRecorder call audio_recorder_.start() OK---...");
    }
}
void stopAudioRecorder() {
    if (null == audio_recorder_) return;
    Log.i(TAG, "stopAudioRecorder+++");
    audio_recorder_.Stop();
    if (audio_recorder_callback_ != null) {
        audio_recorder_.RemoveCallback(audio_recorder_callback_);
        audio_recorder_callback_ = null;
    }
    audio_recorder_ = null;
    Log.i(TAG, "stopAudioRecorder---");
}回调音频数据投递:
private static class NTAudioRecordV2CallbackImpl implements NTAudioRecordV2Callback {
    private WeakReference<LibPublisherWrapper> publisher_0_;
    private WeakReference<LibPublisherWrapper> publisher_1_;
    public NTAudioRecordV2CallbackImpl(LibPublisherWrapper publisher_0) {
        if (publisher_0 != null)
            publisher_0_ = new WeakReference<>(publisher_0);
    }
    private final LibPublisherWrapper get_publisher_0() {
        if (publisher_0_ != null)
            return publisher_0_.get();
        return null;
    }
    @Override
    public void onNTAudioRecordV2Frame(ByteBuffer data, int size, int sampleRate, int channel, int per_channel_sample_number) {
        LibPublisherWrapper publisher_0 = get_publisher_0();
        if (publisher_0 != null)
            publisher_0.OnPCMData(data, size, sampleRate, channel, per_channel_sample_number);
    }
}7. 释放资源
在 onDestroy() 方法中,释放相关资源:
@Override
protected void onDestroy() {
    Log.i(TAG, "activity destory!");
    stopAudioRecorder();
    stopRtspPublisher();
    stream_publisher_.release();
    rtsp_server_.reset();
    LibPublisherWrapper.RTSPServer.deinitialize_sdk(libPublisher);
    stopLayerPostThread();
    if (camera2Helper != null) {
        camera2Helper.release();
    }
    super.onDestroy();
}四、总结
以上为 Android 平台轻量级 RTSP 服务模块的详细技术对接说明。该模块不仅支持编码前音视频数据的对接,还支持编码后音视频数据的对接,并可与本地录像、快照等功能组合使用,以满足多样化的应用场景需求。开发者可根据实际需求进行集成与开发,如有任何疑问或需要进一步探讨,欢迎与我们联系。
 
 
                     
            
        













 
                    

 
                 
                    