Android上实现RTSP服务器的方法
作者:音视频牛哥
技术背景
在Android上实现RTSP服务器确实是一个不太常见的需求,因为Android平台主要是为客户端应用设计的。在一些内网场景下,我们更希望把安卓终端或开发板,作为一个IPC(网络摄像机)一样,对外提供个拉流的rtsp url,然后把摄像头麦克风甚至屏幕采集的数据,共享出去,轻量级RTSP的设计理念脱颖而出。
轻量级RTSP服务设计初衷,就是避免用户单独部署RTSP或者RTMP服务,实现本地的音视频数据(如摄像头、麦克风),编码后,汇聚到内置RTSP服务,对外提供可供拉流的RTSP URL,轻量级RTSP服务,适用于内网环境下,对并发要求不高的场景,支持H.264/H.265,支持RTSP鉴权、单播、组播模式,考虑到单个服务承载能力,我们支持同时创建多个RTSP服务,并支持获取当前RTSP服务会话连接数。
如何实现RTSP服务器
如果你找不到合适的库,或者需要更高级的功能,你可以考虑编写自己的RTSP服务器:
- 了解RTSP协议:首先,你需要深入了解RTSP协议的工作原理,包括其消息格式、会话管理和流控制机制。
- 网络编程:在Android上,你可以使用Java的Socket API来处理网络通信,也可以直接底层实现,然后对上提供jni的接口。你需要编写代码来监听端口、接收RTSP请求、解析请求并发送响应。
- 媒体处理:RTSP服务器需要能够捕获、编码和传输媒体数据。你可以使用Android的Camera或Camera2的API来捕获视频,并使用FFmpeg或Android的MediaCodec API来实现音视频数据编码。
- 多线程和并发:RTSP服务器需要处理多个并发客户端连接。你可以使用Java的线程或并发API来管理这些连接。
功能设计
一个完整的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及以上版本。
接口设计
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流 |
调用逻辑
以大牛直播SDK的Android平台Camera2对接为例,先初始化RTSP Server,启动RTSP服务,然后发布RTSP流即可,如果需要停止RTSP服务,那么先停止RTSP流,再停止RTSP服务即可:
/* * 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(); }
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(); }
总结
Android上实现RTSP服务器是一个极具挑战的任务,功能设计这块,除了需要支持接编码前音视频数据外,还需要支持对接编码后音视频数据,并实现本地录像、快照等功能组合使用。需要注意的是,就像海康、大华的摄像头一样,对外的并发,一般限于4-8个,Android设备的性能一般来说,可能不足以处理高负载的RTSP服务器,但是小并发模式下,能稳定的运行,就达到设计初衷了,感兴趣的开发者,可以单独跟单独探讨。
到此这篇关于如何在Android上实现RTSP服务器的文章就介绍到这了,更多相关Android RTSP服务器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!