Android上實(shí)現(xiàn)RTSP服務(wù)器的方法
技術(shù)背景
在Android上實(shí)現(xiàn)RTSP服務(wù)器確實(shí)是一個(gè)不太常見的需求,因?yàn)锳ndroid平臺(tái)主要是為客戶端應(yīng)用設(shè)計(jì)的。在一些內(nèi)網(wǎng)場景下,我們更希望把安卓終端或開發(fā)板,作為一個(gè)IPC(網(wǎng)絡(luò)攝像機(jī))一樣,對外提供個(gè)拉流的rtsp url,然后把攝像頭麥克風(fēng)甚至屏幕采集的數(shù)據(jù),共享出去,輕量級RTSP的設(shè)計(jì)理念脫穎而出。
輕量級RTSP服務(wù)設(shè)計(jì)初衷,就是避免用戶單獨(dú)部署RTSP或者RTMP服務(wù),實(shí)現(xiàn)本地的音視頻數(shù)據(jù)(如攝像頭、麥克風(fēng)),編碼后,匯聚到內(nèi)置RTSP服務(wù),對外提供可供拉流的RTSP URL,輕量級RTSP服務(wù),適用于內(nèi)網(wǎng)環(huán)境下,對并發(fā)要求不高的場景,支持H.264/H.265,支持RTSP鑒權(quán)、單播、組播模式,考慮到單個(gè)服務(wù)承載能力,我們支持同時(shí)創(chuàng)建多個(gè)RTSP服務(wù),并支持獲取當(dāng)前RTSP服務(wù)會(huì)話連接數(shù)。

如何實(shí)現(xiàn)RTSP服務(wù)器
如果你找不到合適的庫,或者需要更高級的功能,你可以考慮編寫自己的RTSP服務(wù)器:
- 了解RTSP協(xié)議:首先,你需要深入了解RTSP協(xié)議的工作原理,包括其消息格式、會(huì)話管理和流控制機(jī)制。
- 網(wǎng)絡(luò)編程:在Android上,你可以使用Java的Socket API來處理網(wǎng)絡(luò)通信,也可以直接底層實(shí)現(xiàn),然后對上提供jni的接口。你需要編寫代碼來監(jiān)聽端口、接收RTSP請求、解析請求并發(fā)送響應(yīng)。
- 媒體處理:RTSP服務(wù)器需要能夠捕獲、編碼和傳輸媒體數(shù)據(jù)。你可以使用Android的Camera或Camera2的API來捕獲視頻,并使用FFmpeg或Android的MediaCodec API來實(shí)現(xiàn)音視頻數(shù)據(jù)編碼。
- 多線程和并發(fā):RTSP服務(wù)器需要處理多個(gè)并發(fā)客戶端連接。你可以使用Java的線程或并發(fā)API來管理這些連接。
功能設(shè)計(jì)
一個(gè)完整的RTSP服務(wù),需要設(shè)計(jì)的功能如下:
- ?[視頻格式]H.264/H.265(Android H.265硬編碼);
- [音頻格式]G.711 A律、AAC;
- 協(xié)議:RTSP;
- [音量調(diào)節(jié)]Android平臺(tái)采集端支持實(shí)時(shí)音量調(diào)節(jié);
- [H.264硬編碼]支持H.264特定機(jī)型硬編碼;
- [H.265硬編碼]支持H.265特定機(jī)型硬編碼;
- [音視頻]支持純音頻/純視頻/音視頻;
- [攝像頭]支持采集過程中,前后攝像頭實(shí)時(shí)切換;
- 支持幀率、關(guān)鍵幀間隔(GOP)、碼率(bit-rate)設(shè)置;
- [實(shí)時(shí)水印]支持動(dòng)態(tài)文字水印、png水??;
- [實(shí)時(shí)快照]支持實(shí)時(shí)快照;
- [降噪]支持環(huán)境音、手機(jī)干擾等引起的噪音降噪處理、自動(dòng)增益、VAD檢測;
- [外部編碼前視頻數(shù)據(jù)對接]支持YUV數(shù)據(jù)對接;
- [外部編碼前音頻數(shù)據(jù)對接]支持PCM對接;
- [外部編碼后視頻數(shù)據(jù)對接]支持外部H.264、H.265數(shù)據(jù)對接;
- [外部編碼后音頻數(shù)據(jù)對接]外部AAC數(shù)據(jù)對接;
- [擴(kuò)展錄像功能]支持和錄像SDK組合使用,錄像相關(guān)功能。?
- 支持RTSP端口設(shè)置;
- 支持RTSP鑒權(quán)用戶名、密碼設(shè)置;
- 支持獲取當(dāng)前RTSP服務(wù)會(huì)話連接數(shù);
- 支持Android 5.1及以上版本。
接口設(shè)計(jì)
Android內(nèi)置輕量級RTSP服務(wù)SDK接口詳解 | ||
調(diào)用描述 | 接口 | 接口描述 |
SmartRTSPServerSDK | ||
初始化RTSP Server | InitRtspServer | Init rtsp server(和UnInitRtspServer配對使用,即便是啟動(dòng)多個(gè)RTSP服務(wù),也只需調(diào)用一次InitRtspServer,請確保在OpenRtspServer之前調(diào)用) |
創(chuàng)建一個(gè)rtsp server | OpenRtspServer | 創(chuàng)建一個(gè)rtsp server,返回rtsp server句柄 |
設(shè)置端口 | SetRtspServerPort | 設(shè)置rtsp server 監(jiān)聽端口, 在StartRtspServer之前必須要設(shè)置端口 |
設(shè)置鑒權(quán)用戶名、密碼 | SetRtspServerUserNamePassword | 設(shè)置rtsp server 鑒權(quán)用戶名和密碼, 這個(gè)可以不設(shè)置,只有需要鑒權(quán)的再設(shè)置 |
獲取rtsp server當(dāng)前會(huì)話數(shù) | GetRtspServerClientSessionNumbers | 獲取rtsp server當(dāng)前的客戶會(huì)話數(shù), 這個(gè)接口必須在StartRtspServer之后再調(diào)用 |
啟動(dòng)rtsp server | StartRtspServer | 啟動(dòng)rtsp server |
停止rtsp server | StopRtspServer | 停止rtsp server |
關(guān)閉rtsp server | CloseRtspServer | 關(guān)閉rtsp server |
UnInit rtsp server | UnInitRtspServer | UnInit rtsp server(和InitRtspServer配對使用,即便是啟動(dòng)多個(gè)RTSP服務(wù),也只需調(diào)用一次UnInitRtspServer) |
SmartRTSPServerSDK供Publisher調(diào)用的接口 | ||
設(shè)置rtsp的流名稱 | SetRtspStreamName | 設(shè)置rtsp的流名稱 |
給要發(fā)布的rtsp流設(shè)置rtsp server | AddRtspStreamServer | 給要發(fā)布的rtsp流設(shè)置rtsp server, 一個(gè)流可以發(fā)布到多個(gè)rtsp server上,rtsp server的創(chuàng)建啟動(dòng)請參考OpenRtspServer和StartRtspServer接口 |
清除設(shè)置的rtsp server | ClearRtspStreamServer | 清除設(shè)置的rtsp server |
啟動(dòng)rtsp流 | StartRtspStream | 啟動(dòng)rtsp流 |
停止rtsp流 | StopRtspStream | 停止rtsp流 |
調(diào)用邏輯
以大牛直播SDK的Android平臺(tái)Camera2對接為例,先初始化RTSP Server,啟動(dòng)RTSP服務(wù),然后發(fā)布RTSP流即可,如果需要停止RTSP服務(wù),那么先停止RTSP流,再停止RTSP服務(wù)即可:

/*
* 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配對使用,即便是啟動(dòng)多個(gè)RTSP服務(wù),也只需調(diào)用一次InitRtspServer,請確保在OpenRtspServer之前調(diào)用
}啟動(dòng)、停止RTSP服務(wù):
//啟動(dòng)/停止RTSP服務(wù)
class ButtonRtspServiceListener implements View.OnClickListener {
public void onClick(View v) {
if (isRTSPServiceRunning) {
stopRtspService();
btnRtspService.setText("啟動(dòng)RTSP服務(wù)");
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, "創(chuàng)建rtsp server實(shí)例失敗! 請檢查SDK有效性");
} else {
int port = 8554;
if (libPublisher.SetRtspServerPort(rtsp_handle_, port) != 0) {
libPublisher.CloseRtspServer(rtsp_handle_);
rtsp_handle_ = 0;
Log.e(TAG, "創(chuàng)建rtsp server端口失敗! 請檢查端口是否重復(fù)或者端口不在范圍內(nèi)!");
}
if (libPublisher.StartRtspServer(rtsp_handle_, 0) == 0) {
Log.i(TAG, "啟動(dòng)rtsp server 成功!");
} else {
libPublisher.CloseRtspServer(rtsp_handle_);
rtsp_handle_ = 0;
Log.e(TAG, "啟動(dòng)rtsp server失敗! 請檢查設(shè)置的端口是否被占用!");
}
btnRtspService.setText("停止RTSP服務(wù)");
btnRtspPublisher.setEnabled(true);
isRTSPServiceRunning = true;
}
}
}stopRtspService()實(shí)現(xiàn)如下:
//停止RTSP服務(wù)
private void stopRtspService() {
if(!isRTSPServiceRunning)
{
return;
}
if (libPublisher != null && rtsp_handle_ != 0) {
libPublisher.StopRtspServer(rtsp_handle_);
libPublisher.CloseRtspServer(rtsp_handle_);
rtsp_handle_ = 0;
}
}發(fā)布、停止RTSP流:
//發(fā)布/停止RTSP流
class ButtonRtspPublisherListener implements View.OnClickListener {
public void onClick(View v) {
if (stream_publisher_.is_rtsp_publishing()) {
stopRtspPublisher();
btnRtspPublisher.setText("發(fā)布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, "調(diào)用發(fā)布rtsp流接口失敗!");
return;
}
startAudioRecorder();
startLayerPostThread();
btnRtspPublisher.setText("停止RTSP流");
btnGetRtspSessionNumbers.setEnabled(true);
btnRtspService.setEnabled(false);
}
}stopRtspPublisher()實(shí)現(xiàn)如下:
//停止發(fā)布RTSP流
private void stopRtspPublisher() {
stream_publisher_.StopRtspStream();
stream_publisher_.try_release();
if (!stream_publisher_.is_publishing())
stopAudioRecorder();
}其中,InitAndSetConfig()實(shí)現(xiàn)如下,通過調(diào)研SmartPublisherOpen()接口,生成推送實(shí)例句柄。
/*
* 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);
}對應(yīng)的initialize_publisher()實(shí)現(xiàn)如下,設(shè)置軟硬編碼、幀率、關(guān)鍵幀間隔等。
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 多數(shù)情況下,這個(gè)夠用了
//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;
}發(fā)布RTSP流成功后,會(huì)回調(diào)上來可供拉流的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服務(wù)URL: " + param3;
break;
}
}
}獲取RTSP Session會(huì)話數(shù):
//獲取RTSP會(huì)話數(shù)
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);
}
}
}
//當(dāng)前RTSP會(huì)話數(shù)彈出框
private void PopRtspSessionNumberDialog(int session_numbers) {
final EditText inputUrlTxt = new EditText(this);
inputUrlTxt.setFocusable(true);
inputUrlTxt.setEnabled(false);
String session_numbers_tag = "RTSP服務(wù)當(dāng)前客戶會(huì)話數(shù): " + session_numbers;
inputUrlTxt.setText(session_numbers_tag);
AlertDialog.Builder builderUrl = new AlertDialog.Builder(this);
builderUrl
.setTitle("內(nèi)置RTSP服務(wù)")
.setView(inputUrlTxt).setNegativeButton("確定", null);
builderUrl.show();
}onDestroy() 的時(shí)候,調(diào)研UnInitRtspServer()即可:
@Override
protected void onDestroy() {
Log.i(TAG, "activity destory!");
stopAudioRecorder();
stopRtspPublisher();
stopRtspService();
isRTSPServiceRunning = false;
stream_publisher_.release();
if (libPublisher != null)
libPublisher.UnInitRtspServer(); //如已啟用內(nèi)置服務(wù)功能(InitRtspServer),調(diào)用UnInitRtspServer, 注意,即便是啟動(dòng)多個(gè)RTSP服務(wù),也只需調(diào)用UnInitRtspServer一次
stopLayerPostThread();
if (camera2Helper != null) {
camera2Helper.release();
}
super.onDestroy();
}總結(jié)
Android上實(shí)現(xiàn)RTSP服務(wù)器是一個(gè)極具挑戰(zhàn)的任務(wù),功能設(shè)計(jì)這塊,除了需要支持接編碼前音視頻數(shù)據(jù)外,還需要支持對接編碼后音視頻數(shù)據(jù),并實(shí)現(xiàn)本地錄像、快照等功能組合使用。需要注意的是,就像海康、大華的攝像頭一樣,對外的并發(fā),一般限于4-8個(gè),Android設(shè)備的性能一般來說,可能不足以處理高負(fù)載的RTSP服務(wù)器,但是小并發(fā)模式下,能穩(wěn)定的運(yùn)行,就達(dá)到設(shè)計(jì)初衷了,感興趣的開發(fā)者,可以單獨(dú)跟單獨(dú)探討。
到此這篇關(guān)于如何在Android上實(shí)現(xiàn)RTSP服務(wù)器的文章就介紹到這了,更多相關(guān)Android RTSP服務(wù)器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Flutter Navigator路由傳參的實(shí)現(xiàn)
本文主要介紹了Flutter Navigator路由傳參的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
Android Studio實(shí)現(xiàn)下拉列表效果
這篇文章主要為大家詳細(xì)介紹了Android Studio實(shí)現(xiàn)下拉列表效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
Android進(jìn)階KOOM線上APM監(jiān)控全面剖析
這篇文章主要為大家介紹了Android進(jìn)階KOOM線上APM監(jiān)控全面剖析詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
詳解Android Webview加載網(wǎng)頁時(shí)發(fā)送HTTP頭信息
這篇文章主要介紹了詳解Android Webview加載網(wǎng)頁時(shí)發(fā)送HTTP頭信息的相關(guān)資料,需要的朋友可以參考下2017-05-05
Android編程實(shí)現(xiàn)3D旋轉(zhuǎn)效果實(shí)例
這篇文章主要介紹了Android編程實(shí)現(xiàn)3D旋轉(zhuǎn)效果的方法,基于Android的Camera類實(shí)現(xiàn)坐標(biāo)變換達(dá)到圖片3D旋轉(zhuǎn)效果,需要的朋友可以參考下2016-01-01
Android Webview添加網(wǎng)頁加載進(jìn)度條實(shí)例詳解
這篇文章主要介紹了Android Webview添加網(wǎng)頁加載進(jìn)度條實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2016-01-01
淺談Android獲取ImageView上的圖片,和一個(gè)有可能遇到的問題
下面小編就為大家?guī)硪黄獪\談Android獲取ImageView上的圖片,和一個(gè)有可能遇到的問題。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-04-04

