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

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

技术背景

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


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


提到语音对讲,典型的限制如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包发送或接收:

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 端口号 RTP/AVP 96
a=rtpmap:96 PS/90000
a=sendrecv
y=xxxxxxxx....


“s=Play”模式下,SDP处理难度加大,按照GB/T28181规范,当看到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设备接入模块,已经实现了上述提到的技术方案,相关接口设计如下:

// 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);
}

相关调用示例代码如下:

    @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技术优势有目共睹,更适合相对复杂的网络环境。遗憾的是,大多公司都没有实现,或者说市面上真正实现跨网段语音对讲的尚在少数,感兴趣的开发者可以酌情参考。

相关文章
|
6月前
|
存储 Android开发
如何查看Flutter应用在Android设备上已被撤销的权限?
如何查看Flutter应用在Android设备上已被撤销的权限?
270 64
|
3月前
|
人工智能 Android开发 iOS开发
安卓版快捷指令,加了AI语音可以一句话操作v0.2.7
Shortcuts for Android(SFA)是一款安卓自动化工具,支持语音创建快捷指令,实现听歌、导航、发消息等操作。操作简单,提升效率,快来体验语音控制的便捷!
251 0
安卓版快捷指令,加了AI语音可以一句话操作v0.2.7
|
6月前
|
存储 Android开发 数据安全/隐私保护
如何在Android设备上撤销Flutter应用程序的所有权限?
如何在Android设备上撤销Flutter应用程序的所有权限?
351 64
|
6月前
|
缓存 Android开发 开发者
Flutter环境配置完成后,如何在Android设备上运行Flutter应用程序?
Flutter环境配置完成后,如何在Android设备上运行Flutter应用程序?
1008 62
|
6月前
|
开发工具 Android开发 开发者
在Android设备上运行Flutter应用程序时,如果遇到设备未授权的问题该如何解决?
在Android设备上运行Flutter应用程序时,如果遇到设备未授权的问题该如何解决?
321 61
|
3月前
|
监控 Android开发 数据安全/隐私保护
批量发送短信的平台,安卓群发短信工具插件脚本,批量群发短信软件【autojs版】
这个Auto.js脚本实现了完整的批量短信发送功能,包含联系人管理、短信内容编辑、发送状态监控等功能
|
4月前
|
机器学习/深度学习 人工智能 搜索推荐
安卓声音克隆:让你的声音独一无二,探索个性化语音新世界!
在这个数字化飞速发展的时代,个性化已成为我们追求的重要目标之一。从独特的手机铃声到定制化的社交媒体内容,我们总希望能展现出与众不同的自我。那么,你是否想过在安卓设备上也能找到声音克隆的神奇功能,让你的
|
7月前
|
监控 Shell Linux
Android调试终极指南:ADB安装+多设备连接+ANR日志抓取全流程解析,覆盖环境变量配置/多设备调试/ANR日志分析全流程,附Win/Mac/Linux三平台解决方案
ADB(Android Debug Bridge)是安卓开发中的重要工具,用于连接电脑与安卓设备,实现文件传输、应用管理、日志抓取等功能。本文介绍了 ADB 的基本概念、安装配置及常用命令。包括:1) 基本命令如 `adb version` 和 `adb devices`;2) 权限操作如 `adb root` 和 `adb shell`;3) APK 操作如安装、卸载应用;4) 文件传输如 `adb push` 和 `adb pull`;5) 日志记录如 `adb logcat`;6) 系统信息获取如屏幕截图和录屏。通过这些功能,用户可高效调试和管理安卓设备。
|
12天前
|
开发工具 Android开发
X Android SDK file not found: adb.安卓开发常见问题-Android SDK 缺少 `adb`(Android Debug Bridge)-优雅草卓伊凡
X Android SDK file not found: adb.安卓开发常见问题-Android SDK 缺少 `adb`(Android Debug Bridge)-优雅草卓伊凡
172 11
X Android SDK file not found: adb.安卓开发常见问题-Android SDK 缺少 `adb`(Android Debug Bridge)-优雅草卓伊凡
|
22天前
|
Java 开发工具 Maven
【01】完整的安卓二次商业实战-详细的初级步骤同步项目和gradle配置以及开发思路-优雅草伊凡
【01】完整的安卓二次商业实战-详细的初级步骤同步项目和gradle配置以及开发思路-优雅草伊凡
82 6

热门文章

最新文章