技术背景
GB/T28181-2016官方规范和交互流程,我们不再赘述。
SIP服务器发起广播流程示意图如下:
需要注意的是:语音广播通知、语音广播应答命令
消息头 Content-type字段为 Content-type:Application/MANSCDP+xml。
语音广播通知、语音广播应答命令采用 MANSCDP协议格式定义。
消息示例如下:
a) 语音广播通知
MESSAGE sip:34020000001310000056@192.168.100.9:6720 SIP/2.0\ Via: SIP/2.0/UDP 192.168.100.10:5060;rport=5060;branch=z9hG4bK1073741856;received=192.168.100.10\ From: <sip:34020000002000000001@192.168.100.10:5060>;tag=912513446\ To: <sip:34020000001310000056@192.168.100.9:6720>\ Call-ID: 536870958\ CSeq: 1 MESSAGE\ Contact: <sip:34020000002000000001@192.168.100.10:5060>\ Content-Type: Application/MANSCDP+xml\ Max-Forwards: 70\ User-Agent: Hikvision\ Content-Length: 172\ \ <?xml version="1.0"?>\ <Notify>\ <CmdType>Broadcast</CmdType>\ <SN>11</SN>\ <SourceID>34020000002000000001</SourceID>\ <TargetID>34020000001310000056</TargetID>\ </Notify>\
b) 语音广播应答
MESSAGE sip:34020000002000000001@192.168.100.10:5060 SIP/2.0\ Call-ID: 6d6bc2a5d380a0f8d6787cf614fb0bd8@192.168.100.9\ CSeq: 18300138 MESSAGE\ From: <sip:34020000001310000056@3402000000>;tag=4a5b3953\ To: <sip:34020000002000000001@192.168.100.10:5060>\ Via: SIP/2.0/UDP 192.168.100.9:6720;rport;branch=z9hG4bK-373435-549b6376963815eb98e2a2f011473b41\ Max-Forwards: 70\ User-Agent: NT GB UserAgent V1.91-20230420[daniusdk.com]\ Content-Type: Application/MANSCDP+xml\ Content-Length: 173\ \ <?xml version="1.0" encoding="GB2312"?>\ <Response>\ <CmdType>Broadcast</CmdType>\ <SN>11</SN>\ <DeviceID>34020000001310000056</DeviceID>\ <Result>OK</Result>\ </Response>\
c) 平台侧回复200 OK:
SIP/2.0 200 OK\ Via: SIP/2.0/UDP 192.168.100.9:6720;rport;branch=z9hG4bK-373435-549b6376963815eb98e2a2f011473b41\ From: <sip:34020000001310000056@3402000000>;tag=4a5b3953\ To: <sip:34020000002000000001@192.168.100.10:5060>\ Call-ID: 6d6bc2a5d380a0f8d6787cf614fb0bd8@192.168.100.9\ CSeq: 18300138 MESSAGE\ User-Agent: Hikvision\ Content-Length: 0\
d) 设备接入侧发起invite请求:
INVITE sip:34020000002000000001@3402000000 SIP/2.0\ ... User-Agent: NT GB UserAgent V1.91-20230420[daniusdk.com]\ Content-Type: APPLICATION/SDP\ Content-Length: 245\ \ v=0\ o=34020000001310000056 3898650599696 3898650599696 IN IP4 192.168.100.9\ s=Play\ c=IN IP4 192.168.100.9\ t=0 0\ m=audio 25000 TCP/RTP/AVP 8\ a=setup:active\ a=connection:new\ a=recvonly\ a=rtpmap:8 PCMA/8000\ y=0200009722\ f=v/a/1/8/1\
e) 国标平台侧回复200 OK:
SIP/2.0 200 OK\ ... Content-Type: application/sdp\ User-Agent: Hikvision\ Content-Length: 205\ \ v=0\ o=34020000002000000001 0 0 IN IP4 192.168.100.10\ s=Play\ c=IN IP4 192.168.100.10\ t=0 0\ m=audio 16002 TCP/RTP/AVP 8\ a=rtpmap:8 PCMA/8000\ a=sendonly\ a=setup:passive\ y=0200009727\ f=v/a/1/8/1\
f) 设备接入侧发Ack:
ACK sip:34020000002000000001@192.168.100.10:5060 SIP/2.0\ ... Max-Forwards: 70\ Contact: <sip:34020000001310000056@192.168.100.9:6720>\ User-Agent: NT GB UserAgent V1.91-20230420[daniusdk.com]\ Content-Length: 0\
技术实现
以大牛直播SDK的Android平台GB28181设备接入侧为例:
收到语音广播:
@Override public void ntsOnNotifyBroadcastCommand(String fromUserName, String fromUserNameAtDomain, String sn, String sourceID, String targetID) { handler_.postDelayed(new Runnable() { @Override public void run() { Log.i(TAG, "daniusdk, ntsOnNotifyBroadcastCommand, fromUserName:"+ from_user_name_ + ", fromUserNameAtDomain:"+ from_user_name_at_domain_ + ", SN:" + sn_ + ", sourceID:" + source_id_ + ", targetID:" + target_id_); if (gb28181_agent_ != null ) { gb28181_agent_.respondBroadcastCommand(from_user_name_, from_user_name_at_domain_,sn_,source_id_, target_id_, true); btnGB28181AudioBroadcast.setText("收到GB28181语音广播通知"); } } private String from_user_name_; private String from_user_name_at_domain_; private String sn_; private String source_id_; private String target_id_; public Runnable set(String from_user_name, String from_user_name_at_domain, String sn, String source_id, String target_id) { this.from_user_name_ = from_user_name; this.from_user_name_at_domain_ = from_user_name_at_domain; this.sn_ = sn; this.source_id_ = source_id; this.target_id_ = target_id; return this; } }.set(fromUserName, fromUserNameAtDomain, sn, sourceID, targetID),0); }
ntsOnAudioBroadcast处理:
@Override public void ntsOnAudioBroadcast(String commandFromUserName, String commandFromUserNameAtDomain, String sourceID, String targetID) { handler_.postDelayed(new Runnable() { @Override public void run() { Log.i(TAG, "ntsOnAudioBroadcastPlay, fromFromUserName:" + command_from_user_name_ + " FromUserNameAtDomain:" + command_from_user_name_at_domain_ + " sourceID:" + source_id_ + ", targetID:" + target_id_); stopAudioPlayer(); destoryRTPReceiver(); if (gb28181_agent_ != null ) { String local_ip_addr = IPAddrUtils.getIpAddress(context_); boolean is_tcp = true; // 考虑到跨网段, 默认用TCP传输rtp包 rtp_receiver_handle_ = lib_player_.CreateRTPReceiver(0); if (rtp_receiver_handle_ != 0 ) { lib_player_.SetRTPReceiverTransportProtocol(rtp_receiver_handle_, is_tcp?1:0); lib_player_.SetRTPReceiverIPAddressType(rtp_receiver_handle_, 0); if (0 == lib_player_.CreateRTPReceiverSession(rtp_receiver_handle_, 0) ) { int local_port = lib_player_.GetRTPReceiverLocalPort(rtp_receiver_handle_); boolean ret = gb28181_agent_.inviteAudioBroadcast(command_from_user_name_,command_from_user_name_at_domain_, source_id_, target_id_, "IP4", local_ip_addr, local_port, is_tcp?"TCP/RTP/AVP":"RTP/AVP"); if (!ret ) { destoryRTPReceiver(); btnGB28181AudioBroadcast.setText("GB28181语音广播"); } else { btnGB28181AudioBroadcast.setText("GB28181语音广播呼叫中"); } } else { destoryRTPReceiver(); btnGB28181AudioBroadcast.setText("GB28181语音广播"); } } } } private String command_from_user_name_; private String command_from_user_name_at_domain_; private String source_id_; private String target_id_; public Runnable set(String command_from_user_name, String command_from_user_name_at_domain, String source_id, String target_id) { this.command_from_user_name_ = command_from_user_name; this.command_from_user_name_at_domain_ = command_from_user_name_at_domain; this.source_id_ = source_id; this.target_id_ = target_id; return this; } }.set(commandFromUserName, commandFromUserNameAtDomain, sourceID, targetID),0); }
Broadcast Response处理:
@Override public void ntsOnInviteAudioBroadcastResponse(String sourceID, String targetID, int statusCode, SessionDescription sessionDescription) { handler_.postDelayed(new Runnable() { @Override public void run() { Log.i(TAG, "ntsOnInviteAudioBroadcastResponse, statusCode:" + status_code_ +" sourceID:" + source_id_ + ", targetID:" + target_id_); boolean is_need_destory_rtp = true; if (gb28181_agent_ != null ) { boolean is_need_bye = 200==status_code_; if (200 == status_code_ && session_description_ != null && rtp_receiver_handle_ != 0 ) { MediaSessionDescription audio_des = null; List<SDPRtpMapAttribute> audio_attrs = new LinkedList<>(); Vector<MediaSessionDescription> audio_des_list = session_description_.getAudioDescriptions(); if (audio_des_list != null && !audio_des_list.isEmpty() ) { for (MediaSessionDescription m : audio_des_list) { if (m != null && m.isValidAddressType() && m.isHasAddress() && m.isHasRtpMapAttribute()) { audio_attrs.clear(); Vector<SDPRtpMapAttribute> rtp_maps = m.getRtpMapAttributes(); for (SDPRtpMapAttribute a : rtp_maps) { int type = a.getPayloadType(); String name = a.getEncodingName(); if (0 == type || 8 == type) audio_attrs.add(a); else if (name != null && !name.isEmpty()) { if (name.equals("PS") || name.equals("PCMA") || name.equals("PCMU")) audio_attrs.add(a); } } if (!audio_attrs.isEmpty()) { audio_des = m; break; } } } } if (audio_des != null && !audio_attrs.isEmpty() ) { // 有些场景下 SDP.SSRC 和 RTP.SSRC 不相等, 对于这种情况,不要设置SSRC给SDK, 屏蔽掉下面这行设置SSRC的代码 lib_player_.SetRTPReceiverSSRC(rtp_receiver_handle_, audio_des.getSSRC()); .... lib_player_.SetRTPReceiverRemoteAddress(rtp_receiver_handle_, audio_des.getAddress(), audio_des.getPort()); lib_player_.InitRTPReceiver(rtp_receiver_handle_); if (startAudioPlay()) { is_need_bye = false; is_need_destory_rtp = false; gb_broadcast_source_id_ = source_id_; gb_broadcast_target_id_ = target_id_; btnGB28181AudioBroadcast.setText("终止GB28181语音广播"); btnGB28181AudioBroadcast.setEnabled(true); } } } else { btnGB28181AudioBroadcast.setText("GB28181语音广播"); } if (is_need_bye) gb28181_agent_.byeAudioBroadcast(source_id_, target_id_); } if (is_need_destory_rtp) destoryRTPReceiver(); } private String source_id_; private String target_id_; private int status_code_; private SessionDescription session_description_; public Runnable set(String source_id, String target_id, int status_code, SessionDescription session_description) { this.source_id_ = source_id; this.target_id_ = target_id; this.status_code_ = status_code; this.session_description_ = session_description; return this; } }.set(sourceID, targetID, statusCode, sessionDescription),0); }
需要注意的是,以上述GB28181平台厂商为例,尽管SDP协商的是PCMA,实际上,平台侧下发的是PS的audio数据,如果不设置PS下去,会有以下日志:
2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo I/NTLogAndroid: NTRTP readSource: received rtp packet, is_udp:0, payload_type=96, len=232, t:0, sn=0, ssrc=200009727, src_address:0.0.0.0:0\ 2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\ 2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\ 2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\ 2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\ 2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\ 2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\ 2023-07-18 14:30:05.250 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\ 2023-07-18 14:30:05.277 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\ 2023-07-18 14:30:05.277 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\
所以通用的做法是判断SDP里面有没有PS,如果没有,设置RTP Receiver Payload Type下去:
总结
Android平台GB28181设备接入侧为什么没有公司愿意做?真的是坑太多,GB28181厂商太多,好多厂商包括大厂商并没有严格按照规范来,简单来说,50%的精力写代码,50%的精力查问题和各种兼容处理。