技术背景
好多开发者,对我们Android平台轻量级RTSP服务模块有些陌生,不知道这个模块具体适用于怎样的场景,有什么优缺点,实际上,我们的Android平台轻量级RTSP服务模块更适用于内网环境下、对并发要求不高的场景,实现低成本、低延迟的音视频实时传输。本文就上述问题,做个技术探讨,先说适用场景:
1. 内网环境
- 无纸化/电子教室:在这些环境中,需要实现音视频的低延迟传输,而轻量级RTSP服务能够避免单独部署RTSP或RTMP服务器,简化部署流程,同时满足对并发要求不高的场景。
- 车载自组网:在多辆车组成的网络中,轻量级RTSP服务可以确保车辆间实时视频传输,帮助驾驶员了解前方路况等信息。
- 视频监控记录仪:把Android终端做成类似于网络摄像头的执法记录类设备,更便携。
- 智能安全帽:用于内网自组网环境智能安全帽,实时巡检时可录像可快照,指挥中心还可实时查看现场情况。
2. 本地音视频数据传输
- 摄像头和麦克风数据:将本地的摄像头和麦克风采集的音视频数据编码后,通过轻量级RTSP服务汇聚并对外提供可供拉流的RTSP URL,实现音视频数据的实时传输。
- 屏幕共享:除了摄像头和麦克风,轻量级RTSP服务还支持屏幕共享功能,可以将设备屏幕内容编码后通过RTSP服务进行传输。
3. 低成本解决方案
- 避免额外服务器部署:轻量级RTSP服务直接在Android设备上实现,无需额外部署RTSP或RTMP服务器,降低了成本。
- 简化产品设计:对于需要实时音视频传输但不想增加产品复杂度的开发者来说,轻量级RTSP服务是一个理想的选择。
4. 特定功能支持
- 支持多种音视频格式:轻量级RTSP服务通常支持H.264/H.265视频编码和AAC音频编码,满足不同的音视频传输需求。
- 鉴权、单播和组播模式:支持RTSP鉴权功能,保障传输安全;同时支持单播和组播模式,满足不同的传输需求。
- 多服务并发:考虑到单个服务承载能力,支持同时创建多个RTSP服务,并支持获取当前RTSP服务会话连接数。
Android轻量级RTSP服务优缺点探讨
优点
- 低成本与简化部署:
- 轻量级RTSP服务直接在Android设备上实现,无需额外部署RTSP或RTMP服务器,从而降低了硬件和运营成本。
- 简化了产品设计和部署流程,使得开发者能够更快速地集成实时音视频传输功能。
- 高效与低延迟:
- RTSP协议本身对实时性有较好的支持,能够提供低延迟的音视频传输服务。
- 轻量级RTSP服务通过优化传输机制和减少中间环节,进一步提高了传输效率。
- 灵活性与可扩展性:
- 支持多种音视频编码格式,如H.264/H.265视频编码和AAC音频编码,满足不同场景下的传输需求。
- 支持RTSP鉴权功能,保障传输安全;同时支持单播和组播模式,满足不同的传输需求。
- 考虑到单个服务承载能力,支持同时创建多个RTSP服务,并支持获取当前RTSP服务会话连接数,便于管理和扩展。
- 内网环境友好:
- 特别适用于内网环境下的音视频传输,如企业内网、校园网络等。
- 在这些环境中,轻量级RTSP服务能够避免网络延迟和带宽限制等问题,提供稳定的音视频传输服务。
- 易于集成与调试:
- 提供了丰富的接口和文档支持,便于开发者进行集成和调试工作。
- 开发者可以根据实际需求进行定制开发,实现更加个性化的功能。
缺点
- 并发能力有限:
- 轻量级RTSP服务通常适用于对并发要求不高的场景。在高并发场景下,可能需要考虑增加服务器数量或使用更高级的流媒体服务器解决方案。
- 功能相对单一:
- 相比于专业的流媒体服务器解决方案,轻量级RTSP服务的功能可能相对单一。如果需要实现更复杂的音视频处理功能(如转码、录制等),可能需要结合其他工具或服务来实现。
如何实现Android轻量级RTSP服务
在Android平台上实现轻量级RTSP服务,主要涉及到视频和音频的采集、编码、封装成RTSP流,并通过网络进行传输。由于Android原生API并不直接支持RTSP服务器的功能,因此通常需要使用第三方库或自行实现RTSP服务器的逻辑。以下是一个基本的实现步骤和思路:
1. 自行开发
如果你对RTSP协议有较深的理解,并希望实现自定义的RTSP服务器,你可以考虑使用Java或C++(通过JNI)来编写RTSP服务器的核心逻辑。这通常涉及到解析RTSP请求、管理会话、控制音视频流等。
2. 音视频采集与编码
在Android上,你可以使用MediaCodec
API来进行音视频数据的编码。MediaCodec
是Android提供的一个强大的API,支持多种音视频编码格式,如H.264、AAC等。
- 视频采集:可以使用
Camera2
API(Android 5.0及以上)或Camera
API(较旧的Android版本)来捕获视频帧。 - 音频采集:可以使用
AudioRecord
API来捕获音频数据。
3. 封装成RTSP流
将编码后的音视频数据封装成RTSP流需要遵循RTSP协议。这通常涉及到将音视频数据封装成RTP(Real-time Transport Protocol)包,并通过RTSP协议控制这些包的传输。
- RTP封装:RTP是用于在互联网上传输音频和视频数据的协议。你需要将编码后的音视频数据按照RTP包的格式进行封装。
- RTSP控制:RTSP协议用于控制音频/视频的播放停止等。你需要实现RTSP服务器以处理这些控制请求。
4. 网络传输
使用Socket
编程在Android上进行网络传输。你可以使用TCP或UDP协议来传输RTP包。RTSP控制信令通常使用TCP,而RTP数据包则常使用UDP。
5. 集成与测试
将上述所有组件集成到你的Android应用中,并进行充分的测试以确保RTSP服务的稳定性和性能。测试应包括不同的网络环境、设备性能以及并发请求等场景。
6. 注意事项
- 性能优化:音视频处理和网络传输都是资源密集型的任务,因此需要注意性能优化,如合理使用线程、避免内存泄漏等。
- 兼容性:由于Android设备的多样性和不同版本的API差异,你的RTSP服务需要尽可能兼容更多的设备和Android版本。
- 权限:确保你的应用已正确声明了所有必要的权限,以便进行音视频采集和网络通信。
轻量级RTSP服务设计示例
文本以大牛直播SDK的Android平台轻量级RTSP服务模块为例,介绍下我们的开发思路和功能设计。
数据接入类型
轻量级RTSP服务数据源,支持编码前、编码后数据对接:
- 编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型);
- 编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据);
- 拉取RTSP或RTMP流并注入轻量级RTSP服务模块,组合形成内置RTSP网关模块。
接口设计
我们的接口,RTSP服务和流发布分离,并添加了获取RTSP Session链接数接口,便于轻量级RTSP服务模块统计实时并发连接数。
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()。
以Android平台Camera2对接为例,先初始化RTSP Server:
/*
* MainActivity.java
* Author: daniusdk.com
* WeChat: xinsheng120
*/
@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之前调用
}
启动、停止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;
}
}
}
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;
}
}
发布、停止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);
}
}
stopRtspPublisher()实现如下:
//停止发布RTSP流
private void stopRtspPublisher() {
stream_publisher_.StopRtspStream();
stream_publisher_.try_release();
if (!stream_publisher_.is_publishing())
stopAudioRecorder();
}
其中,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);
}
对应的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;
}
发布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;
}
}
}
获取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();
}
总结
Android平台轻量级RTSP服务模块,除了可以对接编码前音视频数据外,还还需要支持对接编码后音视频数据,并实现本地录像、快照等。实现一个完整的轻量级RTSP服务是一个相对复杂的任务,需要对音视频处理、网络编程和RTSP协议有深入的理解。如果你没有这些经验,使用现成的第三方库可能是一个更好的选择。感兴趣的开发者,可以单独跟我们探讨。