Android平台GB28181设备接入端如何支持跨网段语音对讲
技術(shù)背景
如果你是音視頻開發(fā)者亦或?qū)で筮@塊技術(shù)方案的公司,在探討這個(gè)問題之前,你可能網(wǎng)上看了太多關(guān)于語音廣播和語音對(duì)講相關(guān)的資料,大多文章認(rèn)為語音對(duì)講和語音廣播無本質(zhì)區(qū)別,實(shí)現(xiàn)思路也大同小異。
今天我們主要探討的是,語音對(duì)講有哪些可行的技術(shù)方案?實(shí)際使用場(chǎng)景下,分別有哪些限制?如何實(shí)現(xiàn)相對(duì)可行的語音對(duì)講方案?
提到語音對(duì)講,典型的限制如RTP UDP包無法實(shí)現(xiàn)跨網(wǎng)段的數(shù)據(jù)傳輸,基于此,一般可以考慮以下兩種解決方案:
方案1:
Android平臺(tái)GB28181設(shè)備接入端,語音這塊,走實(shí)時(shí)音視頻點(diǎn)播通道,編碼后的audio數(shù)據(jù),封裝到PS包,和視頻數(shù)據(jù)一起打包。數(shù)據(jù)接收這塊,跨網(wǎng)段使用RTP over TCP模式。
不幸的是,好多國(guó)標(biāo)平臺(tái)側(cè),并不支持TCP,使用UDP打洞,這需要部署單獨(dú)的打洞服務(wù)器,也存在穿透不成功的情況。
方案2:
通過語音對(duì)講模式,一般來說SDP里面“s=Talk”代表語音對(duì)講,但實(shí)際場(chǎng)景下,又有兩種模式:
模式1:“s=Talk”模式,這種實(shí)現(xiàn),相對(duì)來說難度稍小,只需把PCMA打包成rtp包發(fā)送或接收:
s=Talk ............ t=0 0 m=audio 端口 RTP/AVP 8 a=rtpmap:8 PCMA/8000 a=sendrecv y=xxxx.......模式2:“s=Play”模式:
s=Play ............ t=0 0 m=audio 端口號(hào) RTP/AVP 96 a=rtpmap:96 PS/90000 a=sendrecv y=xxxxxxxx....“s=Play”模式下,SDP處理難度加大,按照GB/T28181規(guī)范,當(dāng)看到SDP描述里面“m=audio”時(shí),可判定國(guó)標(biāo)平臺(tái)側(cè)不想要video數(shù)據(jù),僅需要國(guó)標(biāo)設(shè)備接入端發(fā)送純音頻即可,從而實(shí)現(xiàn)傳統(tǒng)意義的語音對(duì)講。
大多開發(fā)者在實(shí)現(xiàn)GB28181設(shè)備接入的時(shí)候都是音視頻數(shù)據(jù)一起打包發(fā)送的,如果需要兼容這種情況,需要針對(duì)純音頻打包PS,純音頻打包PS,可以參照GB/T28181-2016規(guī)范針對(duì)音視頻或純視頻模式下的PS打包,當(dāng)然,也可以直接PCMA over RTP模式。
方案2的SDP信息有個(gè)“a=sendrecv”,具體來說,用同一個(gè)端口來同時(shí)發(fā)送和接收RTP包。按照GB28181標(biāo)準(zhǔn),語音對(duì)講,先把a(bǔ)udio RTP包發(fā)到媒體服務(wù)器,需要確保各個(gè)網(wǎng)段的GB28181設(shè)備可以訪問到媒體服務(wù)器。Android平臺(tái)GB28181設(shè)備接入端先主動(dòng)發(fā)RTP包到媒體服務(wù)器,媒體服務(wù)器再用相同的端口,發(fā)到Android平臺(tái)GB28181設(shè)備接入端。
值得一提的是,語音廣播在一些國(guó)標(biāo)平臺(tái)的實(shí)現(xiàn),可能走點(diǎn)對(duì)點(diǎn)模式(如宇視),并沒有通過媒體服務(wù)器來轉(zhuǎn)發(fā)RTP包,此外,如果SDP信息中“s=Play”,那么對(duì)應(yīng)的200 OK響應(yīng)中的SDP 也需要確保是Play模式,即“s=Play”。
優(yōu)劣勢(shì)分析
方案1把音視頻數(shù)據(jù),按照GB/T28181-2016規(guī)范,都打到一個(gè)PS包中,然后使用相同的端口發(fā)送,PS包大,對(duì)帶寬要求也高,如因網(wǎng)絡(luò)抖動(dòng)很容易出現(xiàn)延遲或丟包,從而導(dǎo)致語音對(duì)講的極差體驗(yàn),而且UDP存在穿透問題。
方案2,我們只傳純音頻,加之PCMA碼率僅有64kbps,加上RTP頭的字節(jié)數(shù),帶寬占用非常小,美中不足的是,技術(shù)實(shí)現(xiàn)相對(duì)復(fù)雜。
技術(shù)實(shí)現(xiàn)
我們Android平臺(tái)GB28181設(shè)備接入模塊,已經(jīng)實(shí)現(xiàn)了上述提到的技術(shù)方案,相關(guān)接口設(shè)計(jì)如下:
// Github: https://github.com/daniulive/SmarterStreaming // Contract: 89030985@qq.compublic interface GBSIPAgentTalkListener {/**收到語音對(duì)講INVITE*/void ntsOnInviteTalk(String deviceId, SessionDescription sessionDescription);/**發(fā)送talk invite response 異常*/void ntsOnTalkInviteResponseException(String deviceId, int statusCode, String errorInfo);/** 收到CANCEL Talk INVITE請(qǐng)求*/void ntsOnCancelTalk(String deviceId);/** 收到Ack*/void ntsOnAckTalk(String deviceId);/** 收到Bye*/void ntsOnByeTalk(String deviceId);/** 不是在收到BYE Message情況下,終止Talk*/void ntsOnTerminateTalk(String deviceId);void ntsOnTalkDialogTerminated(String deviceId); }public interface GBSIPAgent {// 其他接口省略......void addTalkListener(GBSIPAgentTalkListener talkListener);/**響應(yīng)Invite Talk 200 OK*/boolean respondTalkInviteOK(String deviceId, String addressType, String localAddress,MediaSessionDescription mainLocalAudioDescription, MediaSessionDescription subLocalAudioDescription);/**響應(yīng)Invite Talk 其他狀態(tài)碼*/boolean respondTalkInvite(int statusCode, String deviceId);/**終止Talk會(huì)話*/void terminateTalk(String deviceId, boolean isSendBYE);/**終止所有Talk會(huì)話*/void terminateAllTalks(boolean isSendBYE); }相關(guān)調(diào)用示例代碼如下:
@Overridepublic void ntsOnInviteTalk(String deviceId, SessionDescription sessionDescription) {handler_.postDelayed(new Runnable() {@Overridepublic 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()) {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;}}}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();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.SetRTPSenderSSRC(rtp_sender_handle, audio_description.getSSRC());libPublisher.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 256*1024); // 音頻配置到256KBlibPublisher.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");if (gb_talk_is_receive_) {libPublisher.EnableRTPSenderReceive(rtp_sender_handle, 1);// 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());// 目前發(fā)現(xiàn)某些平臺(tái) PS-PCMA 是8000, 不建議設(shè)置//if (gb_talk_rtp_encoding_name_.equals("PS")) {// libPublisher.SetRTPSenderReceivePSClockFrequency(rtp_sender_handle, 8000);// }// 如果是PCMA編碼, 采樣率和通道可以先不設(shè)置// 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主動(dòng)鏈接服務(wù)端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主動(dòng)鏈接服務(wù)端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);}總結(jié)
實(shí)際上,GB28181平臺(tái)語音廣播和語音對(duì)講,特別是語音對(duì)講,不光要解決傳輸跨網(wǎng)段問題,還可能要處理回音,噪音,增益控制等,這塊,我們之前有了非常好的技術(shù)積累,處理起來輕車熟路,有需要測(cè)試的開發(fā)者,也可以私信聯(lián)系我。
兩種技術(shù)方案雖然都可以實(shí)現(xiàn)語音對(duì)講,方案1相對(duì)實(shí)現(xiàn)起來簡(jiǎn)單,但缺點(diǎn)明顯,方案2技術(shù)優(yōu)勢(shì)有目共睹,更適合相對(duì)復(fù)雜的網(wǎng)絡(luò)環(huán)境。遺憾的是,大多公司都沒有實(shí)現(xiàn),或者說市面上真正實(shí)現(xiàn)跨網(wǎng)段語音對(duì)講的尚在少數(shù),感興趣的開發(fā)者可以酌情參考。
總結(jié)
以上是生活随笔為你收集整理的Android平台GB28181设备接入端如何支持跨网段语音对讲的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何提升智慧办公效率?华为云桌面不可少,
- 下一篇: android sina oauth2.