技术背景
对接Android平台GB28181设备接入端语音广播的时候,我们有遇到过INVITE SDP需要PCMA格式的audio,对方同时回了PS和PCMA两种,然后,发数据的时候,直接发了PS的。
举个不恰当的例子,就像我去星巴克跟服务员协商,我需要点一杯卡布基诺,然后服务员说,我有红茶拿铁和卡布基诺,我很高兴,结果最后妹子给我上来的是红茶拿铁。是不是很遗憾?更遗憾的是,GB28181-2016规范里面,针对语音广播PCMA格式有明确的说明和范例,并没有针对PS的描述。
场景还原
服务员:先生您好,您要点咖啡吗?
MESSAGE sip:34020000002000000001@192.168.2.120:15060 SIP/2.0 Call-ID: 5ae7cfcd8b6696b02a512e32c38bbe35@192.168.43.177 CSeq: 5796113 MESSAGE From: <sip:44138091001320090001@3402000000>;tag=acc504a0 To: <sip:34020000002000000001@192.168.2.120:15060> Via: SIP/2.0/UDP 192.168.43.177:5060;rport;branch=z9hG4bK-353834-ae5e552996a10590419bd216b81ebaa6 Max-Forwards: 70 User-Agent: Daniusdk GB UserAgent V1.9-20221027 Content-Type: Application/MANSCDP+xml Content-Length: 172 <?xml version="1.0" encoding="GB2312"?> <Response> <CmdType>Broadcast</CmdType> <SN>1</SN> <DeviceID>34020000001380000001</DeviceID> <Result>OK</Result> </Response>
我:是的,我需要咖啡。
SIP/2.0 200 OK Via: SIP/2.0/UDP 192.168.43.177:5060;rport=5060;branch=z9hG4bK-353834-ae5e552996a10590419bd216b81ebaa6;received=192.168.225.1 From: <sip:44138091001320090001@3402000000>;tag=acc504a0 To: <sip:34020000002000000001@192.168.2.120:15060>;tag=1678330894580 Call-ID: 5ae7cfcd8b6696b02a512e32c38bbe35@192.168.43.177 CSeq: 5796113 MESSAGE Content-Length: 0 Contact: <sip:34020000002000000001@192.168.2.120:15060> User-Agent: GoSIP
我:能给我杯卡布基诺吗?
INVITE sip:31010400001360000001@3101040000 SIP/2.0 Call-ID: 33a3b4d751e1bf831f9d7ddd03218fcaf CSeq: 796255 INVITE From: <sip:44138091001320090001@3402000000>;tag=f2f8e526 To: <sip:31010400001360000001@3101040000> Via: SIP/2.0/UDP 192.168.43.177:5060;rport;branch=z9hG4bK-353834-b98838ae9494d94a9e3acb10c1dbc8ad Max-Forwards: 70 Contact: <sip:44138091001320090001@192.168.43.177:5060> Subject: 31010400001360000001:0104001939,34020000001380000001:0 User-Agent: Daniusdk GB UserAgent V1.9-20221027 Content-Type: APPLICATION/SDP Content-Length: 209 v=0 o=44138091001320090001 3887319688604 3887319688604 IN IP4 192.168.43.177 s=Play c=IN IP4 192.168.43.177 t=0 0 m=audio 52428 RTP/AVP 8 a=recvonly a=rtpmap:8 PCMA/8000 y=0104001939 f=v/a/1/8/1
服务员:先生您好,我们这边有红茶拿铁和卡布基诺。
SIP/2.0 200 OK Via: SIP/2.0/UDP 192.168.43.177:5060;rport=5060;branch=z9hG4bK-353834-b98838ae9494d94a9e3acb10c1dbc8ad;received=192.168.225.1 From: <sip:44138091001320090001@3402000000>;tag=f2f8e526 To: <sip:31010400001360000001@3101040000>;tag=1678330894725 Call-ID: 33a3b4d751e1bf831f9d7ddd03218fcaf CSeq: 796255 INVITE Content-Length: 208 Contact: <sip:34020000002000000001@192.168.2.120:15060> Content-Type: application/sdp Allow: INVITE, ACK, CANCEL, REGISTER, MESSAGE, NOTIFY User-Agent: GoSIP v=2 o=34020000002000000001 0 0 IN IP4 192.168.2.120 s=Play c=IN IP4 192.168.2.120 t=0 0 m=audio 15082 RTP/AVP 96 8 a=sendonly a=rtpmap:96 PS/90000 a=rtpmap:8 PCMA/8000 y=0104001939 f=v/a/1/8/1
我:好的,那太棒了。
ACK sip:34020000002000000001@192.168.2.120:15060 SIP/2.0 Call-ID: 33a3b4d751e1bf831f9d7ddd03218fcaf CSeq: 796255 ACK Via: SIP/2.0/UDP 192.168.43.177:5060;rport;branch=z9hG4bK-353834-b01972a6eaad0137b0bca192d6388540 From: <sip:44138091001320090001@3402000000>;tag=f2f8e526 To: <sip:31010400001360000001@3101040000>;tag=1678330894725 Max-Forwards: 70 Contact: <sip:44138091001320090001@192.168.43.177:5060> User-Agent: Daniusdk GB UserAgent V1.9-20221027 Content-Length: 0
咖啡上来了。。
我:怎么给我上的是红茶拿铁?明明我们协商的时候,我要的是卡布基诺!
你能有什么办法?
遇到这种情况,要么push厂商调整,既然SDP回的有PCMA格式,那就直接发送PCMA的语音广播数据,要么只能我们这边兼容,就像明明不喜欢红茶拿铁,还是要耐着性子喝。
规范回顾
说了这么多废话,还是回顾下语音广播的交互流程,因为之前的blog做过几次说明,这里不再赘述:
技术实现
本文以大牛直播SDK的Android平台基于Camera2的采集demo为例,如果需要注册到GB28181平台,点击页面的“启动GB28181”即可,有语音广播过来后,使能“GB28181语音广播”按钮,用于主动关闭语音广播之用。
其他处理不再赘述,这里说说invite audio broadcast response里面的处理:
//Author: daniusdk.com @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; SDPRtpMapAttribute audio_attr = null; 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_des = m; audio_attr = audio_des.getRtpMapAttributes().get(0); break; } } } if ( audio_des != null && audio_attr != null ) { lib_player_.SetRTPReceiverSSRC(rtp_receiver_handle_, audio_des.getSSRC()); int payload_type = audio_attr.getPayloadType(); String encoding_name = audio_attr.getEncodingName(); lib_player_.SetRTPReceiverPayloadType(rtp_receiver_handle_, payload_type, encoding_name, 2, audio_attr.getClockRate()); if (encoding_name != null && encoding_name.equals("PS")) { // 分析PS流,需要设置下面的特殊值 // 如果是90000的话,不需要设置 // lib_player_.SetRTPReceiverPSClockFrequency(rtp_receiver_handle_, payload_type, 1000); // 含义请看28181文档, 如果PS流中有PSM的话,不需要设置 // lib_player_.SetRTPReceiverPSMap(rtp_receiver_handle_, payload_type, 0x90, 0xC0); } // 如果是PCMA, SDK会默认填 采样率8000, 通道1, 其他音频编码需要手动填入 //lib_player_.SetRTPReceiverAudioSamplingRate(rtp_receiver_handle_, payload_type, 8000); //lib_player_.SetRTPReceiverAudioChannels(rtp_receiver_handle_, payload_type, 1); 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); }
我们根据回上来的SDP,判断encoding_name是PCMA还是PS的,如果是PS的,可选设置下SetRTPReceiverPSClockFrequency()和SetRTPReceiverPSMap()。如果是PCMA的,还是按照老的逻辑处理即可。
总结
GB28181设备接入这块,遇到的国标平台侧的问题真的是五花八门,真是印证了那句话:GB28181相关的模块,做demo容易,做产品,真的太难了,怪不得这么多公司懒得搞这块。