技術背景
SIP(會話初始化協定)是在 IP網絡上進行多媒體通信的應用層控制協定,以幾種RFC的形式提供,其中最重要的是包含核心協定規範的RFC3261。該協定用于建立,修改和終止與一個或多個參與者的會話。通過會話,我們了解了一組進行通信的發送方和接收方,以及在通信過程中這些發送方和接收方保持的狀态。會話的示例可以包括Internet電話呼叫,多媒體分發,多媒體會議,分布式計算機遊戲等。
SIP不是通信裝置将需要的唯一協定,也不意味着是通用協定。SIP的目的僅僅是使通信成為可能,通信本身必須通過其他方式(可能還有其他協定)來實作。
與SIP一起最常使用的兩種協定是RTP和SDP。 RTP協定用于承載實時多媒體資料(包括音頻,視訊和文本),該協定可以将資料編碼和拆分為資料包,并通過Internet傳輸此類資料包。
另一個重要的協定是SDP,用于描述和編碼會話參與者的功能。 然後,将這種描述用于協商會話的特征,以便所有裝置都可以參與(例如,包括協商用于編碼媒體的編解碼器,以便所有參與者都可以對其進行解碼,協商使用的傳輸協定 等等)。
GB/T28181-2011 《安全防範視訊監控聯網系統資訊傳輸、交換、控制技術要求》是由公安部科技資訊化局提出,由全國安全防範報警系統标準化技術委員會(SAC/TC100)歸口,公安部一所等多家機關共同起草的一部國家标準。
該标準規定了城市監控報警聯網系統中資訊傳輸、交換、控制的互聯結構、通信協定結構,傳輸、交換、控制的基本要求和安全性要求,以及控制、傳輸流程和協定接口等技術要求。該标準适用于安全防範監控報警聯網系統的方案設計、系統檢測、驗收以及與之相關的裝置研發、生産,其他資訊系統可參考采用。
該标準于2012年6月1日正式釋出實施,在全國範圍内的平安城市項目建設中被普遍推廣應用。GB/T28181-2011标準自釋出以來,受到了各大視訊監控廠商的積極響應。截止2012年底,有近百家視訊監控企業通過公安部一所、公安部三所的認證,如深圳宙視達、浙江宇視、超視科技、東方網力、海康威視、高遠時代、浙江大華、先進視訊、波粒科技、華為技術、中興力維、中星電子、科達、天地偉業等。
相關接口
廢話不多說,直接上設計接口,好多開發者網上看到的大多是非常簡單的接口。極緻簡單,一直是我們追求的目标,但是更好的參數化配置和可擴充的設計,也是一個規範化産品的必經之路。
除了正常的音視訊采集編碼接口外,GB28181裝置接入子產品,主要分信令互動和媒體資料傳輸兩個部分,以大牛直播SDK(官方)分别介紹下相關接口設計。
媒體資料傳輸相關
/*+++++++++++++++RTP Sender相關接口+++++++++++++++*/
/*
* 建立RTP Sender執行個體
*
* @param reserve:保留參數傳0
*
* @return RTP Sender 句柄,0表示失敗
*/
public native long CreateRTPSender(int reserve);
/**
*設定 RTP Sender傳輸協定
*
* @param rtp_sender_handle, CreateRTPSender傳回值
* @param transport_protocol, 0:UDP, 1:TCP, 預設是UDP
*
* @return {0} if successful
*/
public native int SetRTPSenderTransportProtocol(long rtp_sender_handle, int transport_protocol);
/**
*設定 RTP Sender IP位址類型
*
* @param rtp_sender_handle, CreateRTPSender傳回值
* @param ip_address_type, 0:IPV4, 1:IPV6, 預設是IPV4, 目前僅支援IPV4
*
* @return {0} if successful
*/
public native int SetRTPSenderIPAddressType(long rtp_sender_handle, int ip_address_type);
/**
*設定 RTP Sender RTP Socket本地端口
*
* @param rtp_sender_handle, CreateRTPSender傳回值
* @param port, 必須是偶數,設定0的話SDK會自動配置設定, 預設值是0
*
* @return {0} if successful
*/
public native int SetRTPSenderLocalPort(long rtp_sender_handle, int port);
/**
*設定 RTP Sender SS-RC
*
* @param rtp_sender_handle, CreateRTPSender傳回值
* @param ssrc, 如果設定的話,這個字元串要能轉換成uint32類型, 否則設定失敗
*
* @return {0} if successful
*/
public native int SetRTPSenderSS-RC(long rtp_sender_handle, String ssrc);
/**
*設定 RTP Sender RTP socket 發送Buffer大小
*
* @param rtp_sender_handle, CreateRTPSender傳回值
* @param buffer_size, 必須大于0, 預設是512*1024, 目前僅對UDP socket有效, 根據視訊碼率考慮設定合适的值
*
* @return {0} if successful
*/
public native int SetRTPSenderSocketSendBuffer(long rtp_sender_handle, int buffer_size);
/**
*設定 RTP Sender RTP時間戳時鐘頻率
*
* @param rtp_sender_handle, CreateRTPSender傳回值
* @param clock_rate, 必須大于0, 對于GB28181 PS規定是90kHz, 也就是90000
*
* @return {0} if successful
*/
public native int SetRTPSenderClockRate(long rtp_sender_handle, int clock_rate);
/**
*設定 RTP Sender 目的IP位址, 注意目前用在GB2818推送上,隻設定一個位址,将來擴充如果用在其他地方,可能要設定多個目的位址,到時候接口可能會調整
*
* @param rtp_sender_handle, CreateRTPSender傳回值
* @param address, IP位址
* @param port, 端口
*
* @return {0} if successful
*/
public native int SetRTPSenderDestination(long rtp_sender_handle, String address, int port);
/**
*初始化RTP Sender, 初始化之前先調用上面的接口配置相關參數
*
* @param rtp_sender_handle, CreateRTPSender傳回值
*
* @return {0} if successful
*/
public native int InitRTPSender(long rtp_sender_handle);
/**
*擷取RTP Sender RTP Socket本地端口
*
* @param rtp_sender_handle, CreateRTPSender傳回值
*
* @return 失敗傳回0, 成功的話傳回響應的端口, 請在InitRTPSender傳回成功之後調用
*/
public native int GetRTPSenderLocalPort(long rtp_sender_handle);
/**
* UnInit RTP Sender
*
* @param rtp_sender_handle, CreateRTPSender傳回值
*
* @return {0} if successful
*/
public native int UnInitRTPSender(long rtp_sender_handle);
/**
* 釋放RTP Sender, 釋放之後rtp_sender_handle就無效了,請不要再使用
*
* @param rtp_sender_handle, CreateRTPSender傳回值
*
* @return {0} if successful
*/
public native int DestoryRTPSender(long rtp_sender_handle);
/*+++++++++++++++RTP Sender相關接口+++++++++++++++*/
複制
信令相關操作
/*+++++++++++++++GB28181相關接口+++++++++++++++*/
/**
* 設定GB28181 RTP Sender
*
* @param rtp_sender_handle, CreateRTPSender傳回值
* @param rtp_payload_type, 對于GB28181 PS, 協定定義是96, 具體以SDP為準
*
* @return {0} if successful
*/
public native int SetGB28181RTPSender(long handle, long rtp_sender_handle, int rtp_payload_type);
/**
* 啟動 GB28181 媒體流
*
* @return {0} if successful
*/
public native int StartGB28181MediaStream(long handle);
/**
* 停止 GB28181 媒體流
*
* @return {0} if successful
*/
public native int StopGB28181MediaStream(long handle);
/*---------------GB28181相關接口---------------*/
複制
啟動、停止GB28181
完成視訊分辨率等參數配置後,點選“啟動GB28181”,即可開始走信令互動流程,裝置端主動發送Register,進入後續互動流程。
如需停止GB28181,點停止即可。
GB28181裝置端主要實作了按需推送資料到平台端。
class ButtonGB28181AgentListener implements OnClickListener {
public void onClick(View v) {
stopGB28181Stream();
destoryRTPSender();
if (null == gb28181_agent_ ) {
if( !initGB28181Agent() )
return;
}
if (gb28181_agent_.isRunning()) {
gb28181_agent_.terminateAllPlays(true);// 目前測試下來,發送BYE之後,有些伺服器會立即發送INVITE,是否發送BYE根據實際情況看
gb28181_agent_.stop();
btnGB28181Agent.setText("啟動GB28181");
}
else {
if ( gb28181_agent_.start() ) {
btnGB28181Agent.setText("停止GB28181");
}
}
}
}
複制
注冊
當Andriod裝置端第一次接入平台端時,裝置端将持續向平台端發送 REGISTER消息,直到 Server端回複"200 OK"代表注冊成功。
如果裝置或系統注冊不成功,宜延遲一定的随機時間後重新注冊。 每隔一定時間用戶端都會再次向伺服器重新整理注冊,防止注冊過期導緻連接配接斷開。
以基本注冊為例:基本注冊采用IETFRFC3261規定的基于數字摘要的挑戰應答式安全技術進行注冊。
注冊流程描述如下:
a) 1:SIP代理向SIP伺服器發送 Register請求;
b) 2:SIP伺服器向 SIP代理發送響應401,并在響應的消息頭 WWW_Authenticate字段中給出适合SIP代理的認證體制和參數;
c) 3:SIP代理重新向SIP伺服器發送 Register請求,在請求的 Authorization字段給出信任書, 包含認證資訊;
d) 4:SIP 伺服器對請求進行驗證,如果檢查出 SIP 代理身份合法,向 SIP 代理發送成功響應 200OK,如果身份不合法則發送拒絕服務應答。
相關注冊回報:
@Override
public void ntsRegisterOK(String dateString) {
Log.i(TAG, "ntsRegisterOK Date: " + (dateString!= null? dateString : ""));
}
@Override
public void ntsRegisterTimeout() {
Log.e(TAG, "ntsRegisterTimeout");
}
@Override
public void ntsRegisterTransportError(String errorInfo) {
Log.e(TAG, "ntsRegisterTransportError error:" + (errorInfo != null?errorInfo :""));
}
複制
資訊查詢(Catalog)
注冊成功後,用戶端與伺服器之間資訊查詢操作,如目錄查詢、曆史錄像檔案檢視等,GB28181使用 SIP擴充協定規定的 Message方法實作。目前使用了兩種類型的查詢指令:Catalog 裝置目錄查詢消息、RecordInfo 曆史錄像檔案查詢消息。
本文以Catalog裝置目錄查詢消息為例:平台端向裝置端發送Catalog請求,裝置端回複200 OK後,發送裝置資訊,平台端回複200 OK,如遇多個裝置資訊,切記分次發送。
private boolean initGB28181Agent()
{
if ( gb28181_agent_ != null )
return true;
String local_ip_addr = IPAddrUtils.getIpAddress(myContext);
Log.i(TAG, "initGB28181Agent local ip addr: " + local_ip_addr);
if ( local_ip_addr == null || local_ip_addr.isEmpty() ) {
Log.e(TAG, "initGB28181Agent local ip is empty");
return false;
}
gb28181_agent_ = GBSIPAgentFactory.getInstance().create();
if ( gb28181_agent_ == null ) {
Log.e(TAG, "initGB28181Agent create agent failed");
return false;
}
gb28181_agent_.addListener(this);
// 必填資訊
gb28181_agent_.setLocalAddressInfo(local_ip_addr, gb28181_sip_local_port_);
gb28181_agent_.setServerParameter(gb28181_sip_server_addr_, gb28181_sip_server_port_, gb28181_sip_server_id_, gb28181_sip_server_domain_);
gb28181_agent_.setUserInfo(gb28181_sip_username_, gb28181_sip_password_);
// 可選參數
gb28181_agent_.setUserAgent(gb28181_sip_user_agent_filed_);
gb28181_agent_.setTransportProtocol(gb28181_sip_trans_protocol_==0?"UDP":"TCP");
// GB28181配置
gb28181_agent_.config(gb28181_reg_expired_, gb28181_heartbeat_interval_, gb28181_heartbeat_count_);
com.gb28181.ntsignalling.Device gb_device = new com.gb28181.ntsignalling.Device("34020000001380000001", "安卓測試裝置", Build.MANUFACTURER, Build.MODEL,
"宇宙","火星1","火星", true);
getLocation(this);
gb_device.setLongitude(mLongitude);
gb_device.setLatitude(mLatitude);
gb28181_agent_.addDevice(gb_device);
if (!gb28181_agent_.initialize()) {
gb28181_agent_ = null;
Log.e(TAG, "initGB28181Agent gb28181_agent_.initialize failed.");
return false;
}
return true;
}
複制
心跳(KeepAlive)
KeepAlive以MESSAGE的形式傳遞,異常處理如下:
@Override
public void ntsOnHeartBeatException(int exceptionCount, String lastExceptionInfo) {
Log.e(TAG, "ntsOnHeartBeatException heart beat timeout count reached, count:" + exceptionCount+
", exception info:" + (lastExceptionInfo!=null?lastExceptionInfo:""));
// 10毫秒後,停止信令, 然後重新開機
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "gb28281_heart_beart_timeout");
stopGB28181Stream();
destoryRTPSender();
if (gb28181_agent_ != null) {
Log.i(TAG, "gb28281_heart_beart_timeout sip stop");
gb28181_agent_.stop();
Log.i(TAG, "gb28281_heart_beart_timeout sip start");
gb28181_agent_.start();
}
}
},10);
}
複制
Invite處理
假定整個信令互動流程順利,Android裝置端完成Register、Catalog、KeepAlive消息處理後,平台端發過來Invite請求并攜帶SDP消息體。
Android裝置端可擷取到比如deviceid, tcp/udp傳輸模式、rtp端口,address類型等,并建構200 OK,攜帶相關的音視訊資訊。
@Override
public void ntsOnInvitePlay(String deviceId, InvitePlaySessionDescription session_des) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG,"ntsInviteReceived, device_id:" +device_id_+", is_tcp:" + session_des_.isRTPOverTCP()
+ " rtp_port:" + session_des_.getMediaPort() + " ss-rc:" + session_des_.getSS-RC()
+ " address_type:" + session_des_.getAddressType() + " address:" + session_des_.getAddress());
// 可以先給信令伺服器發送臨時振鈴響應
//sip_stack_android.respondPlayInvite(180, device_id_);
long rtp_sender_handle = libPublisher.CreateRTPSender(0);
if ( rtp_sender_handle == 0 ) {
gb28181_agent_.respondPlayInvite(488, device_id_);
Log.i(TAG, "ntsInviteReceived CreateRTPSender failed, response 488, device_id:" + device_id_);
return;
}
gb28181_rtp_payload_type_ = session_des_.getPSRtpMapAttribute().getPayloadType();
libPublisher.SetRTPSenderTransportProtocol(rtp_sender_handle, session_des_.isRTPOverUDP()?0:1);
libPublisher.SetRTPSenderIPAddressType(rtp_sender_handle, session_des_.isIPv4()?0:1);
libPublisher.SetRTPSenderLocalPort(rtp_sender_handle, 0);
libPublisher.SetRTPSenderSS-RC(rtp_sender_handle, session_des_.getSS-RC());
libPublisher.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 2*1024*1024); // 設定到2M
libPublisher.SetRTPSenderClockRate(rtp_sender_handle, session_des_.getPSRtpMapAttribute().getClockRate());
libPublisher.SetRTPSenderDestination(rtp_sender_handle, session_des_.getAddress(), session_des_.getMediaPort());
if ( libPublisher.InitRTPSender(rtp_sender_handle) != 0 ) {
gb28181_agent_.respondPlayInvite(488, device_id_);
libPublisher.DestoryRTPSender(rtp_sender_handle);
return;
}
int local_port = libPublisher.GetRTPSenderLocalPort(rtp_sender_handle);
if (local_port == 0) {
gb28181_agent_.respondPlayInvite(488, device_id_);
libPublisher.DestoryRTPSender(rtp_sender_handle);
return;
}
Log.i(TAG,"get local_port:" + local_port);
String local_ip_addr = IPAddrUtils.getIpAddress(myContext);
gb28181_agent_.respondPlayInviteOK(device_id_,local_ip_addr, local_port);
gb28181_rtp_sender_handle_ = rtp_sender_handle;
}
private String device_id_;
private InvitePlaySessionDescription session_des_;
public Runnable set(String device_id, InvitePlaySessionDescription session_des) {
this.device_id_ = device_id;
this.session_des_ = session_des;
return this;
}
}.set(deviceId, session_des),0);
}
複制
Ack确認
平台端發送Ack确認,裝置端收到Ack後,開始發送音視訊資料。
@Override
public void ntsOnAckPlay(String deviceId) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG,"ntsOnACKPlay, device_id:" +device_id_);
if (!isRecording && !isRTSPPublisherRunning && !isPushingRtsp && !isPushingRtmp) {
InitAndSetConfig();
}
libPublisher.SetGB28181RTPSender(publisherHandle, gb28181_rtp_sender_handle_, gb28181_rtp_payload_type_);
int startRet = libPublisher.StartGB28181MediaStream(publisherHandle);
if (startRet != 0) {
if (!isRecording && !isRTSPPublisherRunning && !isPushingRtmp && !isPushingRtsp) {
if (publisherHandle != 0) {
libPublisher.SmartPublisherClose(publisherHandle);
publisherHandle = 0;
}
}
destoryRTPSender();
Log.e(TAG, "Failed to start GB28181 service..");
return;
}
if (!isRecording && !isRTSPPublisherRunning && !isPushingRtsp && !isPushingRtmp) {
if (pushType == 0 || pushType == 1) {
CheckInitAudioRecorder(); //enable pure video publisher..
}
}
isGB28181StreamRunning = true;
}
private String device_id_;
public Runnable set(String device_id) {
this.device_id_ = device_id;
return this;
}
}.set(deviceId),0);
}
複制
BYE
如果用戶端需要斷開Invite會話,則會發送BYE,用戶端傳回200 OK。
@Override
public void ntsOnByePlay(String deviceId)
{
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "ntsOnByePlay, stop GB28181 media stream, deviceId=" + device_id_);
stopGB28181Stream();
destoryRTPSender();
}
private String device_id_;
public Runnable set(String device_id) {
this.device_id_ = device_id;
return this;
}
}.set(deviceId),0);
}
複制
登出
用戶端向伺服器發送 Register指令消息,消息中的 Expire字段設定為0即是登出。