Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Android平台GB28181设备接入端如何支持跨网段语音对讲?

Android平台GB28181设备接入端如何支持跨网段语音对讲?

原创
作者头像
音视频牛哥
发布于 2022-12-12 16:45:20
发布于 2022-12-12 16:45:20
77300
代码可运行
举报
运行总次数:0
代码可运行

技术背景

如果你是音视频开发者亦或寻求这块技术方案的公司,在探讨这个问题之前,你可能网上看了太多关于语音广播和语音对讲相关的资料,大多文章认为语音对讲和语音广播无本质区别,实现思路也大同小异。

今天我们主要探讨的是,语音对讲有哪些可行的技术方案?实际使用场景下,分别有哪些限制?如何实现相对可行的语音对讲方案?

提到语音对讲,典型的限制如RTP UDP包无法实现跨网段的数据传输,基于此,一般可以考虑以下两种解决方案:

方案1:

Android平台GB28181设备接入端,语音这块,实时音视频点播通道,编码后的audio数据,封装到PS包,和视频数据一起打包。数据接收这块,跨网段使用RTP over TCP模式。

不幸的是,好多国标平台侧,并不支持TCP,使用UDP打洞,这需要部署单独的打洞服务器,也存在穿透不成功的情况。

方案2:

通过语音对讲模式,一般来说SDP里面“s=Talk”代表语音对讲,但实际场景下,又有两种模式:

  1. 模式1:“s=Talk”模式;
  2. 模式2:“s=Play”模式。

模式1:“s=Talk”模式,这种实现,相对来说难度稍小,只需把PCMA打包成rtp包发送或接收:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
s=Talk
............
t=0 0
m=audio 端口 RTP/AVP 8
a=rtpmap:8 PCMA/8000
a=sendrecv
y=xxxx.......

模式2:“s=Play”模式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
s=Play
............
t=0 0
m=audio 端口号 RTP/AVP 96
a=rtpmap:96 PS/90000
a=sendrecv
y=xxxxxxxx....

“s=Play”模式下,按照GB28181规范,当看到SDP描述里面“m=audio”时,可判定国标平台侧不想要video数据,仅需要国标设备接入端发送纯音频即可,从而实现传统意义的语音对讲。

大多开发者在实现GB28181设备接入的时候都是音视频数据一起打包发送的,如果需要兼容这种情况,需要针对纯音频打包PS,纯音频打包PS,可以参照GB/T28181-2016规范针对音视频或纯视频模式下的PS打包,当然,也可以直接PCMA over RTP模式。

方案2的SDP信息有个“a=sendrecv”,具体来说,用同一个端口来同时发送和接收RTP包。按照GB28181标准,语音对讲,先把audio RTP包发到媒体服务器,需要确保各个网段的GB28181设备可以访问到媒体服务器。Android平台GB28181设备接入端先主动发RTP包到媒体服务器,媒体服务器再用相同的端口,发到Android平台GB28181设备接入端。

值得一提的是,语音广播在一些国标平台的实现,可能走点对点模式(如宇视),并没有通过媒体服务器来转发RTP包,此外,如果SDP信息中“s=Play”,那么对应的200 OK响应中的SDP 也需要确保是Play模式,即“s=Play”。

优劣势分析

方案1把音视频数据,按照GB/T28181-2016规范,都打到一个PS包中,然后使用相同的端口发送,PS包大,对带宽要求也高,如因网络抖动很容易出现延迟或丢包,从而导致语音对讲的极差体验,而且UDP存在穿透问题。

方案2,我们只传纯音频,加之PCMA码率仅有64kbps,加上RTP头的字节数,带宽占用非常小,美中不足的是,技术实现相对复杂。

技术实现

我们Android平台GB28181设备接入模块,已经实现了上述提到的技术方案,相关接口设计如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Github: https://github.com/daniulive/SmarterStreaming
// Contract: 89030985@qq.com
 
public interface GBSIPAgentTalkListener {
    /*
     *收到语音对讲INVITE
     */
    void ntsOnInviteTalk(String deviceId, SessionDescription sessionDescription);
 
    /*
     *发送talk invite response 异常
     */
    void ntsOnTalkInviteResponseException(String deviceId, int statusCode, String errorInfo);
 
    /*
     * 收到CANCEL Talk INVITE请求
     */
    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);
   
   /*
     *响应Invite Talk 200 OK
     */
    boolean respondTalkInviteOK(String deviceId, String addressType, String localAddress,
                                MediaSessionDescription mainLocalAudioDescription, MediaSessionDescription subLocalAudioDescription);
 
    /*
     *响应Invite Talk 其他状态码
     */
    boolean respondTalkInvite(int statusCode, String deviceId);
 
 
    /*
     *终止Talk会话
     */
    void terminateTalk(String deviceId, boolean isSendBYE);
 
    /*
     *终止所有Talk会话
     */
    void terminateAllTalks(boolean isSendBYE);
}

相关调用示例代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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()) {

                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); // 音频配置到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");

            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());

                // 目前发现某些平台 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);
}

总结

实际上,GB28181平台语音广播和语音对讲,特别是语音对讲,不光要解决传输跨网段问题,还可能要处理回音,噪音,增益控制等,这块,我们之前有了非常好的技术积累,处理起来轻车熟路。

两种技术方案虽然都可以实现语音对讲,方案1相对实现起来简单,但缺点明显,方案2技术优势有目共睹,更适合相对复杂的网络环境。遗憾的是,大多公司都没有实现,或者说市面上真正实现跨网段语音对讲的尚在少数,感兴趣的开发者可以酌情参考。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
国网B接口语音对讲和广播技术探究及与GB28181差别
在谈国网B接口的语音广播和语音对讲的时候,大家会觉得,国网B接口是不是和GB28181大同小异?实际上确实信令有差别,但是因为要GB28181设备接入测的对接,再次做国网B接口就简单多了。
音视频牛哥
2023/04/14
6840
国网B接口语音对讲和广播技术探究及与GB28181差别
GB28181基于TCP协议的视音频媒体传输探究及实现
实时视频点播、历史视频回放与下载的 TCP媒体传输应支持基于RTP封装的视音频PS流,封装格式参照IETFRFC4571。
音视频牛哥
2022/10/30
8660
GB28181基于TCP协议的视音频媒体传输探究及实现
如何实现Android平台GB28181设备接入模块按需打开摄像头并回传数据
实际上,我在年前的blog,已经写过Android平台GB28181后台service模式启动摄像头按需回传数据了,此次版本,是上个demo的迭代版,目的是平台侧如果不发起回传请求的话,摄像头不打开。
音视频牛哥
2024/02/19
2610
如何实现Android平台GB28181设备接入模块按需打开摄像头并回传数据
国网B接口调阅实时视频规范解读和代码示例分析
调阅实时视频包括信令接口和媒体流接口,采用标准的SIP INVITE+SDP流程,媒体传输使用RTP/RTCP。
音视频牛哥
2023/04/14
6180
国网B接口调阅实时视频规范解读和代码示例分析
GB28181平台如何接入无人机实现智能巡检?
大家都知道,无人机-巡检系统,有效解决了传统巡查工作空间和时间局限问题,降低人力工作成本,有效替代人工巡检工作模式。智能巡检系统通过人工智能技术和机械智能技术完美结合,在工业等场景下,应用非常广泛。本文旨在讲如何实现无人机(如大疆无人机)数据到GB28181平台(如海康、大华、宇视等国标平台)。
音视频牛哥
2022/10/08
8720
GB28181平台如何接入无人机实现智能巡检?
Android平台GB28181实时回传流程和技术实现
GB28181 中的 “INVITE” 是会话初始协议(SIP)中的一种请求方法,主要用于邀请一个或多个参与者加入特定的会话。在 GB28181 标准中,“INVITE” 请求通常用于发起媒体流的传输请求。当一个设备想要接收来自另一个设备的媒体流时,它会向目标设备发送一个 “INVITE” 请求,其中包含了关于会话的描述信息,如媒体类型、编码格式、传输协议等。
音视频牛哥
2024/09/30
1560
Android平台GB28181实时回传流程和技术实现
Android平台GB28181设备接入侧如何实现按需打开视音频采集传输
GB/T28181是中国国家标准,全称为《安全防范视频监控联网系统信息传输、交换、控制技术要求》,该标准规定了城市安全防范监控系统中视频监控联网系统的一般要求和架构,以及信息传输、交换、控制的技术要求。它主要应用于安防领域,为各种视频监控系统提供了一致的接口规范,使得不同厂商生产的视频监控设备可以相互兼容。规范规定了公共安全视频监控联网系统(以下简称“联网系统”)的互联结构,传输、交换、控制的基本要求和安全性要求,以及控制、传输流程和协议接口等技术要求。适用于公共安全视频监控联网系统的方案设计、系统检测、验收以及与之相关的设备研发生产。其他视频监控联网系统可参照执行。目前已更新至GB/T28181-2022版。
音视频牛哥
2023/09/15
2790
Android平台GB28181设备接入侧如何实现按需打开视音频采集传输
Android平台GB28181设备接入模块如何实现实时视频和本地录像双码流编码
我们在做Android平台GB28181设备接入模块的时候,遇到这样的场景,比如执法记录仪或智慧工地等场景下,由于GB28181设备接入模块,注册到国标平台后,平时只是心跳保持,或还有实时位置订阅,查看视频的时候,是按需看,而且有时候,网络环境并不是太好,所以,催生了这样一个诉求:部分开发者希望能本地录像的时候,录制高分辨率(比如1920*1080),国标平台侧发起实时视频查看请求的时候,上传低分辨率(如1280*720)数据,有点类似于IPC的主码流和子码流。
音视频牛哥
2023/05/23
4940
Android平台GB28181设备接入模块如何实现实时视频和本地录像双码流编码
GB/T28181联网系统通信协议结构和技术实现
联网系统有关设备之间会话建立过程的会话协商和媒体协商应采用IETF RFC 4566协议描述,主要内容包括会话描述、媒体信息描述、时间信息描述。
音视频牛哥
2022/09/05
4140
GB/T28181联网系统通信协议结构和技术实现
Android平台GB28181设备接入模块实现后台service按需回传摄像头数据到国标平台侧
我们在做Android平台GB28181设备对接模块的时候,遇到这样的技术需求,开发者希望能以后台服务的形式运行程序,国标平台侧没有视频回传请求的时候,仅保持信令链接,有发起视频回传请求或语音广播时,打开摄像头,并实时回传音视频数据或接收处理国标平台侧发过来的语音广播数据。
音视频牛哥
2024/02/06
1780
Android平台GB28181设备接入模块实现后台service按需回传摄像头数据到国标平台侧
Android平台GB28181设备接入端如何实现多视频通道接入?
我们在设计Android平台GB28181设备接入模块的时候,有这样的场景诉求,一个设备可能需要多个通道,常见的场景,比如车载终端,一台设备,可能需要接入多个摄像头,那么这台车载终端设备可以作为主设备,然后,主设备下,配置多个通道,听起来是不是有点儿类似于DVR或NVR?
音视频牛哥
2023/08/08
3480
Android平台GB28181设备接入端如何实现多视频通道接入?
Android平台GB28181设备接入端如何降低资源占用和性能消耗
我们在做GB28181设备接入模块的时候,考虑到好多设备性能一般,我们一般的设计思路是,先注册设备到平台侧,平台侧发calalog过来,获取设备信息,然后,设备侧和国标平台侧维持心跳,如果有位置订阅信息,按照订阅时间间隔,实时上报设备位置信息。
音视频牛哥
2023/08/06
2680
Android平台GB28181设备接入端如何降低资源占用和性能消耗
如何实现Android平台GB28181前端设备接入
在实现Android平台GB28181前端设备接入之前,我们几年前就有了非常成熟的RTMP推送、RTSP推送和轻量级RTSP服务等模块,特别是RTMP推送,行业内应用非常广泛,好多开发者可能会问,既然有了以上模块,干嘛还要实现GB28181的前端接入呢?
音视频牛哥
2022/02/27
1.4K0
​​Android平台GB28181历史视音频文件回放规范解读及技术实现
在实现GB28181历史视音频文件回放之前,我们已完成了历史视音频文件检索和下载,历史视音频回放,在GB28181平台非常重要,比如执法记录仪等前端设备,默认录像数据存储在前端设备侧,如果需要上传到平台统一保存,除了到工作站拷贝外,还可以通过GB28181的历史视音频文件下载到指挥中心。如果指挥中心需要直接看历史视音频文件,也可以通过GB28181历史视音频回放实现。
音视频牛哥
2023/11/07
1.2K1
​​Android平台GB28181历史视音频文件回放规范解读及技术实现
如何让Android平台像IPC一样实现GB28181前端设备接入
好多开发者在做国标对接的时候,首先想到的是IPC摄像头,通过参数化配置,接入到国标平台,实现媒体数据的按需查看等操作。
音视频牛哥
2022/09/14
7060
Android平台实现mp4文件实时推送RTMP|轻量级RTSP服务|GB28181平台
好多开发者有这样的诉求,想把本地录制的MP4文件,以实时流数据的形式,推送到RTMP服务器,注入轻量级RTSP服务,或者对接到GB28181平台,这块前几年我们就有对接。
音视频牛哥
2022/09/29
4020
GB/T28181-2016基于RTP的视音频数据封装和技术实现
基于RTP的 PS封装首先按照ISO/IEC13818-1:2000将视音频流封装成PS包,再将PS包以负载的方式封装成 RTP包。
音视频牛哥
2022/09/25
1.2K0
Android平台实现RTSP|RTMP转GB28181网关接入
在事先Android平台RTSP、RTMP转GB28181网关之前,我们已经实现了Android平台GB28181的接入,可实现Android平台采集到的音视频数据,编码后,打包按需发到GB28181服务平台。此外,拉流端,我们已经有了成熟的RTSP和RTMP拉流播放方案。
音视频牛哥
2022/04/19
7260
Android平台实现RTSP|RTMP转GB28181网关接入
如何实现Android视音频数据对接到GB28181平台(SmartGBD)
在做Android平台GB28181接入模块之前,我们在RTMP推送播放、RTSP轻量级服务、转发、播放这块,已经有很多年的经验,这意味着,我们不需要重复造轮子,已有屏幕、摄像头或编码前(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型)或编码后(H.264/HEVC)数据,只需要实现GB28181的信令交互,和媒体处理,即可实现不具备国标音视频能力的 Android终端,通过平台注册接入到现有的GB/T28181—2016服务。
音视频牛哥
2023/10/26
4760
如何实现Android视音频数据对接到GB28181平台(SmartGBD)
Android平台GB28181设备接入模块开发填坑指南
为什么要开发Android平台GB28181设备接入模块?这个问题不再赘述,在做Android平台GB28181客户端的时候,媒体数据这块,我们已经有了很好的积累,因为在此之前,我们就开发了非常成熟的RTMP推送、轻量级RTSP服务、录像模块、针对音视频的对接处理单元。这让我们在做Android平台GB28181设备接入模块的时候,可以有更多的精力在信令交互和国标平台对接。
音视频牛哥
2023/11/26
7230
Android平台GB28181设备接入模块开发填坑指南
推荐阅读
相关推荐
国网B接口语音对讲和广播技术探究及与GB28181差别
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档