Android平台GB28181设备接入端语音广播支持PS格式

简介: 对接Android平台GB28181设备接入端语音广播的时候,我们有遇到过INVITE SDP需要PCMA格式的audio,对方同时回了PS和PCMA两种,然后,发数据的时候,直接发了PS的。

技术背景

对接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做过几次说明,这里不再赘述:

478ebbb8d1be536ee5043482d96d753d.png

技术实现

本文以大牛直播SDK的Android平台基于Camera2的采集demo为例,如果需要注册到GB28181平台,点击页面的“启动GB28181”即可,有语音广播过来后,使能“GB28181语音广播”按钮,用于主动关闭语音广播之用。

62d48a599711e51957e1fdf0d695d0f4.jpg

其他处理不再赘述,这里说说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容易,做产品,真的太难了,怪不得这么多公司懒得搞这块。

相关文章
|
3月前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
127 1
|
4月前
|
IDE Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【9月更文挑战第27天】在移动应用开发的世界中,Android和iOS是两个主要的操作系统平台。每个系统都有其独特的开发环境、工具和用户群体。本文将深入探讨这两个平台的关键差异点,并分析这些差异如何影响应用的性能、用户体验和最终的市场表现。通过对比分析,我们将揭示选择正确的开发平台对于确保项目成功的重要作用。
|
1月前
|
IDE 开发工具 Android开发
移动应用开发之旅:探索Android和iOS平台
在这篇文章中,我们将深入探讨移动应用开发的两个主要平台——Android和iOS。我们将了解它们的操作系统、开发环境和工具,并通过代码示例展示如何在这两个平台上创建一个简单的“Hello World”应用。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧,帮助你更好地理解和掌握移动应用开发。
65 17
|
3月前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
120 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
5月前
|
传感器 Android开发 芯片
不写一行代码(三):实现安卓基于i2c bus的Slaver设备驱动
本文是系列文章的第三篇,展示了如何在Android系统中利用现有的i2c bus驱动,通过编写设备树节点和应用层的控制代码,实现对基于i2c bus的Slaver设备(如六轴陀螺仪模块QMI8658C)的控制,而无需编写设备驱动代码。
69 0
不写一行代码(三):实现安卓基于i2c bus的Slaver设备驱动
|
5月前
|
Android开发
不写一行代码(二):实现安卓基于PWM的LED设备驱动
本文介绍了在Android系统中不编写任何代码,通过设备树配置和内核支持的通用PWM LED驱动来实现基于PWM的LED设备驱动,并通过测试命令调整LED亮度级别。
71 0
不写一行代码(二):实现安卓基于PWM的LED设备驱动
|
5月前
|
Linux Android开发 C语言
不写一行代码(一):实现安卓基于GPIO的LED设备驱动
本文通过实践操作,展示了在Android系统中不编写任何代码,利用设备树(DTS)配置和内核支持的通用GPIO LED驱动来控制LED设备,并进一步通过C语言编写NDK测试APP来实现LED的闪烁效果。
225 0
不写一行代码(一):实现安卓基于GPIO的LED设备驱动
|
1月前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
55 19
|
1月前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
62 14
下一篇
开通oss服务