Android平台轻量级RTSP服务模块技术接入说明

本文涉及的产品
视觉智能开放平台,分割抠图1万点
视觉智能开放平台,图像通用资源包5000点
视觉智能开放平台,视频通用资源包5000点
简介: 为满足内网无纸化/电子教室等内网超低延迟需求,避免让用户配置单独的服务器,大牛直播SDK在推送端发布了轻量级RTSP服务SDK。轻量级RTSP服务解决的核心痛点是避免用户或者开发者单独部署RTSP或者RTMP服务,实现本地的音视频数据(如摄像头、麦克风),编码后,汇聚到内置RTSP服务,对外提供可供拉流的RTSP URL,轻量级RTSP服务,适用于内网环境下,对并发要求不高的场景,支持H.264/H.265,支持RTSP鉴权、单播、组播模式,考虑到单个服务承载能力,我们支持同时创建多个RTSP服务,并支持获取当前RTSP服务会话连接数。

技术背景

为满足内网无纸化/电子教室等内网超低延迟需求,避免让用户配置单独的服务器,大牛直播SDK在推送端发布了轻量级RTSP服务SDK。

轻量级RTSP服务解决的核心痛点是避免用户或者开发者单独部署RTSP或者RTMP服务,实现本地的音视频数据(如摄像头、麦克风),编码后,汇聚到内置RTSP服务,对外提供可供拉流的RTSP URL,轻量级RTSP服务,适用于内网环境下,对并发要求不高的场景,支持H.264/H.265,支持RTSP鉴权、单播、组播模式,考虑到单个服务承载能力,我们支持同时创建多个RTSP服务,并支持获取当前RTSP服务会话连接数。

轻量级RTSP服务数据源,支持编码前、编码后数据对接:

  • 编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型);
  • 编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据);
  • 拉取RTSP或RTMP流并注入轻量级RTSP服务模块,组合形成内置RTSP网关模块。

技术对接

系统要求

  • SDK支持Android5.1及以上版本;
  • 支持的CPU架构:armv7, arm64, x86, x86_64。

准备工作

  • 确保SmartPublisherJniV2.java放到com.daniulive.smartpublisher包名下(可在其他包名下调用);
  • smartavengine.jar加入到工程;
  • 拷贝libSmartPublisher.so到工程;
  • AndroidManifast.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" />

image.gif

  • Load相关so:
static {  
    System.loadLibrary("SmartPublisher");
}

image.gif

  • build.gradle配置32/64位库:
splits {
    abi {
        enable true
        reset()
        // Specifies a list of ABIs that Gradle should create APKs for
        include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' //select ABIs to build APKs for
        // Specify that we do not want to also generate a universal APK that includes all ABIs
        universalApk true
    }
}

image.gif

  • 如需集成到自己系统测试,请用大牛直播SDK的app name,授权版按照授权app name正常使用即可;
  • 如何改app-name,strings.xml做以下修改:
<string name="app_name">SmartPublisherSDKDemo</string>

image.gif

接口设计

Android内置轻量级RTSP服务SDK接口详解

调用描述

接口

接口描述

SmartRTSPServerSDK

初始化RTSP Server

InitRtspServer

Init rtsp server(和UnInitRtspServer配对使用,即便是启动多个RTSP服务,也只需调用一次InitRtspServer,请确保在OpenRtspServer之前调用)

创建一个rtsp server

OpenRtspServer

创建一个rtsp server,返回rtsp server句柄

设置端口

SetRtspServerPort

设置rtsp server 监听端口, 在StartRtspServer之前必须要设置端口

设置鉴权用户名、密码

SetRtspServerUserNamePassword

设置rtsp server 鉴权用户名和密码, 这个可以不设置,只有需要鉴权的再设置

获取rtsp server当前会话数

GetRtspServerClientSessionNumbers

获取rtsp server当前的客户会话数, 这个接口必须在StartRtspServer之后再调用

启动rtsp server

StartRtspServer

启动rtsp server

停止rtsp server

StopRtspServer

停止rtsp server

关闭rtsp server

CloseRtspServer

关闭rtsp server

UnInit rtsp server

UnInitRtspServer

UnInit rtsp server(和InitRtspServer配对使用,即便是启动多个RTSP服务,也只需调用一次UnInitRtspServer)

SmartRTSPServerSDK供Publisher调用的接口

设置rtsp的流名称

SetRtspStreamName

设置rtsp的流名称

给要发布的rtsp流设置rtsp server

AddRtspStreamServer

给要发布的rtsp流设置rtsp server, 一个流可以发布到多个rtsp server上,rtsp server的创建启动请参考OpenRtspServer和StartRtspServer接口

清除设置的rtsp server

ClearRtspStreamServer

清除设置的rtsp server

启动rtsp流

StartRtspStream

启动rtsp流

停止rtsp流

StopRtspStream

停止rtsp流

功能支持

  •  [视频格式]H.264/H.265(Android H.265硬编码);
  • [音频格式]G.711 A律、AAC;
  • 协议:RTSP;
  • [音量调节]Android平台采集端支持实时音量调节;
  • [H.264硬编码]支持H.264特定机型硬编码;
  • [H.265硬编码]支持H.265特定机型硬编码;
  • [音视频]支持纯音频/纯视频/音视频;
  • [摄像头]支持采集过程中,前后摄像头实时切换;
  • 支持帧率、关键帧间隔(GOP)、码率(bit-rate)设置;
  • [实时水印]支持动态文字水印、png水印;
  • [实时快照]支持实时快照;
  • [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
  • [外部编码前视频数据对接]支持YUV数据对接;
  • [外部编码前音频数据对接]支持PCM对接;
  • [外部编码后视频数据对接]支持外部H.264、H.265数据对接;
  • [外部编码后音频数据对接]外部AAC数据对接;
  • [扩展录像功能]支持和录像SDK组合使用,录像相关功能。
  • 支持RTSP端口设置;
  • 支持RTSP鉴权用户名、密码设置;
  • 支持获取当前RTSP服务会话连接数;
  • 支持Android 5.1及以上版本。

接口调用详解

本文以大牛直播SDK Android平台Camera2Demo为例,启动RTSP服务、发布RTSP流之前,可以先选择视频分辨率、软编还是硬编码,音频是PCMA还是AAC编码等基础设置,其他参数的设置,可以参考下面InitAndSetConfig()。

image.gif

以Android平台Camera2对接为例,先初始化RTSP Server:

/*
 * MainActivity.java
 * Author: daniusdk.com
 */
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    ...
    context_ = this.getApplicationContext();
    
    libPublisher = new SmartPublisherJniV2();
    libPublisher.InitRtspServer(context_);      //和UnInitRtspServer配对使用,即便是启动多个RTSP服务,也只需调用一次InitRtspServer,请确保在OpenRtspServer之前调用
}

image.gif

启动、停止RTSP服务:

//启动/停止RTSP服务
class ButtonRtspServiceListener implements View.OnClickListener {
    public void onClick(View v) {
        if (isRTSPServiceRunning) {
            stopRtspService();
            btnRtspService.setText("启动RTSP服务");
            btnRtspPublisher.setEnabled(false);
            isRTSPServiceRunning = false;
            return;
        }
        Log.i(TAG, "onClick start rtsp service..");
        rtsp_handle_ = libPublisher.OpenRtspServer(0);
        if (rtsp_handle_ == 0) {
            Log.e(TAG, "创建rtsp server实例失败! 请检查SDK有效性");
        } else {
            int port = 8554;
            if (libPublisher.SetRtspServerPort(rtsp_handle_, port) != 0) {
                libPublisher.CloseRtspServer(rtsp_handle_);
                rtsp_handle_ = 0;
                Log.e(TAG, "创建rtsp server端口失败! 请检查端口是否重复或者端口不在范围内!");
            }
            if (libPublisher.StartRtspServer(rtsp_handle_, 0) == 0) {
                Log.i(TAG, "启动rtsp server 成功!");
            } else {
                libPublisher.CloseRtspServer(rtsp_handle_);
                rtsp_handle_ = 0;
                Log.e(TAG, "启动rtsp server失败! 请检查设置的端口是否被占用!");
            }
            btnRtspService.setText("停止RTSP服务");
            btnRtspPublisher.setEnabled(true);
            isRTSPServiceRunning = true;
        }
    }
}

image.gif

stopRtspService()实现如下:

//停止RTSP服务
private void stopRtspService() {
    if(!isRTSPServiceRunning)
    {
        return;
    }
    if (libPublisher != null && rtsp_handle_ != 0) {
        libPublisher.StopRtspServer(rtsp_handle_);
        libPublisher.CloseRtspServer(rtsp_handle_);
        rtsp_handle_ = 0;
    }
}

image.gif

发布、停止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_handle_);
        if (!stream_publisher_.StartRtspStream()) {
            stream_publisher_.try_release();
            Log.e(TAG, "调用发布rtsp流接口失败!");
            return;
        }
        startAudioRecorder();
        startLayerPostThread();
        btnRtspPublisher.setText("停止RTSP流");
        btnGetRtspSessionNumbers.setEnabled(true);
        btnRtspService.setEnabled(false);
    }
}

image.gif

stopRtspPublisher()实现如下:

//停止发布RTSP流
private void stopRtspPublisher() {
    stream_publisher_.StopRtspStream();
    stream_publisher_.try_release();
    if (!stream_publisher_.is_publishing())
        stopAudioRecorder();
}

image.gif

其中,InitAndSetConfig()实现如下,通过调研SmartPublisherOpen()接口,生成推送实例句柄。

/*
 * MainActivity.java
 * Author: daniusdk.com
 */
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);
}

image.gif

对应的initialize_publisher()实现如下,设置软硬编码、帧率、关键帧间隔等。

private boolean initialize_publisher(SmartPublisherJniV2 lib_publisher, long handle, int width, int height, int fps, int gop) {
    if (null == lib_publisher) {
        Log.e(TAG, "initialize_publisher lib_publisher is null");
        return false;
    }
    if (0 == handle) {
        Log.e(TAG, "initialize_publisher handle is 0");
        return false;
    }
    if (videoEncodeType == 1) {
        int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, true);
        Log.i(TAG, "h264HWKbps: " + kbps);
        int isSupportH264HWEncoder = lib_publisher.SetSmartPublisherVideoHWEncoder(handle, kbps);
        if (isSupportH264HWEncoder == 0) {
            lib_publisher.SetNativeMediaNDK(handle, 0);
            lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
            lib_publisher.SetVideoHWEncoderQuality(handle, 39);
            lib_publisher.SetAVCHWEncoderProfile(handle, 0x08); // 0x01: Baseline, 0x02: Main, 0x08: High
            // lib_publisher.SetAVCHWEncoderLevel(handle, 0x200); // Level 3.1
            // lib_publisher.SetAVCHWEncoderLevel(handle, 0x400); // Level 3.2
            // lib_publisher.SetAVCHWEncoderLevel(handle, 0x800); // Level 4
            lib_publisher.SetAVCHWEncoderLevel(handle, 0x1000); // Level 4.1 多数情况下,这个够用了
            //lib_publisher.SetAVCHWEncoderLevel(handle, 0x2000); // Level 4.2
            // lib_publisher.SetVideoHWEncoderMaxBitrate(handle, ((long)h264HWKbps)*1300);
            Log.i(TAG, "Great, it supports h.264 hardware encoder!");
        }
    } else if (videoEncodeType == 2) {
        int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, false);
        Log.i(TAG, "hevcHWKbps: " + kbps);
        int isSupportHevcHWEncoder = lib_publisher.SetSmartPublisherVideoHevcHWEncoder(handle, kbps);
        if (isSupportHevcHWEncoder == 0) {
            lib_publisher.SetNativeMediaNDK(handle, 0);
            lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
            lib_publisher.SetVideoHWEncoderQuality(handle, 39);
            // libPublisher.SetVideoHWEncoderMaxBitrate(handle, ((long)hevcHWKbps)*1200);
            Log.i(TAG, "Great, it supports hevc hardware encoder!");
        }
    }
    boolean is_sw_vbr_mode = true;
    //H.264 software encoder
    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);
    // lib_publisher.SmartPublisherSetSWVideoBitRate(handle, 600, 1200);
    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;
}

image.gif

发布RTSP流成功后,会回调上来可供拉流的RTSP URL:

private static class EventHandlerPublisherV2 implements NTSmartEventCallbackV2 {
    @Override
    public void onNTSmartEventCallbackV2(long handle, int id, long param1, long param2, String param3, String param4, Object param5) {
        switch (id) {
            ...
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RTSP_URL:
                publisher_event = "RTSP服务URL: " + param3;
                break;
        }
    }
}

image.gif

获取RTSP Session会话数:

//获取RTSP会话数
class ButtonGetRtspSessionNumbersListener implements View.OnClickListener {
    public void onClick(View v) {
        if (libPublisher != null && rtsp_handle_ != 0) {
            int session_numbers = libPublisher.GetRtspServerClientSessionNumbers(rtsp_handle_);
            Log.i(TAG, "GetRtspSessionNumbers: " + session_numbers);
            PopRtspSessionNumberDialog(session_numbers);
        }
    }
}
//当前RTSP会话数弹出框
private void PopRtspSessionNumberDialog(int session_numbers) {
    final EditText inputUrlTxt = new EditText(this);
    inputUrlTxt.setFocusable(true);
    inputUrlTxt.setEnabled(false);
    String session_numbers_tag = "RTSP服务当前客户会话数: " + session_numbers;
    inputUrlTxt.setText(session_numbers_tag);
    AlertDialog.Builder builderUrl = new AlertDialog.Builder(this);
    builderUrl
            .setTitle("内置RTSP服务")
            .setView(inputUrlTxt).setNegativeButton("确定", null);
    builderUrl.show();
}

image.gif

数据投递如下(以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);
}

image.gif

音频采集投递设计如下:

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---");
}

image.gif

回调Audio数据的地方,直接投递出去:

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);
    }
}

image.gif

onDestroy() 的时候,调研UnInitRtspServer()即可:

@Override
protected void onDestroy() {
    Log.i(TAG, "activity destory!");
    stopAudioRecorder();
    stopRtspPublisher();
    stopRtspService();
    isRTSPServiceRunning = false;
    stream_publisher_.release();
    if (libPublisher != null)
        libPublisher.UnInitRtspServer();      //如已启用内置服务功能(InitRtspServer),调用UnInitRtspServer, 注意,即便是启动多个RTSP服务,也只需调用UnInitRtspServer一次
    stopLayerPostThread();
    if (camera2Helper != null) {
        camera2Helper.release();
    }
    super.onDestroy();
}

image.gif

总结

以上是Android平台轻量级RTSP服务模块详细的对接说明,除了可以对接编码前音视频数据外,模块还支持对接编码后音视频数据,并实现本地录像、快照等功能组合使用。感兴趣的开发者,可以单独跟我们探讨。

相关文章
|
2月前
|
监控 Android开发 数据安全/隐私保护
批量发送短信的平台,安卓群发短信工具插件脚本,批量群发短信软件【autojs版】
这个Auto.js脚本实现了完整的批量短信发送功能,包含联系人管理、短信内容编辑、发送状态监控等功能
|
6月前
|
存储 编解码 监控
Android平台GB28181执法记录仪技术方案与实现
本文介绍了大牛直播SDK的SmartGBD在执法记录仪场景中的应用。GB28181协议作为视频监控联网的国家标准,为设备互联互通提供规范。SmartGBD专为Android平台设计,支持音视频采集、编码与传输,具备自适应算法和多功能扩展优势。文章分析了执法记录仪的需求,如实时音视频传输、设备管理及数据安全,并详细阐述了基于SmartGBD的技术实现方案,包括环境准备、SDK集成、设备注册、音视频处理及功能扩展等步骤。最后展望了SmartGBD在未来智慧物联领域的广阔应用前景。
257 13
|
6月前
|
监控 Shell Linux
Android调试终极指南:ADB安装+多设备连接+ANR日志抓取全流程解析,覆盖环境变量配置/多设备调试/ANR日志分析全流程,附Win/Mac/Linux三平台解决方案
ADB(Android Debug Bridge)是安卓开发中的重要工具,用于连接电脑与安卓设备,实现文件传输、应用管理、日志抓取等功能。本文介绍了 ADB 的基本概念、安装配置及常用命令。包括:1) 基本命令如 `adb version` 和 `adb devices`;2) 权限操作如 `adb root` 和 `adb shell`;3) APK 操作如安装、卸载应用;4) 文件传输如 `adb push` 和 `adb pull`;5) 日志记录如 `adb logcat`;6) 系统信息获取如屏幕截图和录屏。通过这些功能,用户可高效调试和管理安卓设备。
|
机器学习/深度学习 监控 前端开发
|
JavaScript 前端开发 Java
|
物联网 大数据 Android开发
|
Java 测试技术 Android开发

热门文章

最新文章