接口描述
在谈国网B接口的语音广播和语音对讲的时候,大家会觉得,国网B接口是不是和GB28181大同小异?实际上确实信令有差别,但是因为要GB28181设备接入测的对接,再次做国网B接口就简单多了。
语音对讲和广播包括信令接口和媒体流接口,采用与“调阅实时视频”流程相同的机制,不同之处
在于用户发出的请求消息的 SDP 仅携带音频描述信息。
语音所采用的编解码算法为 ITU-T G.711A。
语音对讲和广播,被调阅的平台无需进行音频分发行为。
接口流程
语音对讲和广播的接口流程如下:
主要功能流程如下:
a) F1:平台用户,对前端系统指定的前端系统摄像机发起实时音频的调阅请求,发送 INVITE 消
息,携带 SDP 内容,通过平台转发到前端系统。
b) F2:按照 SIP 要求,如前端系统在 0.5 s 内未处理该请求,则先发送 1xx 临时响应给平台。
c) F3:前端系统接受了调阅请求的操作,则发送携带 SDP 的 200 OK 响应到平台。
d) F4:平台发送 ACK 给前端系统,确认会话建立。
e) 实时语音流开始传输,前端设备/用户根据相应的解码器解码并语音输出。
f) F5:用户结束会话,平台发送 BYE 消息到前端系统。
g) F6:前端系统发送确认,将媒体通道拆线,会话结束。
接口参数
SIP头字段如下:
SIP响应码返回码如下:
SIP参数定义:
RTP 动态 Payload 定义如下:
消息示例
INVITE sip:前端设备地址编码@前端系统所属平台域名或IP地址 SIP/2.0 From: <sip:用户地址编码@用户所属平台域名或IP地址>;tag=3101300 To: <sip:前端设备地址编码@前端系统所属平台域名或IP地址> Contact: <sip:用户地址编码@用户所属平台域名或IP地址> Call-ID: c47k42 Via: SIP/2.0/UDP 用户所属平台IP地址;branch=z9hG4bK CSeq: 1 INVITE Content-type: application/SDP Content-Length: 消息体的长度 v=0 o=- 0 0 IN IP4 用户会话IP地址描述 s=- c=IN IP4 用户媒体IP地址描述 m=audio 38564 RTP/AVP 8 a=rtpmap:8 PCMA/8000 a=sendrecv
语音会话请求响应如下:
SIP/2.0 200 OK From: <sip: 用户地址编码@用户所属平台域名或IP地址>;tag=3101300 To: <sip: 前端设备地址编码@前端系统所属平台域名或IP地址>;tag=20b0660 Contact: <sip: 用户地址编码@用户所属平台域名或IP地址> Call-ID: c47k42 Via: SIP/2.0/UDP 用户所属平台IP地址;branch=z9hG4bK CSeq: 1 INVITE Content-type: application/SDP Content-Length: 消息体的长度 v=0 o=- 0 0 IN IP4 前端设备会话IP地址描述 s=- c=IN IP4 前端设备媒体IP地址描述 m=audio 1000 RTP/AVP 8 a=rtpmap:8 PCMA/8000 a=sendrecv
代码实现
@Override public void ntsOnInviteTalk(String deviceId, SessionDescription sessionDescription) { handler_.postDelayed(new Runnable() { @Override public void run() { // 先振铃响应下 gb28181_agent_.respondTalkInvite(180, device_id_); MediaSessionDescription audio_description = null; SDPRtpMapAttribute rtp_map_attribute = null; Vector<MediaSessionDescription> audio_des_list = session_description_.getAudioDescriptions(); if (audio_des_list != null && !audio_des_list.isEmpty()) { // 先尝试获取PCMA格式 for(MediaSessionDescription m : audio_des_list) { if (m != null && m.isValidAddressType() && m.isHasAddress()) { rtp_map_attribute = m.getRtpMapAttribute(SDPRtpMapAttribute.PCMA_ENCODING_NAME); if (rtp_map_attribute != null) { audio_description = m; break; } } } // 如果没有PCMA格式,尝试获取PS格式 if (null == rtp_map_attribute) { for(MediaSessionDescription m : audio_des_list) { if (m != null && m.isValidAddressType() && m.isHasAddress()) { rtp_map_attribute = m.getRtpMapAttribute(SDPRtpMapAttribute.PS_ENCODING_NAME); if (rtp_map_attribute != null) { audio_description = m; break; } } } } } if (null == audio_description) { gb28181_agent_.respondTalkInvite(488, device_id_); Log.i(TAG, "ntsOnInviteTalk get audio description is null, response 488, device_id:" + device_id_); return; } if (null == rtp_map_attribute ) { gb28181_agent_.respondTalkInvite(488, device_id_); Log.i(TAG, "ntsOnInviteTalk get rtp map attribute is null, response 488, device_id:" + device_id_); return; } Log.i(TAG,"ntsOnInviteTalk, device_id:" +device_id_+", is_tcp:" + audio_description.isRTPOverTCP() + " rtp_port:" + audio_description.getPort() + " ssrc:" + audio_description.getSSRC() + " address_type:" + audio_description.getAddressType() + " address:" + audio_description.getAddress() + " payload_type:" + rtp_map_attribute.getPayloadType() + " encoding_name:" + rtp_map_attribute.getEncodingName()); long rtp_sender_handle = libPublisher.CreateRTPSender(0); if (0 == rtp_sender_handle) { gb28181_agent_.respondTalkInvite(488, device_id_); Log.i(TAG, "ntsOnInviteTalk CreateRTPSender failed, response 488, device_id:" + device_id_); return; } gb_talk_rtp_payload_type_ = rtp_map_attribute.getPayloadType(); gb_talk_rtp_encoding_name_ = rtp_map_attribute.getEncodingName(); Log.i(TAG, "gb_talk_rtp_payload_type: " + gb_talk_rtp_payload_type_ + " gb_talk_rtp_encoding_name: " + gb_talk_rtp_encoding_name_); gb_talk_rtp_encoding_name_ = "PS"; libPublisher.SetRTPSenderTransportProtocol(rtp_sender_handle, audio_description.isRTPOverUDP()?0:1); libPublisher.SetRTPSenderIPAddressType(rtp_sender_handle, audio_description.isIPv4()?0:1); //libPublisher.SetRTPSenderLocalPort(rtp_sender_handle, 0); libPublisher.SetRTPSenderLocalPort(rtp_sender_handle, audio_description.getPort()); libPublisher.SetRTPSenderSSRC(rtp_sender_handle, audio_description.getSSRC()); libPublisher.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 256*1024); // 音频配置到256KB libPublisher.SetRTPSenderClockRate(rtp_sender_handle, rtp_map_attribute.getClockRate()); libPublisher.SetRTPSenderDestination(rtp_sender_handle, audio_description.getAddress(), audio_description.getPort()); gb_talk_is_receive_ = audio_description.isHasAttribute("sendrecv"); Log.i(TAG, "gb_talk_is_receive: " + gb_talk_is_receive_); if (gb_talk_is_receive_) { libPublisher.EnableRTPSenderReceive(rtp_sender_handle, 1); // 收包SSRC, 暂时不设置, 因为部分平台ssrc不一致的 // libPublisher.SetRTPSenderReceiveSSRC(rtp_sender_handle, audio_description.getSSRC()); // 这个一定要设置 libPublisher.SetRTPSenderReceivePayloadType(rtp_sender_handle, gb_talk_rtp_payload_type_, gb_talk_rtp_encoding_name_, 2, rtp_map_attribute.getClockRate()); // 目前发现某些平台 PS-PCMA 是8000, 不建议设置 if (gb_talk_rtp_encoding_name_.equals("PS")) { libPublisher.SetRTPSenderReceivePSClockFrequency(rtp_sender_handle, 8000); } // 如果是PCMA编码, 采样率和通道可以先不设置 // libPublisher.SetRTPSenderReceiveAudioSamplingRate(rtp_sender_handle, 8000); // libPublisher.SetRTPSenderReceiveAudioChannels(rtp_sender_handle, 1); } if (libPublisher.InitRTPSender(rtp_sender_handle) != 0 ) { gb28181_agent_.respondTalkInvite(488, device_id_); libPublisher.DestoryRTPSender(rtp_sender_handle); return; } int local_port = libPublisher.GetRTPSenderLocalPort(rtp_sender_handle); if (0==local_port) { gb28181_agent_.respondTalkInvite(488, device_id_); libPublisher.DestoryRTPSender(rtp_sender_handle); return; } Log.i(TAG,"ntsOnInviteTalk get local_port:" + local_port); String local_ip_addr = IPAddrUtils.getIpAddress(context_); MediaSessionDescription main_local_audio_des = new MediaSessionDescription(audio_description.getType()); main_local_audio_des.addFormat(String.valueOf(rtp_map_attribute.getPayloadType())); main_local_audio_des.addRtpMapAttribute(rtp_map_attribute); main_local_audio_des.addAttribute(new SDPAttribute("sendonly")); if (audio_description.isRTPOverTCP()) { // tcp主动链接服务端 main_local_audio_des.addAttribute(new SDPAttribute("setup", "active")); main_local_audio_des.addAttribute(new SDPAttribute("connection", "new")); } main_local_audio_des.setPort(local_port); main_local_audio_des.setTransportProtocol(audio_description.getTransportProtocol()); main_local_audio_des.setSSRC(audio_description.getSSRC()); MediaSessionDescription sub_local_audio_des = null; if (gb_talk_is_receive_) { sub_local_audio_des = new MediaSessionDescription(audio_description.getType()); sub_local_audio_des.addFormat(String.valueOf(rtp_map_attribute.getPayloadType())); sub_local_audio_des.addRtpMapAttribute(rtp_map_attribute); sub_local_audio_des.addAttribute(new SDPAttribute("recvonly")); if (audio_description.isRTPOverTCP()) { // tcp主动链接服务端 sub_local_audio_des.addAttribute(new SDPAttribute("setup", "active")); sub_local_audio_des.addAttribute(new SDPAttribute("connection", "new")); } sub_local_audio_des.setPort(local_port); sub_local_audio_des.setTransportProtocol(audio_description.getTransportProtocol()); sub_local_audio_des.setSSRC(audio_description.getSSRC()); } if (!gb28181_agent_.respondTalkInviteOK(device_id_, audio_description.getAddressType(), local_ip_addr, main_local_audio_des, sub_local_audio_des) ) { libPublisher.DestoryRTPSender(rtp_sender_handle); Log.e(TAG, "ntsOnInviteTalk call respondPlayInviteOK failed."); return; } gb_talk_rtp_sender_handle_ = rtp_sender_handle; } private String device_id_; private SessionDescription session_description_; public Runnable set(String device_id, SessionDescription session_des) { this.device_id_ = device_id; this.session_description_ = session_des; return this; } }.set(deviceId, sessionDescription),0); }
总结
国网B接口的语音广播和语音对讲,和GB28181的还是有些差别,B接口的语音广播和语音对讲,不需要先发broadcast过来,不用设备接入端发invite请求,而是电网平台侧发invite,类似实时视频请求播放流程,感兴趣的开发者,可以根据规范仔细解读研究。